diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..0471c9ae --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,10 @@ +When generating TypeScript definitions, follow this advice: + +All types will go into marklogic.d.ts. + +Determining the output type is difficult. You have to look at the module containing the +implementation code and look for an "outputTransform". The implementation of that function +should reveal what the user-facing function will return. + +Please add "runtime" tests to the test-typescript directory. These tests should do +"smoke" tests that - critically - verify the output of each function. diff --git a/marklogic.d.ts b/marklogic.d.ts index c9b7e866..0de2cfde 100644 --- a/marklogic.d.ts +++ b/marklogic.d.ts @@ -128,6 +128,60 @@ declare module 'marklogic' { systemTime?: string; } + /** + * Result from a removeAll operation. + */ + export interface RemoveAllResult { + /** Always false (indicates removal operation completed) */ + exists: boolean; + /** Collection that was removed (if specified) */ + collection?: string; + /** Directory that was removed (if specified) */ + directory?: string; + /** Whether all documents were removed */ + allDocuments?: boolean; + } + + /** + * Result from a protect operation on a temporal document. + */ + export interface ProtectResult { + /** The URI of the protected document */ + uri: string; + /** The temporal collection name */ + temporalCollection: string; + /** The protection level (noWipe, noDelete, or noUpdate) */ + level: string; + } + + /** + * Result from a wipe operation on a temporal document. + */ + export interface WipeResult { + /** The URI of the wiped document */ + uri: string; + /** The temporal collection name */ + temporalCollection: string; + /** Whether the document was wiped */ + wiped: boolean; + } + + /** + * Result from advancing LSQT on a temporal collection. + */ + export interface AdvanceLsqtResult { + /** The new Last Stable Query Time */ + lsqt: string; + } + + /** + * Result from a patch operation. + */ + export interface PatchResult { + /** The URI of the patched document */ + uri: string; + } + /** * Result from a write operation. */ @@ -163,12 +217,158 @@ declare module 'marklogic' { */ write(documents: DocumentDescriptor | DocumentDescriptor[]): ResultProvider; + /** + * Writes one or more documents with additional parameters. + * @param params - Configuration object with documents and optional parameters + * @returns A result provider that resolves to a write result with document URIs + */ + write(params: { + /** The document(s) to write */ + documents: DocumentDescriptor | DocumentDescriptor[]; + /** Categories of information to write */ + categories?: string | string[]; + /** Transaction id or Transaction object */ + txid?: string | object; + /** Transform to apply on the server */ + transform?: string | object; + /** Forest name to write to */ + forestName?: string; + /** Temporal collection for temporal documents */ + temporalCollection?: string; + /** System time for temporal documents (ISO 8601 string or Date object) */ + systemTime?: string | Date; + }): 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; + + /** + * Removes all documents in a collection, directory, or database. + * Requires rest-admin role to delete all documents, rest-writer role otherwise. + * @param params - Configuration object with collection, directory, all, or txid properties + * @returns A result provider that resolves to a remove all result + */ + removeAll(params: { + /** The collection whose documents should be deleted */ + collection?: string; + /** A directory whose documents should be deleted */ + directory?: string; + /** Delete all documents (requires rest-admin role) */ + all?: boolean; + /** Transaction id or Transaction object */ + txid?: string | object; + }): ResultProvider; + + /** + * Protects a temporal document from certain operations. + * Must specify either duration or expireTime. + * @param params - Configuration object with either duration or expireTime + * @returns A result provider that resolves to protect result + */ + protect(params: { + /** The URI of the temporal document */ + uri: string; + /** The temporal collection name */ + temporalCollection: string; + /** Protection level: 'noWipe' | 'noDelete' | 'noUpdate' (default: 'noDelete') */ + level?: string; + /** Archive path for the document */ + archivePath?: string; + } & ( + { /** Duration as XSD duration string (e.g., 'P30D') */ duration: string; expireTime?: never; } | + { /** Expire time (alternative to duration) */ expireTime: string; duration?: never; } + )): ResultProvider; + + /** + * Deletes all versions of a temporal document. + * @param params - Configuration object with uri and temporalCollection + * @returns A result provider that resolves to wipe result + */ + wipe(params: { + /** The URI of the temporal document to wipe */ + uri: string; + /** The temporal collection name */ + temporalCollection: string; + }): ResultProvider; + + /** + * Advances the LSQT (Last Stable Query Time) of a temporal collection. + * @param params - Configuration object or temporal collection name + * @returns A result provider that resolves to the new LSQT + */ + advanceLsqt(params: string | { + /** The temporal collection name */ + temporalCollection: string; + /** Lag in seconds to subtract from maximum system start time */ + lag?: number; + }): ResultProvider; + + /** + * Creates a writable stream for writing large documents (typically binary) in incremental chunks. + * The document descriptor should NOT include a content property - content is written via the stream. + * @param document - Document descriptor without content property + * @returns A WritableStream with a result() method for tracking completion + */ + createWriteStream(document: { + /** The URI for the document to write to the database */ + uri: string; + /** Collections to which the document should belong */ + collections?: string[]; + /** Permissions controlling document access */ + permissions?: Array<{ roleName: string; capabilities: string[] }>; + /** Additional properties of the document */ + properties?: object; + /** Weight to increase or decrease document rank */ + quality?: number; + /** Metadata values of the document */ + metadataValues?: object; + /** Version identifier for optimistic locking */ + versionId?: number; + /** Transaction id or Transaction object */ + txid?: string | object; + /** Transform extension name or [name, params] array */ + transform?: string | [string, object]; + /** Content type of the document */ + contentType?: string; + }): NodeJS.WritableStream & ResultProvider; + + /** + * Applies changes to a document using patch operations. + * @param params - Configuration object with uri and operations + * @returns A result provider that resolves to patch result + */ + patch(params: { + /** The URI of the document to patch */ + uri: string; + /** Patch operations (from patchBuilder) or raw patch string/Buffer */ + operations: any[] | string | Buffer; + /** Categories of information to modify (typically 'content') */ + categories?: string | string[]; + /** Temporal collection name (for temporal documents) */ + temporalCollection?: string; + /** Temporal document URI */ + temporalDocument?: string; + /** Source document URI */ + sourceDocument?: string; + /** Transaction id or Transaction object */ + txid?: string | object; + /** Version identifier for optimistic locking */ + versionId?: string; + /** Format: 'json' or 'xml' */ + format?: string; + }): ResultProvider; + + /** + * Applies changes to a document using patch operations. + * @param uri - The URI of the document to patch + * @param operations - One or more patch operations from patchBuilder + * @returns A result provider that resolves to patch result + */ + patch(uri: string, ...operations: any[]): ResultProvider; } /** diff --git a/test-app/src/main/ml-config/security/users/rest-writer.json b/test-app/src/main/ml-config/security/users/rest-writer.json index 340ec3ba..3dfe87db 100644 --- a/test-app/src/main/ml-config/security/users/rest-writer.json +++ b/test-app/src/main/ml-config/security/users/rest-writer.json @@ -4,6 +4,7 @@ "password": "x", "role": [ "rest-writer", - "rest-evaluator" + "rest-evaluator", + "temporal-admin" ] -} \ No newline at end of file +} diff --git a/test-typescript/documents-runtime.test.ts b/test-typescript/documents-runtime.test.ts index b6b187b3..e9f49932 100644 --- a/test-typescript/documents-runtime.test.ts +++ b/test-typescript/documents-runtime.test.ts @@ -15,7 +15,7 @@ * Run with: npm run test:compile && npx mocha test-typescript/*.js */ -import should = require('should'); +const should = require('should'); import type { DatabaseClient } from 'marklogic'; const testConfig = require('../etc/test-config.js'); @@ -111,4 +111,191 @@ describe('Documents API runtime validation', function() { const probeResult = await client.documents.probe(testUri).result(); probeResult.exists.should.equal(false); }); + + it('removeAll() returns ResultProvider with RemoveAllResult', async function() { + const testCollection = 'typescript-test-collection'; + const testUri1 = '/test-typescript/removeAll-test-1.json'; + const testUri2 = '/test-typescript/removeAll-test-2.json'; + + // Write documents to a collection + await client.documents.write([ + { + uri: testUri1, + content: { test: 'doc1' }, + contentType: 'application/json', + collections: [testCollection] + }, + { + uri: testUri2, + content: { test: 'doc2' }, + contentType: 'application/json', + collections: [testCollection] + } + ]).result(); + + // Remove all documents in the collection + const resultProvider = client.documents.removeAll({ + collection: testCollection + }); + + // Verify ResultProvider has result() method + resultProvider.should.have.property('result'); + resultProvider.result.should.be.a.Function(); + + const result = await resultProvider.result(); + + // Verify RemoveAllResult structure + result.should.have.property('exists', false); + result.should.have.property('collection', testCollection); + + // Verify documents were actually removed + const probe1 = await client.documents.probe(testUri1).result(); + const probe2 = await client.documents.probe(testUri2).result(); + probe1.exists.should.equal(false); + probe2.exists.should.equal(false); + }); + + it('patch() returns ResultProvider with PatchResult', async function() { + const testUri = '/test-typescript/patch-test.json'; + + // Write a document first + await client.documents.write({ + uri: testUri, + content: { name: 'Original', count: 1 }, + contentType: 'application/json' + }).result(); + + // Patch the document using patchBuilder + const p = marklogic.patchBuilder; + const resultProvider = client.documents.patch( + testUri, + p.replace('/name', 'Updated'), + p.replace('/count', 2) + ); + + // Verify ResultProvider has result() method + resultProvider.should.have.property('result'); + resultProvider.result.should.be.a.Function(); + + const result = await resultProvider.result(); + + // Verify PatchResult structure + result.should.have.property('uri', testUri); + + // Verify document was actually patched + const docs = await client.documents.read(testUri).result(); + docs[0].content.should.have.property('name', 'Updated'); + docs[0].content.should.have.property('count', 2); + + // Clean up + await client.documents.remove(testUri).result(); + }); + + it('protect() returns ResultProvider with ProtectResult', async function() { + // Note: Requires temporal document to exist and may need temporal-admin role + this.skip(); + + const testUri = '/test-typescript/temporal-doc.json'; + const temporalCollection = 'temporalCollection'; + + const resultProvider = client.documents.protect({ + uri: testUri, + temporalCollection: temporalCollection, + duration: 'P30D', + level: 'noDelete' + }); + + resultProvider.should.have.property('result'); + resultProvider.result.should.be.a.Function(); + + const result = await resultProvider.result(); + + // Verify ProtectResult structure + result.should.have.property('uri', testUri); + result.should.have.property('temporalCollection', temporalCollection); + result.should.have.property('level', 'noDelete'); + }); + + it('wipe() returns ResultProvider with WipeResult', async function() { + // Note: Requires admin privileges and temporal document to exist + this.skip(); + + const testUri = '/test-typescript/temporal-wipe-doc.json'; + const temporalCollection = 'temporalCollection'; + + const resultProvider = client.documents.wipe({ + uri: testUri, + temporalCollection: temporalCollection + }); + + resultProvider.should.have.property('result'); + resultProvider.result.should.be.a.Function(); + + const result = await resultProvider.result(); + + // Verify WipeResult structure + result.should.have.property('uri', testUri); + result.should.have.property('temporalCollection', temporalCollection); + result.should.have.property('wiped', true); + }); + + it('advanceLsqt() returns ResultProvider with AdvanceLsqtResult', async function() { + // Note: Requires temporal-admin or admin role + this.skip(); + + const temporalCollection = 'temporalCollection'; + + const resultProvider = client.documents.advanceLsqt({ + temporalCollection: temporalCollection, + lag: 10 + }); + + resultProvider.should.have.property('result'); + resultProvider.result.should.be.a.Function(); + + const result = await resultProvider.result(); + + // Verify AdvanceLsqtResult structure + result.should.have.property('lsqt'); + result.lsqt.should.be.a.String(); + }); + + it('should write with createWriteStream and verify types', async function() { + const fs = require('fs'); + const path = require('path'); + const uri = '/test/typescript-stream.png'; + + // Use a small PNG from test data + const binaryPath = path.join(__dirname, '../test-basic/data/mlfavicon.png'); + + // Create write stream + const ws = client.documents.createWriteStream({ + uri: uri, + contentType: 'image/png', + collections: ['typescript-test'] + }); + + // Verify it's a writable stream + ws.should.have.property('write'); + ws.should.have.property('end'); + ws.should.have.property('result'); + + // Write the file through the stream + const writePromise = new Promise((resolve, reject) => { + ws.result((response: any) => { + response.should.have.property('documents'); + response.documents.length.should.equal(1); + response.documents[0].uri.should.equal(uri); + resolve(); + }).catch(reject); + + fs.createReadStream(binaryPath).pipe(ws); + }); + + await writePromise; + + // Verify document was written + const readResult = await client.documents.probe(uri).result(); + readResult.exists.should.equal(true); + }); }); diff --git a/typescript-test-project/test.ts b/typescript-test-project/test.ts index fe780ccf..53667f01 100644 --- a/typescript-test-project/test.ts +++ b/typescript-test-project/test.ts @@ -21,6 +21,41 @@ async function run() { const readResult = await client.documents.read(uri).result(); console.log('Read result', readResult); + + // Try out some temporal functions. + + const temporalUri = '/test/temporal.json'; + const temporalCollection = 'temporalCollection'; + + const temporalDoc = { + "hello": "world", + systemStartTime: '1111-11-11T11:11:11Z', + systemEndTime: '9999-12-31T23:59:59Z', + validStartTime: '1111-11-11T11:11:11Z', + validEndTime: '9999-12-31T23:59:59Z' + }; + + const writeResult = await client.documents.write({ + documents: [ + { + uri: temporalUri, content:temporalDoc, collections: ['other'], + permissions: [{"role-name": 'rest-reader', capabilities: ['read', 'update']}] + } + ], + temporalCollection: temporalCollection + }).result(); + console.log('write', writeResult); + + const protectResult = await client.documents.protect({ + uri: temporalUri, + temporalCollection: temporalCollection, + duration: 'P0D', + level: 'noWipe' + }).result(); + console.log('protectResult', protectResult); + + const wipeResult = await client.documents.wipe({uri: temporalUri, temporalCollection: temporalCollection}).result(); + console.log('wipe', wipeResult); } else { console.error(`❌ Connection failed: ${result.httpStatusCode} - ${result.httpStatusMessage}`); process.exit(1);