From ac43410ad747bcf9d2d3d8033547f4ce3b34a556 Mon Sep 17 00:00:00 2001 From: Rob Rudin Date: Thu, 20 Nov 2025 13:07:19 -0500 Subject: [PATCH] MLE-25554 TS for a handful of documents functions --- marklogic.d.ts | 105 +++++++++++ package.json | 5 +- test-typescript/documents-runtime.test.ts | 114 ++++++++++++ test-typescript/documents.test.ts | 208 ++++++++++++++++++++++ typescript-test-project/test.ts | 8 + 5 files changed, 438 insertions(+), 2 deletions(-) create mode 100644 test-typescript/documents-runtime.test.ts create mode 100644 test-typescript/documents.test.ts diff --git a/marklogic.d.ts b/marklogic.d.ts index 6c75f1a7..c9b7e866 100644 --- a/marklogic.d.ts +++ b/marklogic.d.ts @@ -72,11 +72,116 @@ declare module 'marklogic' { httpStatusMessage?: string; } + /** + * Generic document content type - can be JSON, XML, text, or binary + */ + export type DocumentContent = any; + + /** + * A document descriptor for reading or writing documents. + */ + export interface DocumentDescriptor { + /** The URI identifier for the document */ + uri: string; + /** The content of the document (JSON, XML, text, or Buffer for binary) */ + content?: DocumentContent; + /** The MIME type of the document */ + contentType?: string; + /** Collections to which the document belongs */ + collections?: string | string[]; + /** Permissions controlling document access */ + permissions?: Array<{ + 'role-name': string; + capabilities: string[]; + }>; + /** Properties (metadata) for the document */ + properties?: Record; + /** Quality ranking for the document */ + quality?: number; + /** Metadata values for the document */ + metadataValues?: Record; + } + + /** + * Result from a probe operation indicating if a document exists. + */ + export interface ProbeResult { + /** The URI of the document */ + uri: string; + /** Whether the document exists */ + exists: boolean; + /** Content type if document exists */ + contentType?: string; + /** Content length if document exists */ + contentLength?: number; + } + + /** + * Result from a remove operation. + */ + export interface RemoveResult { + /** Array of removed document URIs */ + uris: string[]; + /** Whether documents were removed */ + removed: boolean; + /** System time of the operation */ + systemTime?: string; + } + + /** + * Result from a write operation. + */ + export interface WriteResult { + /** Array of document descriptors with URIs of written documents */ + documents: DocumentDescriptor[]; + /** System time of the operation */ + systemTime?: string; + } + + /** + * Documents interface for reading and writing documents. + */ + export interface Documents { + /** + * Checks whether a document exists. + * @param uri - The URI of the document to check + * @returns A result provider that resolves to probe result + */ + probe(uri: string): ResultProvider; + + /** + * Reads one or more documents. + * @param uris - A URI string or array of URI strings + * @returns A result provider that resolves to an array of document descriptors + */ + read(uris: string | string[]): ResultProvider; + + /** + * Writes one or more documents. + * @param documents - A document descriptor or array of document descriptors + * @returns A result provider that resolves to a write result with document URIs + */ + write(documents: DocumentDescriptor | DocumentDescriptor[]): ResultProvider; + + /** + * Removes one or more documents. + * @param uris - A URI string or array of URI strings + * @returns A result provider that resolves to a remove result + */ + remove(uris: string | string[]): ResultProvider; + } + /** * A database client object returned by createDatabaseClient. * Provides access to document, graph, and query operations. */ export interface DatabaseClient { + /** + * Documents interface for reading and writing documents. + * @since 1.0 + */ + documents: Documents; + /** * Tests if a connection is successful. * Call .result() to get a promise. diff --git a/package.json b/package.json index f76806c4..66a473f5 100644 --- a/package.json +++ b/package.json @@ -18,9 +18,10 @@ "scripts": { "doc": "jsdoc -c jsdoc.json lib/*.js README.md", "lint": "gulp lint", + "pretest:typescript": "npm run test:compile", "test:types": "tsc --noEmit", - "test:compile": "tsc test-typescript/checkConnection-runtime.test.ts", - "pretest:typescript": "npm run test:compile" + "test:compile": "tsc test-typescript/*-runtime.test.ts", + "test:typescript": "npx mocha test-typescript/*.js" }, "keywords": [ "marklogic", diff --git a/test-typescript/documents-runtime.test.ts b/test-typescript/documents-runtime.test.ts new file mode 100644 index 00000000..b6b187b3 --- /dev/null +++ b/test-typescript/documents-runtime.test.ts @@ -0,0 +1,114 @@ +/* +* Copyright (c) 2015-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved. +*/ + +/// + +/** + * Runtime validation tests for Documents API. + * + * These tests make actual calls to MarkLogic to verify: + * - Type definitions match runtime behavior + * - ResultProvider pattern works correctly + * - Methods return expected data structures + * + * Run with: npm run test:compile && npx mocha test-typescript/*.js + */ + +import should = require('should'); +import type { DatabaseClient } from 'marklogic'; + +const testConfig = require('../etc/test-config.js'); +const marklogic = require('../lib/marklogic.js'); + +describe('Documents API runtime validation', function() { + let client: DatabaseClient; + const testUri = '/test-typescript/documents-runtime-test.json'; + const testContent = { message: 'TypeScript test document', timestamp: Date.now() }; + + before(function() { + client = marklogic.createDatabaseClient(testConfig.restWriterConnection); + }); + + after(function() { + client.release(); + }); + + it('write() returns ResultProvider with WriteResult', async function() { + const resultProvider = client.documents.write({ + uri: testUri, + content: testContent, + contentType: 'application/json' + }); + + // Verify ResultProvider has result() method + resultProvider.should.have.property('result'); + resultProvider.result.should.be.a.Function(); + + const writeResult = await resultProvider.result(); + + // Verify return type is WriteResult with documents array + writeResult.should.be.an.Object(); + writeResult.should.have.property('documents'); + writeResult.documents.should.be.an.Array(); + writeResult.documents.should.have.length(1); + writeResult.documents[0].should.have.property('uri'); + writeResult.documents[0].uri.should.equal(testUri); + }); + + it('probe() returns ResultProvider with ProbeResult', async function() { + const resultProvider = client.documents.probe(testUri); + + // Verify ResultProvider has result() method + resultProvider.should.have.property('result'); + resultProvider.result.should.be.a.Function(); + + const probeResult = await resultProvider.result(); + + // Verify ProbeResult structure + probeResult.should.have.property('uri', testUri); + probeResult.should.have.property('exists', true); + probeResult.should.have.property('contentType'); + probeResult.should.have.property('contentLength'); + }); + + it('read() returns ResultProvider with DocumentDescriptor array', async function() { + const resultProvider = client.documents.read(testUri); + + // Verify ResultProvider has result() method + resultProvider.should.have.property('result'); + resultProvider.result.should.be.a.Function(); + + const docs = await resultProvider.result(); + + // Verify return type is DocumentDescriptor array + docs.should.be.an.Array(); + docs.should.have.length(1); + + const doc = docs[0]; + doc.should.have.property('uri', testUri); + doc.should.have.property('content'); + doc.content.should.have.property('message', testContent.message); + }); + + it('remove() returns ResultProvider with RemoveResult', async function() { + const resultProvider = client.documents.remove(testUri); + + // Verify ResultProvider has result() method + resultProvider.should.have.property('result'); + resultProvider.result.should.be.a.Function(); + + const result = await resultProvider.result(); + + // Verify RemoveResult structure + result.should.have.property('uris'); + result.should.have.property('removed', true); + result.uris.should.be.an.Array(); + result.uris.should.have.length(1); + result.uris[0].should.equal(testUri); + + // Verify document was actually removed + const probeResult = await client.documents.probe(testUri).result(); + probeResult.exists.should.equal(false); + }); +}); diff --git a/test-typescript/documents.test.ts b/test-typescript/documents.test.ts new file mode 100644 index 00000000..de33b9fd --- /dev/null +++ b/test-typescript/documents.test.ts @@ -0,0 +1,208 @@ +/* +* Copyright (c) 2015-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved. +*/ + +/// + +/** + * TypeScript type checking tests for documents interface. + * + * This file validates: + * - probe() method + * - read() method + * - write() method + * - remove() method + * - DocumentDescriptor interface + * + * These tests are compiled but NOT executed - they verify type correctness only. + * Run with: npm run test:types + */ + +import type { DatabaseClient, DocumentDescriptor, ProbeResult, WriteResult } from 'marklogic'; + +// Test probe() method +async function testProbe(client: DatabaseClient) { + // Should accept a URI string + const resultProvider = client.documents.probe('/documents/test.json'); + const result = await resultProvider.result(); + + // Result should have correct shape + const uri: string = result.uri; + + if (result.exists) { + const contentType: string | undefined = result.contentType; + const contentLength: number | undefined = result.contentLength; + console.log(`Found document: ${uri} (${contentType}, ${contentLength} bytes)`); + } + + return result; +} + +// Test read() method with single URI +async function testReadSingle(client: DatabaseClient) { + // Should accept a single URI string + const resultProvider = client.documents.read('/documents/test.json'); + const docs = await resultProvider.result(); + + // Should return array of DocumentDescriptor + for (const doc of docs) { + const uri: string = doc.uri; + const content = doc.content; + console.log(`Read document: ${uri}`, content); + } + + return docs; +} + +// Test read() method with multiple URIs +async function testReadMultiple(client: DatabaseClient) { + // Should accept array of URI strings + const uris = ['/doc1.json', '/doc2.json', '/doc3.xml']; + const resultProvider = client.documents.read(uris); + const docs = await resultProvider.result(); + + return docs; +} + +// Test write() method with single document +async function testWriteSingle(client: DatabaseClient) { + // Should accept a single DocumentDescriptor + const document: DocumentDescriptor = { + uri: '/documents/new-doc.json', + content: { title: 'Test Document', value: 42 }, + contentType: 'application/json', + collections: ['test-collection'], + permissions: [ + { 'role-name': 'rest-reader', capabilities: ['read'] }, + { 'role-name': 'rest-writer', capabilities: ['read', 'update'] } + ], + quality: 10 + }; + + const resultProvider = client.documents.write(document); + const writeResult = await resultProvider.result(); + + // Should return WriteResult with documents array + const firstDoc = writeResult.documents[0]; + const firstUri: string = firstDoc.uri; + console.log(`Wrote document: ${firstUri}`); + + return writeResult; +} + +// Test write() method with multiple documents +async function testWriteMultiple(client: DatabaseClient) { + // Should accept array of DocumentDescriptor + const documents: DocumentDescriptor[] = [ + { + uri: '/doc1.json', + content: { name: 'Document 1' }, + collections: 'my-collection' // Can be single string + }, + { + uri: '/doc2.json', + content: { name: 'Document 2' }, + collections: ['col1', 'col2'] // Or array of strings + }, + { + uri: '/doc3.xml', + content: 'value', + contentType: 'application/xml', + properties: { + customProp: 'value', + timestamp: new Date().toISOString() + } + } + ]; + + const resultProvider = client.documents.write(documents); + const writeResult = await resultProvider.result(); + + // WriteResult contains documents array with uri property + const firstUri: string = writeResult.documents[0].uri; + console.log(`Wrote ${writeResult.documents.length} documents, first: ${firstUri}`); + + return writeResult; +} + +// Test remove() method with single URI +async function testRemoveSingle(client: DatabaseClient) { + // Should accept a single URI string + const resultProvider = client.documents.remove('/documents/to-delete.json'); + const uris = await resultProvider.result(); + + // Should return array of removed URIs + return uris; +} + +// Test remove() method with multiple URIs +async function testRemoveMultiple(client: DatabaseClient) { + // Should accept array of URI strings + const toRemove = ['/doc1.json', '/doc2.json', '/doc3.xml']; + const resultProvider = client.documents.remove(toRemove); + const uris = await resultProvider.result(); + + return uris; +} + +// Test complete workflow +async function testDocumentWorkflow(client: DatabaseClient) { + const uri = '/test/workflow-doc.json'; + + // Check if exists + const probeResult = await client.documents.probe(uri).result(); + if (probeResult.exists) { + console.log('Document already exists'); + } + + // Write document + await client.documents.write({ + uri, + content: { workflow: 'test', step: 1 }, + collections: ['workflow-tests'] + }).result(); + + // Read it back + const docs = await client.documents.read(uri).result(); + console.log('Read document:', docs[0].content); + + // Remove it + await client.documents.remove(uri).result(); + console.log('Document removed'); +} + +// ============================================================================= +// ERROR EXAMPLES - Uncomment to see TypeScript catch mistakes! +// ============================================================================= + +// ❌ Error: probe() requires string URI +// async function badProbe(client: DatabaseClient) { +// await client.documents.probe(123).result(); // Error: number not assignable to string +// } + +// ❌ Error: write() requires uri property +// async function badWrite(client: DatabaseClient) { +// await client.documents.write({ +// content: { data: 'test' } +// // Missing required 'uri' property! +// }).result(); +// } + +// ❌ Error: remove() requires string or string array +// async function badRemove(client: DatabaseClient) { +// await client.documents.remove(123).result(); // Error: number not valid +// } + +console.log('✅ Documents API type validation complete!'); + +// Export to prevent unused warnings +export { + testProbe, + testReadSingle, + testReadMultiple, + testWriteSingle, + testWriteMultiple, + testRemoveSingle, + testRemoveMultiple, + testDocumentWorkflow +}; diff --git a/typescript-test-project/test.ts b/typescript-test-project/test.ts index dc45f1fe..fe780ccf 100644 --- a/typescript-test-project/test.ts +++ b/typescript-test-project/test.ts @@ -13,6 +13,14 @@ async function run() { if (result.connected) { console.log('✅ Connected successfully!'); + + const uri = '/optic/test/albums1.json'; + + const probeResult = await client.documents.probe(uri).result(); + console.log('Probe result', probeResult); + + const readResult = await client.documents.read(uri).result(); + console.log('Read result', readResult); } else { console.error(`❌ Connection failed: ${result.httpStatusCode} - ${result.httpStatusMessage}`); process.exit(1);