@internetarchive/metadata-service 1.1.0 → 1.2.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/src/backend/default-metadata-backend.d.ts +8 -0
- package/dist/src/backend/default-metadata-backend.js +32 -0
- package/dist/src/backend/default-metadata-backend.js.map +1 -1
- package/dist/src/backend/metadata-backend-interface.d.ts +14 -0
- package/dist/src/backend/metadata-backend-interface.js.map +1 -1
- package/dist/src/metadata-patch.d.ts +21 -0
- package/dist/src/metadata-patch.js +37 -0
- package/dist/src/metadata-patch.js.map +1 -0
- package/dist/src/metadata-service-error.d.ts +3 -1
- package/dist/src/metadata-service-error.js +2 -0
- package/dist/src/metadata-service-error.js.map +1 -1
- package/dist/src/metadata-service-interface.d.ts +42 -0
- package/dist/src/metadata-service-interface.js.map +1 -1
- package/dist/src/metadata-service.d.ts +6 -1
- package/dist/src/metadata-service.js +28 -0
- package/dist/src/metadata-service.js.map +1 -1
- package/dist/test/default-metadata-backend.test.js +52 -0
- package/dist/test/default-metadata-backend.test.js.map +1 -1
- package/dist/test/metadata-patch.test.js +41 -0
- package/dist/test/metadata-patch.test.js.map +1 -0
- package/dist/test/metadata-service-write.test.js +96 -0
- package/dist/test/metadata-service-write.test.js.map +1 -0
- package/index.ts +9 -1
- package/package.json +1 -1
- package/src/backend/default-metadata-backend.ts +47 -0
- package/src/backend/metadata-backend-interface.ts +19 -0
- package/src/metadata-patch.ts +53 -0
- package/src/metadata-service-error.ts +2 -0
- package/src/metadata-service-interface.ts +56 -0
- package/src/metadata-service.ts +57 -1
- package/test/default-metadata-backend.test.ts +66 -0
- package/test/metadata-patch.test.ts +53 -0
- package/test/metadata-service-write.test.ts +138 -0
- package/.claude/settings.local.json +0 -16
- package/dist/src/models/file.d.ts +0 -29
- package/dist/src/models/file.js +0 -104
- package/dist/src/models/file.js.map +0 -1
- package/dist/src/models/review.d.ts +0 -11
- package/dist/src/models/review.js +0 -46
- package/dist/src/models/review.js.map +0 -1
- package/dist/src/models/speech-music-asr-entry.d.ts +0 -10
- package/dist/src/models/speech-music-asr-entry.js +0 -2
- package/dist/src/models/speech-music-asr-entry.js.map +0 -1
- package/dist/src/models/task.d.ts +0 -11
- package/dist/src/models/task.js +0 -2
- package/dist/src/models/task.js.map +0 -1
- package/dist/test/models/file.test.js +0 -57
- package/dist/test/models/file.test.js.map +0 -1
- package/dist/test/models/review.test.js +0 -53
- package/dist/test/models/review.test.js.map +0 -1
- /package/dist/test/{models/file.test.d.ts → metadata-patch.test.d.ts} +0 -0
- /package/dist/test/{models/review.test.d.ts → metadata-service-write.test.d.ts} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metadata-patch.test.js","sourceRoot":"","sources":["../../test/metadata-patch.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { expect } from '@open-wc/testing';\nimport { buildMetadataFieldPatch } from '../src/metadata-patch';\n\ndescribe('buildMetadataFieldPatch', () => {\n it('replaces an existing field', () => {\n expect(buildMetadataFieldPatch('title', 'New', 'Old')).to.deep.equal([\n { op: 'replace', path: '/title', value: 'New' },\n ]);\n });\n\n it('adds a field that is absent', () => {\n expect(buildMetadataFieldPatch('volume', ['2'], undefined)).to.deep.equal([\n { op: 'add', path: '/volume', value: '2' },\n ]);\n });\n\n it('writes a single-entry list as a bare string', () => {\n expect(\n buildMetadataFieldPatch('subject', ['one'], ['one', 'two']),\n ).to.deep.equal([{ op: 'replace', path: '/subject', value: 'one' }]);\n });\n\n it('keeps multiple values as a list', () => {\n expect(buildMetadataFieldPatch('subject', ['a', 'b'], 'a')).to.deep.equal([\n { op: 'replace', path: '/subject', value: ['a', 'b'] },\n ]);\n });\n\n it('removes a field set to nothing', () => {\n expect(buildMetadataFieldPatch('notes', [], 'old')).to.deep.equal([\n { op: 'remove', path: '/notes' },\n ]);\n expect(buildMetadataFieldPatch('notes', '', ['a', 'b'])).to.deep.equal([\n { op: 'remove', path: '/notes' },\n ]);\n });\n\n it('returns an empty patch when nothing changes', () => {\n expect(buildMetadataFieldPatch('title', ['Same'], 'Same')).to.deep.equal(\n [],\n );\n expect(buildMetadataFieldPatch('title', 'Same', ['Same'])).to.deep.equal(\n [],\n );\n expect(buildMetadataFieldPatch('notes', [], undefined)).to.deep.equal([]);\n });\n\n it('escapes the key as a JSON Pointer', () => {\n expect(buildMetadataFieldPatch('a/b~c', 'x', undefined)).to.deep.equal([\n { op: 'add', path: '/a~1b~0c', value: 'x' },\n ]);\n });\n});\n"]}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { expect } from '@open-wc/testing';
|
|
3
|
+
import { MetadataService } from '../src/metadata-service';
|
|
4
|
+
import { MetadataServiceError, MetadataServiceErrorType, } from '../src/metadata-service-error';
|
|
5
|
+
class WritableMockBackend {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.current = {};
|
|
8
|
+
this.writes = [];
|
|
9
|
+
}
|
|
10
|
+
async fetchMetadata(_identifier, keypath) {
|
|
11
|
+
if (this.readError)
|
|
12
|
+
return { error: this.readError };
|
|
13
|
+
if (keypath !== 'metadata')
|
|
14
|
+
return { success: {} };
|
|
15
|
+
return { success: { result: this.current } };
|
|
16
|
+
}
|
|
17
|
+
async writeMetadata(identifier, target, patch) {
|
|
18
|
+
if (this.writeError)
|
|
19
|
+
return { error: this.writeError };
|
|
20
|
+
this.writes.push({ identifier, target, patch });
|
|
21
|
+
return { success: { success: true, task_id: 42 } };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
describe('MetadataService writes', () => {
|
|
25
|
+
describe('patchMetadata', () => {
|
|
26
|
+
it('sends the patch to the metadata target by default', async () => {
|
|
27
|
+
const backend = new WritableMockBackend();
|
|
28
|
+
const service = new MetadataService(backend);
|
|
29
|
+
const patch = [{ op: 'remove', path: '/notes' }];
|
|
30
|
+
const result = await service.patchMetadata('foo', patch);
|
|
31
|
+
expect(result.success).to.deep.equal({ changed: true, taskId: 42 });
|
|
32
|
+
expect(backend.writes).to.deep.equal([
|
|
33
|
+
{ identifier: 'foo', target: 'metadata', patch },
|
|
34
|
+
]);
|
|
35
|
+
});
|
|
36
|
+
it('passes a backend error through', async () => {
|
|
37
|
+
var _a;
|
|
38
|
+
const backend = new WritableMockBackend();
|
|
39
|
+
backend.writeError = new MetadataServiceError(MetadataServiceErrorType.writeError, 'nope');
|
|
40
|
+
const service = new MetadataService(backend);
|
|
41
|
+
const result = await service.patchMetadata('foo', []);
|
|
42
|
+
expect((_a = result.error) === null || _a === void 0 ? void 0 : _a.message).to.equal('nope');
|
|
43
|
+
});
|
|
44
|
+
it('reports writeNotSupported for a backend without writes', async () => {
|
|
45
|
+
var _a;
|
|
46
|
+
const readOnlyBackend = {
|
|
47
|
+
fetchMetadata: async () => ({ success: {} }),
|
|
48
|
+
};
|
|
49
|
+
const service = new MetadataService(readOnlyBackend);
|
|
50
|
+
const result = await service.patchMetadata('foo', []);
|
|
51
|
+
expect((_a = result.error) === null || _a === void 0 ? void 0 : _a.type).to.equal(MetadataServiceErrorType.writeNotSupported);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
describe('updateMetadataField', () => {
|
|
55
|
+
it('replaces a field MDAPI has', async () => {
|
|
56
|
+
const backend = new WritableMockBackend();
|
|
57
|
+
backend.current = { title: 'Old' };
|
|
58
|
+
const service = new MetadataService(backend);
|
|
59
|
+
const result = await service.updateMetadataField('foo', 'title', 'New');
|
|
60
|
+
expect(result.success).to.deep.equal({ changed: true, taskId: 42 });
|
|
61
|
+
expect(backend.writes[0].patch).to.deep.equal([
|
|
62
|
+
{ op: 'replace', path: '/title', value: 'New' },
|
|
63
|
+
]);
|
|
64
|
+
});
|
|
65
|
+
it('adds a field MDAPI does not have', async () => {
|
|
66
|
+
const backend = new WritableMockBackend();
|
|
67
|
+
backend.current = { title: 'T' };
|
|
68
|
+
const service = new MetadataService(backend);
|
|
69
|
+
await service.updateMetadataField('foo', 'volume', ['2']);
|
|
70
|
+
expect(backend.writes[0].patch).to.deep.equal([
|
|
71
|
+
{ op: 'add', path: '/volume', value: '2' },
|
|
72
|
+
]);
|
|
73
|
+
});
|
|
74
|
+
it('skips the write when MDAPI already has the value', async () => {
|
|
75
|
+
const backend = new WritableMockBackend();
|
|
76
|
+
backend.current = { subject: ['a', 'b'] };
|
|
77
|
+
const service = new MetadataService(backend);
|
|
78
|
+
const result = await service.updateMetadataField('foo', 'subject', [
|
|
79
|
+
'a',
|
|
80
|
+
'b',
|
|
81
|
+
]);
|
|
82
|
+
expect(result.success).to.deep.equal({ changed: false });
|
|
83
|
+
expect(backend.writes).to.be.empty;
|
|
84
|
+
});
|
|
85
|
+
it('returns the read error without writing', async () => {
|
|
86
|
+
var _a;
|
|
87
|
+
const backend = new WritableMockBackend();
|
|
88
|
+
backend.readError = new MetadataServiceError(MetadataServiceErrorType.searchEngineError, "Couldn't locate item 'foo'");
|
|
89
|
+
const service = new MetadataService(backend);
|
|
90
|
+
const result = await service.updateMetadataField('foo', 'title', 'New');
|
|
91
|
+
expect((_a = result.error) === null || _a === void 0 ? void 0 : _a.message).to.equal("Couldn't locate item 'foo'");
|
|
92
|
+
expect(backend.writes).to.be.empty;
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
//# sourceMappingURL=metadata-service-write.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metadata-service-write.test.js","sourceRoot":"","sources":["../../test/metadata-service-write.test.ts"],"names":[],"mappings":"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","sourcesContent":["/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { expect } from '@open-wc/testing';\n\nimport { Result } from '@internetarchive/result-type';\nimport { MetadataBackendInterface } from '../src/backend/metadata-backend-interface';\nimport { MetadataService } from '../src/metadata-service';\nimport {\n MetadataServiceError,\n MetadataServiceErrorType,\n} from '../src/metadata-service-error';\nimport type { MetadataPatchOperation } from '../src/metadata-patch';\n\nclass WritableMockBackend implements MetadataBackendInterface {\n current: Record<string, unknown> = {};\n\n readError?: MetadataServiceError;\n\n writeError?: MetadataServiceError;\n\n writes: { identifier: string; target: string; patch: any[] }[] = [];\n\n async fetchMetadata(\n _identifier: string,\n keypath?: string,\n ): Promise<Result<any, MetadataServiceError>> {\n if (this.readError) return { error: this.readError };\n if (keypath !== 'metadata') return { success: {} };\n return { success: { result: this.current } };\n }\n\n async writeMetadata(\n identifier: string,\n target: string,\n patch: MetadataPatchOperation[],\n ): Promise<Result<any, MetadataServiceError>> {\n if (this.writeError) return { error: this.writeError };\n this.writes.push({ identifier, target, patch });\n return { success: { success: true, task_id: 42 } };\n }\n}\n\ndescribe('MetadataService writes', () => {\n describe('patchMetadata', () => {\n it('sends the patch to the metadata target by default', async () => {\n const backend = new WritableMockBackend();\n const service = new MetadataService(backend);\n const patch = [{ op: 'remove' as const, path: '/notes' }];\n\n const result = await service.patchMetadata('foo', patch);\n\n expect(result.success).to.deep.equal({ changed: true, taskId: 42 });\n expect(backend.writes).to.deep.equal([\n { identifier: 'foo', target: 'metadata', patch },\n ]);\n });\n\n it('passes a backend error through', async () => {\n const backend = new WritableMockBackend();\n backend.writeError = new MetadataServiceError(\n MetadataServiceErrorType.writeError,\n 'nope',\n );\n const service = new MetadataService(backend);\n\n const result = await service.patchMetadata('foo', []);\n\n expect(result.error?.message).to.equal('nope');\n });\n\n it('reports writeNotSupported for a backend without writes', async () => {\n const readOnlyBackend: MetadataBackendInterface = {\n fetchMetadata: async () => ({ success: {} }),\n };\n const service = new MetadataService(readOnlyBackend);\n\n const result = await service.patchMetadata('foo', []);\n\n expect(result.error?.type).to.equal(\n MetadataServiceErrorType.writeNotSupported,\n );\n });\n });\n\n describe('updateMetadataField', () => {\n it('replaces a field MDAPI has', async () => {\n const backend = new WritableMockBackend();\n backend.current = { title: 'Old' };\n const service = new MetadataService(backend);\n\n const result = await service.updateMetadataField('foo', 'title', 'New');\n\n expect(result.success).to.deep.equal({ changed: true, taskId: 42 });\n expect(backend.writes[0].patch).to.deep.equal([\n { op: 'replace', path: '/title', value: 'New' },\n ]);\n });\n\n it('adds a field MDAPI does not have', async () => {\n const backend = new WritableMockBackend();\n backend.current = { title: 'T' };\n const service = new MetadataService(backend);\n\n await service.updateMetadataField('foo', 'volume', ['2']);\n\n expect(backend.writes[0].patch).to.deep.equal([\n { op: 'add', path: '/volume', value: '2' },\n ]);\n });\n\n it('skips the write when MDAPI already has the value', async () => {\n const backend = new WritableMockBackend();\n backend.current = { subject: ['a', 'b'] };\n const service = new MetadataService(backend);\n\n const result = await service.updateMetadataField('foo', 'subject', [\n 'a',\n 'b',\n ]);\n\n expect(result.success).to.deep.equal({ changed: false });\n expect(backend.writes).to.be.empty;\n });\n\n it('returns the read error without writing', async () => {\n const backend = new WritableMockBackend();\n backend.readError = new MetadataServiceError(\n MetadataServiceErrorType.searchEngineError,\n \"Couldn't locate item 'foo'\",\n );\n const service = new MetadataService(backend);\n\n const result = await service.updateMetadataField('foo', 'title', 'New');\n\n expect(result.error?.message).to.equal(\"Couldn't locate item 'foo'\");\n expect(backend.writes).to.be.empty;\n });\n });\n});\n"]}
|
package/index.ts
CHANGED
|
@@ -13,4 +13,12 @@ export {
|
|
|
13
13
|
export { DefaultMetadataBackend } from './src/backend/default-metadata-backend';
|
|
14
14
|
export { MetadataService } from './src/metadata-service';
|
|
15
15
|
export { MetadataServiceError } from './src/metadata-service-error';
|
|
16
|
-
export {
|
|
16
|
+
export {
|
|
17
|
+
MetadataServiceInterface,
|
|
18
|
+
MetadataWriteResult,
|
|
19
|
+
} from './src/metadata-service-interface';
|
|
20
|
+
export { buildMetadataFieldPatch } from './src/metadata-patch';
|
|
21
|
+
export type {
|
|
22
|
+
MetadataFieldValue,
|
|
23
|
+
MetadataPatchOperation,
|
|
24
|
+
} from './src/metadata-patch';
|
package/package.json
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
MetadataServiceErrorType,
|
|
6
6
|
} from '../metadata-service-error';
|
|
7
7
|
import { MetadataBackendInterface } from './metadata-backend-interface';
|
|
8
|
+
import type { MetadataPatchOperation } from '../metadata-patch';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* The DefaultSearchBackend performs a `window.fetch` request to archive.org
|
|
@@ -56,6 +57,52 @@ export class DefaultMetadataBackend implements MetadataBackendInterface {
|
|
|
56
57
|
return this.fetchUrl(url);
|
|
57
58
|
}
|
|
58
59
|
|
|
60
|
+
/**
|
|
61
|
+
* @inheritdoc
|
|
62
|
+
*
|
|
63
|
+
* Makes a single attempt: a write that timed out may still have queued its
|
|
64
|
+
* task, and retrying would queue another.
|
|
65
|
+
*/
|
|
66
|
+
async writeMetadata(
|
|
67
|
+
identifier: string,
|
|
68
|
+
target: string,
|
|
69
|
+
patch: MetadataPatchOperation[],
|
|
70
|
+
): Promise<Result<any, MetadataServiceError>> {
|
|
71
|
+
const url = `https://${this.baseUrl}/metadata/${identifier}`;
|
|
72
|
+
const body = new URLSearchParams({
|
|
73
|
+
'-target': target,
|
|
74
|
+
'-patch': JSON.stringify(patch),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const result = await this.fetchUrl(url, {
|
|
78
|
+
requestOptions: {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
credentials: this.includeCredentials ? 'include' : 'same-origin',
|
|
81
|
+
headers: {
|
|
82
|
+
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
|
83
|
+
},
|
|
84
|
+
body,
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// MDAPI reports a rejected write as `{ success: false, error }`
|
|
89
|
+
if (result.error?.type === MetadataServiceErrorType.searchEngineError) {
|
|
90
|
+
return this.getErrorResult(
|
|
91
|
+
MetadataServiceErrorType.writeError,
|
|
92
|
+
result.error.message,
|
|
93
|
+
result.error.details,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
if (result.success && result.success.success !== true) {
|
|
97
|
+
return this.getErrorResult(
|
|
98
|
+
MetadataServiceErrorType.writeError,
|
|
99
|
+
'Metadata write was not accepted',
|
|
100
|
+
result.success,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
|
|
59
106
|
/**
|
|
60
107
|
* Fires a request to the URL (with this backend's options applied) and
|
|
61
108
|
* asynchronously returns a Result object containing either the raw response
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
import type { Result } from '@internetarchive/result-type';
|
|
3
3
|
import { MetadataServiceError } from '../metadata-service-error';
|
|
4
|
+
import type { MetadataPatchOperation } from '../metadata-patch';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* An interface to provide the network layer to the `MetadataService`.
|
|
@@ -22,4 +23,22 @@ export interface MetadataBackendInterface {
|
|
|
22
23
|
identifier: string,
|
|
23
24
|
keypath?: string,
|
|
24
25
|
): Promise<Result<any, MetadataServiceError>>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Apply a JSON Patch to one of an item's targets (e.g. `metadata`) through
|
|
29
|
+
* the MDAPI write endpoint, as the logged-in user. Resolves with MDAPI's raw
|
|
30
|
+
* response (`{ success, task_id, log }`) on success.
|
|
31
|
+
*
|
|
32
|
+
* Optional so backends written before writes existed keep compiling; the
|
|
33
|
+
* service reports `writeNotSupported` for a backend without it.
|
|
34
|
+
*
|
|
35
|
+
* @param identifier
|
|
36
|
+
* @param target
|
|
37
|
+
* @param patch
|
|
38
|
+
*/
|
|
39
|
+
writeMetadata?(
|
|
40
|
+
identifier: string,
|
|
41
|
+
target: string,
|
|
42
|
+
patch: MetadataPatchOperation[],
|
|
43
|
+
): Promise<Result<any, MetadataServiceError>>;
|
|
25
44
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** A metadata field value as MDAPI stores it: one string, or a list of them */
|
|
2
|
+
export type MetadataFieldValue = string | string[];
|
|
3
|
+
|
|
4
|
+
/** One JSON Patch (RFC 6902) operation, as MDAPI's write endpoint accepts */
|
|
5
|
+
export type MetadataPatchOperation =
|
|
6
|
+
| { op: 'add' | 'replace'; path: string; value: unknown }
|
|
7
|
+
| { op: 'remove'; path: string };
|
|
8
|
+
|
|
9
|
+
/** Escapes a metadata key for use as a JSON Pointer segment (RFC 6901) */
|
|
10
|
+
function pointerFor(field: string): string {
|
|
11
|
+
return `/${field.replace(/~/g, '~0').replace(/\//g, '~1')}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** MDAPI's storage shape: a one-entry list collapses to a bare string */
|
|
15
|
+
function normalize(
|
|
16
|
+
value: MetadataFieldValue | undefined,
|
|
17
|
+
): MetadataFieldValue | undefined {
|
|
18
|
+
return Array.isArray(value) && value.length === 1 ? value[0] : value;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isEmpty(value: MetadataFieldValue | undefined): boolean {
|
|
22
|
+
if (value == null) return true;
|
|
23
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
24
|
+
return value === '';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Builds the JSON Patch that sets one metadata field to `value`, given the
|
|
29
|
+
* field's current value.
|
|
30
|
+
*
|
|
31
|
+
* A single-entry list is written as a bare string, which is how MDAPI stores
|
|
32
|
+
* a one-value field, and an empty string or list removes the field. Returns
|
|
33
|
+
* an empty patch when nothing changes, since MDAPI rejects a write that
|
|
34
|
+
* doesn't modify the item.
|
|
35
|
+
*/
|
|
36
|
+
export function buildMetadataFieldPatch(
|
|
37
|
+
field: string,
|
|
38
|
+
value: MetadataFieldValue,
|
|
39
|
+
current: MetadataFieldValue | undefined,
|
|
40
|
+
): MetadataPatchOperation[] {
|
|
41
|
+
const path = pointerFor(field);
|
|
42
|
+
const next = normalize(value);
|
|
43
|
+
const existing = normalize(current);
|
|
44
|
+
|
|
45
|
+
if (next == null || isEmpty(next)) {
|
|
46
|
+
return isEmpty(existing) ? [] : [{ op: 'remove', path }];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (JSON.stringify(next) === JSON.stringify(existing)) return [];
|
|
50
|
+
|
|
51
|
+
const op = isEmpty(existing) ? 'add' : 'replace';
|
|
52
|
+
return [{ op, path, value: next }];
|
|
53
|
+
}
|
|
@@ -3,6 +3,8 @@ export enum MetadataServiceErrorType {
|
|
|
3
3
|
itemNotFound = 'MetadataService.ItemNotFound',
|
|
4
4
|
decodingError = 'MetadataService.DecodingError',
|
|
5
5
|
searchEngineError = 'MetadataService.SearchEngineError',
|
|
6
|
+
writeError = 'MetadataService.WriteError',
|
|
7
|
+
writeNotSupported = 'MetadataService.WriteNotSupported',
|
|
6
8
|
}
|
|
7
9
|
|
|
8
10
|
export class MetadataServiceError extends Error {
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import type { Result } from '@internetarchive/result-type';
|
|
2
2
|
import type { MetadataServiceError } from './metadata-service-error';
|
|
3
3
|
import type { MetadataResponse } from './responses/metadata-response';
|
|
4
|
+
import type {
|
|
5
|
+
MetadataFieldValue,
|
|
6
|
+
MetadataPatchOperation,
|
|
7
|
+
} from './metadata-patch';
|
|
8
|
+
|
|
9
|
+
export type MetadataWriteResult = {
|
|
10
|
+
/** Whether a write was sent. False when the field already had the value. */
|
|
11
|
+
changed: boolean;
|
|
12
|
+
/** The catalog task that will apply the change to the item */
|
|
13
|
+
taskId?: number;
|
|
14
|
+
};
|
|
4
15
|
|
|
5
16
|
export interface MetadataServiceInterface {
|
|
6
17
|
/**
|
|
@@ -48,4 +59,49 @@ export interface MetadataServiceInterface {
|
|
|
48
59
|
identifier: string,
|
|
49
60
|
keypath: string,
|
|
50
61
|
): Promise<Result<T, MetadataServiceError>>;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Apply a JSON Patch to an item's metadata as the logged-in user.
|
|
65
|
+
*
|
|
66
|
+
* MDAPI applies the change through a queued catalog task. MDAPI reads
|
|
67
|
+
* replay pending tasks' patches ("lookahead"), so they show the change right
|
|
68
|
+
* away, but caches built on MDAPI (search, page services) can lag it. MDAPI
|
|
69
|
+
* rejects a patch that doesn't change the item.
|
|
70
|
+
*
|
|
71
|
+
* ```ts
|
|
72
|
+
* await metadataService.patchMetadata('goody', [
|
|
73
|
+
* { op: 'replace', path: '/title', value: 'Goody Two-Shoes' },
|
|
74
|
+
* ]);
|
|
75
|
+
* ```
|
|
76
|
+
*
|
|
77
|
+
* @param identifier
|
|
78
|
+
* @param patch JSON Patch operations against the item's metadata
|
|
79
|
+
* @param target MDAPI write target, `metadata` by default
|
|
80
|
+
*/
|
|
81
|
+
patchMetadata(
|
|
82
|
+
identifier: string,
|
|
83
|
+
patch: MetadataPatchOperation[],
|
|
84
|
+
target?: string,
|
|
85
|
+
): Promise<Result<MetadataWriteResult, MetadataServiceError>>;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Set one metadata field on an item as the logged-in user. An empty string
|
|
89
|
+
* or list removes the field.
|
|
90
|
+
*
|
|
91
|
+
* Reads the item's current metadata from MDAPI first and patches against
|
|
92
|
+
* that, so the right add / replace / remove goes out even when the caller's
|
|
93
|
+
* copy is stale. That read includes this user's still-queued writes through
|
|
94
|
+
* MDAPI's lookahead, except for tasks lookahead skips (e.g. ones held for an
|
|
95
|
+
* admin). Resolves with `changed: false`, without writing, when the field
|
|
96
|
+
* already has the value.
|
|
97
|
+
*
|
|
98
|
+
* @param identifier
|
|
99
|
+
* @param field The metadata key, e.g. `title`
|
|
100
|
+
* @param value
|
|
101
|
+
*/
|
|
102
|
+
updateMetadataField(
|
|
103
|
+
identifier: string,
|
|
104
|
+
field: string,
|
|
105
|
+
value: MetadataFieldValue,
|
|
106
|
+
): Promise<Result<MetadataWriteResult, MetadataServiceError>>;
|
|
51
107
|
}
|
package/src/metadata-service.ts
CHANGED
|
@@ -5,7 +5,15 @@ import {
|
|
|
5
5
|
MetadataServiceError,
|
|
6
6
|
MetadataServiceErrorType,
|
|
7
7
|
} from './metadata-service-error';
|
|
8
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
MetadataServiceInterface,
|
|
10
|
+
MetadataWriteResult,
|
|
11
|
+
} from './metadata-service-interface';
|
|
12
|
+
import {
|
|
13
|
+
buildMetadataFieldPatch,
|
|
14
|
+
type MetadataFieldValue,
|
|
15
|
+
type MetadataPatchOperation,
|
|
16
|
+
} from './metadata-patch';
|
|
9
17
|
import { MetadataResponse } from './responses/metadata-response';
|
|
10
18
|
|
|
11
19
|
/**
|
|
@@ -60,4 +68,52 @@ export class MetadataService implements MetadataServiceInterface {
|
|
|
60
68
|
|
|
61
69
|
return { success: result.success.result };
|
|
62
70
|
}
|
|
71
|
+
|
|
72
|
+
/** @inheritdoc */
|
|
73
|
+
async patchMetadata(
|
|
74
|
+
identifier: string,
|
|
75
|
+
patch: MetadataPatchOperation[],
|
|
76
|
+
target = 'metadata',
|
|
77
|
+
): Promise<Result<MetadataWriteResult, MetadataServiceError>> {
|
|
78
|
+
if (!this.backend.writeMetadata) {
|
|
79
|
+
return {
|
|
80
|
+
error: new MetadataServiceError(
|
|
81
|
+
MetadataServiceErrorType.writeNotSupported,
|
|
82
|
+
'This metadata backend does not support writes',
|
|
83
|
+
),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const result = await this.backend.writeMetadata(identifier, target, patch);
|
|
88
|
+
if (result.error) {
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { success: { changed: true, taskId: result.success?.task_id } };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** @inheritdoc */
|
|
96
|
+
async updateMetadataField(
|
|
97
|
+
identifier: string,
|
|
98
|
+
field: string,
|
|
99
|
+
value: MetadataFieldValue,
|
|
100
|
+
): Promise<Result<MetadataWriteResult, MetadataServiceError>> {
|
|
101
|
+
const current = await this.fetchMetadataValue<
|
|
102
|
+
Record<string, MetadataFieldValue>
|
|
103
|
+
>(identifier, 'metadata');
|
|
104
|
+
if (current.error) {
|
|
105
|
+
return { error: current.error };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const patch = buildMetadataFieldPatch(
|
|
109
|
+
field,
|
|
110
|
+
value,
|
|
111
|
+
current.success?.[field],
|
|
112
|
+
);
|
|
113
|
+
if (patch.length === 0) {
|
|
114
|
+
return { success: { changed: false } };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return this.patchMetadata(identifier, patch);
|
|
118
|
+
}
|
|
63
119
|
}
|
|
@@ -102,3 +102,69 @@ describe('DefaultMetadataBackend', () => {
|
|
|
102
102
|
window.fetch = fetchBackup;
|
|
103
103
|
});
|
|
104
104
|
});
|
|
105
|
+
|
|
106
|
+
describe('DefaultMetadataBackend writeMetadata', () => {
|
|
107
|
+
const fetchBackup = window.fetch;
|
|
108
|
+
|
|
109
|
+
afterEach(() => {
|
|
110
|
+
window.fetch = fetchBackup;
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('posts the patch as a form to the item endpoint', async () => {
|
|
114
|
+
let requestUrl = '';
|
|
115
|
+
let requestInit: RequestInit | undefined;
|
|
116
|
+
window.fetch = async (
|
|
117
|
+
input: RequestInfo | URL,
|
|
118
|
+
init?: RequestInit,
|
|
119
|
+
): Promise<Response> => {
|
|
120
|
+
requestUrl = String(input);
|
|
121
|
+
requestInit = init;
|
|
122
|
+
return new Response('{ "success": true, "task_id": 7, "log": "x" }');
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const backend = new DefaultMetadataBackend({ includeCredentials: true });
|
|
126
|
+
const patch = [{ op: 'replace' as const, path: '/title', value: 'New' }];
|
|
127
|
+
const result = await backend.writeMetadata('foo', 'metadata', patch);
|
|
128
|
+
|
|
129
|
+
expect(result.success?.task_id).to.equal(7);
|
|
130
|
+
expect(requestUrl).to.equal('https://archive.org/metadata/foo');
|
|
131
|
+
expect(requestInit?.method).to.equal('POST');
|
|
132
|
+
expect(requestInit?.credentials).to.equal('include');
|
|
133
|
+
const body = requestInit?.body as URLSearchParams;
|
|
134
|
+
expect(body.get('-target')).to.equal('metadata');
|
|
135
|
+
expect(JSON.parse(body.get('-patch') ?? '')).to.deep.equal(patch);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('returns a writeError with MDAPI’s message when the write is rejected', async () => {
|
|
139
|
+
window.fetch = async (): Promise<Response> =>
|
|
140
|
+
new Response('{ "success": false, "error": "Authorization failed" }', {
|
|
141
|
+
status: 401,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const backend = new DefaultMetadataBackend();
|
|
145
|
+
const result = await backend.writeMetadata('foo', 'metadata', []);
|
|
146
|
+
|
|
147
|
+
expect(result.error?.type).to.equal(MetadataServiceErrorType.writeError);
|
|
148
|
+
expect(result.error?.message).to.equal('Authorization failed');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('returns a writeError when MDAPI does not report success', async () => {
|
|
152
|
+
window.fetch = async (): Promise<Response> => new Response('{}');
|
|
153
|
+
|
|
154
|
+
const backend = new DefaultMetadataBackend();
|
|
155
|
+
const result = await backend.writeMetadata('foo', 'metadata', []);
|
|
156
|
+
|
|
157
|
+
expect(result.error?.type).to.equal(MetadataServiceErrorType.writeError);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('returns a networkError when the request fails', async () => {
|
|
161
|
+
window.fetch = async (): Promise<Response> => {
|
|
162
|
+
throw new Error('offline');
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const backend = new DefaultMetadataBackend();
|
|
166
|
+
const result = await backend.writeMetadata('foo', 'metadata', []);
|
|
167
|
+
|
|
168
|
+
expect(result.error?.type).to.equal(MetadataServiceErrorType.networkError);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { expect } from '@open-wc/testing';
|
|
2
|
+
import { buildMetadataFieldPatch } from '../src/metadata-patch';
|
|
3
|
+
|
|
4
|
+
describe('buildMetadataFieldPatch', () => {
|
|
5
|
+
it('replaces an existing field', () => {
|
|
6
|
+
expect(buildMetadataFieldPatch('title', 'New', 'Old')).to.deep.equal([
|
|
7
|
+
{ op: 'replace', path: '/title', value: 'New' },
|
|
8
|
+
]);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('adds a field that is absent', () => {
|
|
12
|
+
expect(buildMetadataFieldPatch('volume', ['2'], undefined)).to.deep.equal([
|
|
13
|
+
{ op: 'add', path: '/volume', value: '2' },
|
|
14
|
+
]);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('writes a single-entry list as a bare string', () => {
|
|
18
|
+
expect(
|
|
19
|
+
buildMetadataFieldPatch('subject', ['one'], ['one', 'two']),
|
|
20
|
+
).to.deep.equal([{ op: 'replace', path: '/subject', value: 'one' }]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('keeps multiple values as a list', () => {
|
|
24
|
+
expect(buildMetadataFieldPatch('subject', ['a', 'b'], 'a')).to.deep.equal([
|
|
25
|
+
{ op: 'replace', path: '/subject', value: ['a', 'b'] },
|
|
26
|
+
]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('removes a field set to nothing', () => {
|
|
30
|
+
expect(buildMetadataFieldPatch('notes', [], 'old')).to.deep.equal([
|
|
31
|
+
{ op: 'remove', path: '/notes' },
|
|
32
|
+
]);
|
|
33
|
+
expect(buildMetadataFieldPatch('notes', '', ['a', 'b'])).to.deep.equal([
|
|
34
|
+
{ op: 'remove', path: '/notes' },
|
|
35
|
+
]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('returns an empty patch when nothing changes', () => {
|
|
39
|
+
expect(buildMetadataFieldPatch('title', ['Same'], 'Same')).to.deep.equal(
|
|
40
|
+
[],
|
|
41
|
+
);
|
|
42
|
+
expect(buildMetadataFieldPatch('title', 'Same', ['Same'])).to.deep.equal(
|
|
43
|
+
[],
|
|
44
|
+
);
|
|
45
|
+
expect(buildMetadataFieldPatch('notes', [], undefined)).to.deep.equal([]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('escapes the key as a JSON Pointer', () => {
|
|
49
|
+
expect(buildMetadataFieldPatch('a/b~c', 'x', undefined)).to.deep.equal([
|
|
50
|
+
{ op: 'add', path: '/a~1b~0c', value: 'x' },
|
|
51
|
+
]);
|
|
52
|
+
});
|
|
53
|
+
});
|