@internetarchive/metadata-service 1.0.6 → 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.
Files changed (67) hide show
  1. package/README.md +18 -0
  2. package/dist/index.d.ts +4 -1
  3. package/dist/index.js +1 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/src/backend/default-metadata-backend.d.ts +8 -0
  6. package/dist/src/backend/default-metadata-backend.js +32 -0
  7. package/dist/src/backend/default-metadata-backend.js.map +1 -1
  8. package/dist/src/backend/metadata-backend-interface.d.ts +14 -0
  9. package/dist/src/backend/metadata-backend-interface.js.map +1 -1
  10. package/dist/src/metadata-patch.d.ts +21 -0
  11. package/dist/src/metadata-patch.js +37 -0
  12. package/dist/src/metadata-patch.js.map +1 -0
  13. package/dist/src/metadata-service-error.d.ts +3 -1
  14. package/dist/src/metadata-service-error.js +2 -0
  15. package/dist/src/metadata-service-error.js.map +1 -1
  16. package/dist/src/metadata-service-interface.d.ts +42 -0
  17. package/dist/src/metadata-service-interface.js.map +1 -1
  18. package/dist/src/metadata-service.d.ts +6 -1
  19. package/dist/src/metadata-service.js +28 -0
  20. package/dist/src/metadata-service.js.map +1 -1
  21. package/dist/src/responses/metadata-response.d.ts +22 -0
  22. package/dist/src/responses/metadata-response.js +5 -0
  23. package/dist/src/responses/metadata-response.js.map +1 -1
  24. package/dist/test/default-metadata-backend.test.js +52 -0
  25. package/dist/test/default-metadata-backend.test.js.map +1 -1
  26. package/dist/test/metadata-patch.test.js +41 -0
  27. package/dist/test/metadata-patch.test.js.map +1 -0
  28. package/dist/test/metadata-service-write.test.js +96 -0
  29. package/dist/test/metadata-service-write.test.js.map +1 -0
  30. package/dist/test/mock-response-generator.js +11 -0
  31. package/dist/test/mock-response-generator.js.map +1 -1
  32. package/dist/test/responses/metadata-response.test.d.ts +1 -0
  33. package/dist/test/responses/metadata-response.test.js +48 -0
  34. package/dist/test/responses/metadata-response.test.js.map +1 -0
  35. package/index.ts +13 -1
  36. package/package.json +2 -2
  37. package/src/backend/default-metadata-backend.ts +47 -0
  38. package/src/backend/metadata-backend-interface.ts +19 -0
  39. package/src/metadata-patch.ts +53 -0
  40. package/src/metadata-service-error.ts +2 -0
  41. package/src/metadata-service-interface.ts +56 -0
  42. package/src/metadata-service.ts +57 -1
  43. package/src/responses/metadata-response.ts +34 -0
  44. package/test/default-metadata-backend.test.ts +66 -0
  45. package/test/metadata-patch.test.ts +53 -0
  46. package/test/metadata-service-write.test.ts +138 -0
  47. package/test/mock-response-generator.ts +11 -0
  48. package/test/responses/metadata-response.test.ts +55 -0
  49. package/.claude/settings.local.json +0 -15
  50. package/dist/src/models/file.d.ts +0 -29
  51. package/dist/src/models/file.js +0 -104
  52. package/dist/src/models/file.js.map +0 -1
  53. package/dist/src/models/review.d.ts +0 -11
  54. package/dist/src/models/review.js +0 -46
  55. package/dist/src/models/review.js.map +0 -1
  56. package/dist/src/models/speech-music-asr-entry.d.ts +0 -10
  57. package/dist/src/models/speech-music-asr-entry.js +0 -2
  58. package/dist/src/models/speech-music-asr-entry.js.map +0 -1
  59. package/dist/src/models/task.d.ts +0 -11
  60. package/dist/src/models/task.js +0 -2
  61. package/dist/src/models/task.js.map +0 -1
  62. package/dist/test/models/file.test.js +0 -57
  63. package/dist/test/models/file.test.js.map +0 -1
  64. package/dist/test/models/review.test.js +0 -53
  65. package/dist/test/models/review.test.js.map +0 -1
  66. /package/dist/test/{models/file.test.d.ts → metadata-patch.test.d.ts} +0 -0
  67. /package/dist/test/{models/review.test.d.ts → metadata-service-write.test.d.ts} +0 -0
package/README.md CHANGED
@@ -19,6 +19,24 @@ metadataResponse.metadata.collection.value // => 'some-collection'
19
19
  metadataResponse.metadata.collection.values // => ['some-collection', 'another-collection', 'more-collections']
20
20
  ```
21
21
 
22
+ ### Write Metadata
23
+
24
+ Writes go through MDAPI as the logged-in user, so they need the viewer's archive.org cookies (`includeCredentials`). MDAPI applies a write through a queued catalog task. MDAPI reads replay pending tasks, so they show the change right away, but search and other caches built on MDAPI can lag it.
25
+
26
+ ```ts
27
+ // Set one field. Reads the item's current metadata first and sends the
28
+ // right add / replace / remove. An empty string or list removes the field.
29
+ const result = await metadataService.updateMetadataField('some-identifier', 'subject', ['cats', 'dogs']);
30
+ result.success // => { changed: true, taskId: 12345 }, or { changed: false } if it already had that value
31
+
32
+ // Or send a JSON Patch yourself
33
+ await metadataService.patchMetadata('some-identifier', [
34
+ { op: 'replace', path: '/title', value: 'A new title' },
35
+ ]);
36
+ ```
37
+
38
+ Failed writes come back as a `MetadataServiceError` with type `writeError` and MDAPI's message.
39
+
22
40
  ## Metadata Values
23
41
 
24
42
  Internet Archive Metadata is expansive and nearly all metadata fields can be returned as either an array, string, or number.
package/dist/index.d.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  export { MetadataResponse } from './src/responses/metadata-response';
2
+ export type { AlternateLocation, AlternateLocations, } from './src/responses/metadata-response';
2
3
  export { File, Review, SpeechMusicASREntry, } from '@internetarchive/iaux-item-metadata';
3
4
  export { DefaultMetadataBackend } from './src/backend/default-metadata-backend';
4
5
  export { MetadataService } from './src/metadata-service';
5
6
  export { MetadataServiceError } from './src/metadata-service-error';
6
- export { MetadataServiceInterface } from './src/metadata-service-interface';
7
+ export { MetadataServiceInterface, MetadataWriteResult, } from './src/metadata-service-interface';
8
+ export { buildMetadataFieldPatch } from './src/metadata-patch';
9
+ export type { MetadataFieldValue, MetadataPatchOperation, } from './src/metadata-patch';
package/dist/index.js CHANGED
@@ -3,4 +3,5 @@ export { File, Review, } from '@internetarchive/iaux-item-metadata';
3
3
  export { DefaultMetadataBackend } from './src/backend/default-metadata-backend';
4
4
  export { MetadataService } from './src/metadata-service';
5
5
  export { MetadataServiceError } from './src/metadata-service-error';
6
+ export { buildMetadataFieldPatch } from './src/metadata-patch';
6
7
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AAErE,OAAO,EACL,IAAI,EACJ,MAAM,GAEP,MAAM,qCAAqC,CAAC;AAE7C,OAAO,EAAE,sBAAsB,EAAE,MAAM,wCAAwC,CAAC;AAChF,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC","sourcesContent":["export { MetadataResponse } from './src/responses/metadata-response';\n\nexport {\n File,\n Review,\n SpeechMusicASREntry,\n} from '@internetarchive/iaux-item-metadata';\n\nexport { DefaultMetadataBackend } from './src/backend/default-metadata-backend';\nexport { MetadataService } from './src/metadata-service';\nexport { MetadataServiceError } from './src/metadata-service-error';\nexport { MetadataServiceInterface } from './src/metadata-service-interface';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AAMrE,OAAO,EACL,IAAI,EACJ,MAAM,GAEP,MAAM,qCAAqC,CAAC;AAE7C,OAAO,EAAE,sBAAsB,EAAE,MAAM,wCAAwC,CAAC;AAChF,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AAKpE,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC","sourcesContent":["export { MetadataResponse } from './src/responses/metadata-response';\nexport type {\n AlternateLocation,\n AlternateLocations,\n} from './src/responses/metadata-response';\n\nexport {\n File,\n Review,\n SpeechMusicASREntry,\n} from '@internetarchive/iaux-item-metadata';\n\nexport { DefaultMetadataBackend } from './src/backend/default-metadata-backend';\nexport { MetadataService } from './src/metadata-service';\nexport { MetadataServiceError } from './src/metadata-service-error';\nexport {\n MetadataServiceInterface,\n MetadataWriteResult,\n} from './src/metadata-service-interface';\nexport { buildMetadataFieldPatch } from './src/metadata-patch';\nexport type {\n MetadataFieldValue,\n MetadataPatchOperation,\n} from './src/metadata-patch';\n"]}
@@ -1,6 +1,7 @@
1
1
  import type { Result } from '@internetarchive/result-type';
2
2
  import { MetadataServiceError } from '../metadata-service-error';
3
3
  import { MetadataBackendInterface } from './metadata-backend-interface';
4
+ import type { MetadataPatchOperation } from '../metadata-patch';
4
5
  /**
5
6
  * The DefaultSearchBackend performs a `window.fetch` request to archive.org
6
7
  */
@@ -15,6 +16,13 @@ export declare class DefaultMetadataBackend implements MetadataBackendInterface
15
16
  });
16
17
  /** @inheritdoc */
17
18
  fetchMetadata(identifier: string, keypath?: string): Promise<Result<any, MetadataServiceError>>;
19
+ /**
20
+ * @inheritdoc
21
+ *
22
+ * Makes a single attempt: a write that timed out may still have queued its
23
+ * task, and retrying would queue another.
24
+ */
25
+ writeMetadata(identifier: string, target: string, patch: MetadataPatchOperation[]): Promise<Result<any, MetadataServiceError>>;
18
26
  /**
19
27
  * Fires a request to the URL (with this backend's options applied) and
20
28
  * asynchronously returns a Result object containing either the raw response
@@ -35,6 +35,38 @@ export class DefaultMetadataBackend {
35
35
  const url = `https://${this.baseUrl}/metadata/${identifier}${path}`;
36
36
  return this.fetchUrl(url);
37
37
  }
38
+ /**
39
+ * @inheritdoc
40
+ *
41
+ * Makes a single attempt: a write that timed out may still have queued its
42
+ * task, and retrying would queue another.
43
+ */
44
+ async writeMetadata(identifier, target, patch) {
45
+ var _a;
46
+ const url = `https://${this.baseUrl}/metadata/${identifier}`;
47
+ const body = new URLSearchParams({
48
+ '-target': target,
49
+ '-patch': JSON.stringify(patch),
50
+ });
51
+ const result = await this.fetchUrl(url, {
52
+ requestOptions: {
53
+ method: 'POST',
54
+ credentials: this.includeCredentials ? 'include' : 'same-origin',
55
+ headers: {
56
+ 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
57
+ },
58
+ body,
59
+ },
60
+ });
61
+ // MDAPI reports a rejected write as `{ success: false, error }`
62
+ if (((_a = result.error) === null || _a === void 0 ? void 0 : _a.type) === MetadataServiceErrorType.searchEngineError) {
63
+ return this.getErrorResult(MetadataServiceErrorType.writeError, result.error.message, result.error.details);
64
+ }
65
+ if (result.success && result.success.success !== true) {
66
+ return this.getErrorResult(MetadataServiceErrorType.writeError, 'Metadata write was not accepted', result.success);
67
+ }
68
+ return result;
69
+ }
38
70
  /**
39
71
  * Fires a request to the URL (with this backend's options applied) and
40
72
  * asynchronously returns a Result object containing either the raw response
@@ -1 +1 @@
1
- {"version":3,"file":"default-metadata-backend.js","sourceRoot":"","sources":["../../../src/backend/default-metadata-backend.ts"],"names":[],"mappings":"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","sourcesContent":["/* eslint-disable @typescript-eslint/no-explicit-any */\nimport type { Result } from '@internetarchive/result-type';\nimport {\n MetadataServiceError,\n MetadataServiceErrorType,\n} from '../metadata-service-error';\nimport { MetadataBackendInterface } from './metadata-backend-interface';\n\n/**\n * The DefaultSearchBackend performs a `window.fetch` request to archive.org\n */\nexport class DefaultMetadataBackend implements MetadataBackendInterface {\n private baseUrl: string;\n\n private includeCredentials: boolean;\n\n private requestScope?: string;\n\n constructor(options?: {\n baseUrl?: string;\n includeCredentials?: boolean;\n scope?: string;\n }) {\n this.baseUrl = options?.baseUrl ?? 'archive.org';\n\n if (options?.includeCredentials !== undefined) {\n this.includeCredentials = options.includeCredentials;\n } else {\n // include credentials if the request is coming from an archive.org domain\n // since credentialed requests are only allowed from archive.org domains\n // due to CORS restrictions, see\n // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors/CORSNotSupportingCredentials\n this.includeCredentials =\n window.location.href.match(/^https?:\\/\\/.*archive\\.org(:[0-9]+)?/) !==\n null;\n }\n\n if (options?.scope !== undefined) {\n this.requestScope = options.scope;\n } else {\n const currentUrl = new URL(window.location.href);\n const scope = currentUrl.searchParams.get('scope');\n if (scope) {\n this.requestScope = scope;\n }\n }\n }\n\n /** @inheritdoc */\n async fetchMetadata(\n identifier: string,\n keypath?: string,\n ): Promise<Result<any, MetadataServiceError>> {\n const path = keypath ? `/${keypath}` : '';\n const url = `https://${this.baseUrl}/metadata/${identifier}${path}`;\n return this.fetchUrl(url);\n }\n\n /**\n * Fires a request to the URL (with this backend's options applied) and\n * asynchronously returns a Result object containing either the raw response\n * JSON or a MetadataServiceError.\n */\n protected async fetchUrl(\n url: string,\n options?: {\n requestOptions?: RequestInit;\n },\n ): Promise<Result<any, MetadataServiceError>> {\n const finalUrl = new URL(url);\n if (this.requestScope) {\n finalUrl.searchParams.set('scope', this.requestScope);\n }\n\n let response: Response;\n // first try the fetch and return a networkError if it fails\n try {\n const fetchOptions = options?.requestOptions ?? {\n credentials: this.includeCredentials ? 'include' : 'same-origin',\n };\n response = await fetch(finalUrl.href, fetchOptions);\n } catch (err) {\n const message =\n err instanceof Error\n ? err.message\n : typeof err === 'string'\n ? err\n : 'Unknown error';\n return this.getErrorResult(\n MetadataServiceErrorType.networkError,\n message,\n );\n }\n\n // then try json decoding and return a decodingError if it fails\n try {\n const json = await response.json();\n // the advanced search endpoint doesn't return an HTTP Error 400\n // and instead returns an HTTP 200 with an `error` key in the payload\n const error = json['error'];\n if (error) {\n const forensics = json['forensics'];\n return this.getErrorResult(\n MetadataServiceErrorType.searchEngineError,\n error,\n forensics,\n );\n } else {\n // success\n return { success: json };\n }\n } catch (err) {\n const message =\n err instanceof Error\n ? err.message\n : typeof err === 'string'\n ? err\n : 'Unknown error';\n return this.getErrorResult(\n MetadataServiceErrorType.decodingError,\n message,\n );\n }\n }\n\n private getErrorResult(\n errorType: MetadataServiceErrorType,\n message?: string,\n details?: any,\n ): Result<any, MetadataServiceError> {\n const error = new MetadataServiceError(errorType, message, details);\n const result = { error };\n return result;\n }\n}\n"]}
1
+ {"version":3,"file":"default-metadata-backend.js","sourceRoot":"","sources":["../../../src/backend/default-metadata-backend.ts"],"names":[],"mappings":"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","sourcesContent":["/* eslint-disable @typescript-eslint/no-explicit-any */\nimport type { Result } from '@internetarchive/result-type';\nimport {\n MetadataServiceError,\n MetadataServiceErrorType,\n} from '../metadata-service-error';\nimport { MetadataBackendInterface } from './metadata-backend-interface';\nimport type { MetadataPatchOperation } from '../metadata-patch';\n\n/**\n * The DefaultSearchBackend performs a `window.fetch` request to archive.org\n */\nexport class DefaultMetadataBackend implements MetadataBackendInterface {\n private baseUrl: string;\n\n private includeCredentials: boolean;\n\n private requestScope?: string;\n\n constructor(options?: {\n baseUrl?: string;\n includeCredentials?: boolean;\n scope?: string;\n }) {\n this.baseUrl = options?.baseUrl ?? 'archive.org';\n\n if (options?.includeCredentials !== undefined) {\n this.includeCredentials = options.includeCredentials;\n } else {\n // include credentials if the request is coming from an archive.org domain\n // since credentialed requests are only allowed from archive.org domains\n // due to CORS restrictions, see\n // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors/CORSNotSupportingCredentials\n this.includeCredentials =\n window.location.href.match(/^https?:\\/\\/.*archive\\.org(:[0-9]+)?/) !==\n null;\n }\n\n if (options?.scope !== undefined) {\n this.requestScope = options.scope;\n } else {\n const currentUrl = new URL(window.location.href);\n const scope = currentUrl.searchParams.get('scope');\n if (scope) {\n this.requestScope = scope;\n }\n }\n }\n\n /** @inheritdoc */\n async fetchMetadata(\n identifier: string,\n keypath?: string,\n ): Promise<Result<any, MetadataServiceError>> {\n const path = keypath ? `/${keypath}` : '';\n const url = `https://${this.baseUrl}/metadata/${identifier}${path}`;\n return this.fetchUrl(url);\n }\n\n /**\n * @inheritdoc\n *\n * Makes a single attempt: a write that timed out may still have queued its\n * task, and retrying would queue another.\n */\n async writeMetadata(\n identifier: string,\n target: string,\n patch: MetadataPatchOperation[],\n ): Promise<Result<any, MetadataServiceError>> {\n const url = `https://${this.baseUrl}/metadata/${identifier}`;\n const body = new URLSearchParams({\n '-target': target,\n '-patch': JSON.stringify(patch),\n });\n\n const result = await this.fetchUrl(url, {\n requestOptions: {\n method: 'POST',\n credentials: this.includeCredentials ? 'include' : 'same-origin',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',\n },\n body,\n },\n });\n\n // MDAPI reports a rejected write as `{ success: false, error }`\n if (result.error?.type === MetadataServiceErrorType.searchEngineError) {\n return this.getErrorResult(\n MetadataServiceErrorType.writeError,\n result.error.message,\n result.error.details,\n );\n }\n if (result.success && result.success.success !== true) {\n return this.getErrorResult(\n MetadataServiceErrorType.writeError,\n 'Metadata write was not accepted',\n result.success,\n );\n }\n return result;\n }\n\n /**\n * Fires a request to the URL (with this backend's options applied) and\n * asynchronously returns a Result object containing either the raw response\n * JSON or a MetadataServiceError.\n */\n protected async fetchUrl(\n url: string,\n options?: {\n requestOptions?: RequestInit;\n },\n ): Promise<Result<any, MetadataServiceError>> {\n const finalUrl = new URL(url);\n if (this.requestScope) {\n finalUrl.searchParams.set('scope', this.requestScope);\n }\n\n let response: Response;\n // first try the fetch and return a networkError if it fails\n try {\n const fetchOptions = options?.requestOptions ?? {\n credentials: this.includeCredentials ? 'include' : 'same-origin',\n };\n response = await fetch(finalUrl.href, fetchOptions);\n } catch (err) {\n const message =\n err instanceof Error\n ? err.message\n : typeof err === 'string'\n ? err\n : 'Unknown error';\n return this.getErrorResult(\n MetadataServiceErrorType.networkError,\n message,\n );\n }\n\n // then try json decoding and return a decodingError if it fails\n try {\n const json = await response.json();\n // the advanced search endpoint doesn't return an HTTP Error 400\n // and instead returns an HTTP 200 with an `error` key in the payload\n const error = json['error'];\n if (error) {\n const forensics = json['forensics'];\n return this.getErrorResult(\n MetadataServiceErrorType.searchEngineError,\n error,\n forensics,\n );\n } else {\n // success\n return { success: json };\n }\n } catch (err) {\n const message =\n err instanceof Error\n ? err.message\n : typeof err === 'string'\n ? err\n : 'Unknown error';\n return this.getErrorResult(\n MetadataServiceErrorType.decodingError,\n message,\n );\n }\n }\n\n private getErrorResult(\n errorType: MetadataServiceErrorType,\n message?: string,\n details?: any,\n ): Result<any, MetadataServiceError> {\n const error = new MetadataServiceError(errorType, message, details);\n const result = { error };\n return result;\n }\n}\n"]}
@@ -1,5 +1,6 @@
1
1
  import type { Result } from '@internetarchive/result-type';
2
2
  import { MetadataServiceError } from '../metadata-service-error';
3
+ import type { MetadataPatchOperation } from '../metadata-patch';
3
4
  /**
4
5
  * An interface to provide the network layer to the `MetadataService`.
5
6
  *
@@ -17,4 +18,17 @@ export interface MetadataBackendInterface {
17
18
  * @param keypath
18
19
  */
19
20
  fetchMetadata(identifier: string, keypath?: string): Promise<Result<any, MetadataServiceError>>;
21
+ /**
22
+ * Apply a JSON Patch to one of an item's targets (e.g. `metadata`) through
23
+ * the MDAPI write endpoint, as the logged-in user. Resolves with MDAPI's raw
24
+ * response (`{ success, task_id, log }`) on success.
25
+ *
26
+ * Optional so backends written before writes existed keep compiling; the
27
+ * service reports `writeNotSupported` for a backend without it.
28
+ *
29
+ * @param identifier
30
+ * @param target
31
+ * @param patch
32
+ */
33
+ writeMetadata?(identifier: string, target: string, patch: MetadataPatchOperation[]): Promise<Result<any, MetadataServiceError>>;
20
34
  }
@@ -1 +1 @@
1
- {"version":3,"file":"metadata-backend-interface.js","sourceRoot":"","sources":["../../../src/backend/metadata-backend-interface.ts"],"names":[],"mappings":"","sourcesContent":["/* eslint-disable @typescript-eslint/no-explicit-any */\nimport type { Result } from '@internetarchive/result-type';\nimport { MetadataServiceError } from '../metadata-service-error';\n\n/**\n * An interface to provide the network layer to the `MetadataService`.\n *\n * Objects implementing this interface are responsible for making calls to the Internet Archive\n * `metadata` endpoint or otherwise providing a similar reponse in JSON format.\n *\n * @export\n * @interface MetadataBackendInterface\n */\nexport interface MetadataBackendInterface {\n /**\n * Fetch metadata for a single item with an optional keypath\n *\n * @param identifier\n * @param keypath\n */\n fetchMetadata(\n identifier: string,\n keypath?: string,\n ): Promise<Result<any, MetadataServiceError>>;\n}\n"]}
1
+ {"version":3,"file":"metadata-backend-interface.js","sourceRoot":"","sources":["../../../src/backend/metadata-backend-interface.ts"],"names":[],"mappings":"","sourcesContent":["/* eslint-disable @typescript-eslint/no-explicit-any */\nimport type { Result } from '@internetarchive/result-type';\nimport { MetadataServiceError } from '../metadata-service-error';\nimport type { MetadataPatchOperation } from '../metadata-patch';\n\n/**\n * An interface to provide the network layer to the `MetadataService`.\n *\n * Objects implementing this interface are responsible for making calls to the Internet Archive\n * `metadata` endpoint or otherwise providing a similar reponse in JSON format.\n *\n * @export\n * @interface MetadataBackendInterface\n */\nexport interface MetadataBackendInterface {\n /**\n * Fetch metadata for a single item with an optional keypath\n *\n * @param identifier\n * @param keypath\n */\n fetchMetadata(\n identifier: string,\n keypath?: string,\n ): Promise<Result<any, MetadataServiceError>>;\n\n /**\n * Apply a JSON Patch to one of an item's targets (e.g. `metadata`) through\n * the MDAPI write endpoint, as the logged-in user. Resolves with MDAPI's raw\n * response (`{ success, task_id, log }`) on success.\n *\n * Optional so backends written before writes existed keep compiling; the\n * service reports `writeNotSupported` for a backend without it.\n *\n * @param identifier\n * @param target\n * @param patch\n */\n writeMetadata?(\n identifier: string,\n target: string,\n patch: MetadataPatchOperation[],\n ): Promise<Result<any, MetadataServiceError>>;\n}\n"]}
@@ -0,0 +1,21 @@
1
+ /** A metadata field value as MDAPI stores it: one string, or a list of them */
2
+ export type MetadataFieldValue = string | string[];
3
+ /** One JSON Patch (RFC 6902) operation, as MDAPI's write endpoint accepts */
4
+ export type MetadataPatchOperation = {
5
+ op: 'add' | 'replace';
6
+ path: string;
7
+ value: unknown;
8
+ } | {
9
+ op: 'remove';
10
+ path: string;
11
+ };
12
+ /**
13
+ * Builds the JSON Patch that sets one metadata field to `value`, given the
14
+ * field's current value.
15
+ *
16
+ * A single-entry list is written as a bare string, which is how MDAPI stores
17
+ * a one-value field, and an empty string or list removes the field. Returns
18
+ * an empty patch when nothing changes, since MDAPI rejects a write that
19
+ * doesn't modify the item.
20
+ */
21
+ export declare function buildMetadataFieldPatch(field: string, value: MetadataFieldValue, current: MetadataFieldValue | undefined): MetadataPatchOperation[];
@@ -0,0 +1,37 @@
1
+ /** Escapes a metadata key for use as a JSON Pointer segment (RFC 6901) */
2
+ function pointerFor(field) {
3
+ return `/${field.replace(/~/g, '~0').replace(/\//g, '~1')}`;
4
+ }
5
+ /** MDAPI's storage shape: a one-entry list collapses to a bare string */
6
+ function normalize(value) {
7
+ return Array.isArray(value) && value.length === 1 ? value[0] : value;
8
+ }
9
+ function isEmpty(value) {
10
+ if (value == null)
11
+ return true;
12
+ if (Array.isArray(value))
13
+ return value.length === 0;
14
+ return value === '';
15
+ }
16
+ /**
17
+ * Builds the JSON Patch that sets one metadata field to `value`, given the
18
+ * field's current value.
19
+ *
20
+ * A single-entry list is written as a bare string, which is how MDAPI stores
21
+ * a one-value field, and an empty string or list removes the field. Returns
22
+ * an empty patch when nothing changes, since MDAPI rejects a write that
23
+ * doesn't modify the item.
24
+ */
25
+ export function buildMetadataFieldPatch(field, value, current) {
26
+ const path = pointerFor(field);
27
+ const next = normalize(value);
28
+ const existing = normalize(current);
29
+ if (next == null || isEmpty(next)) {
30
+ return isEmpty(existing) ? [] : [{ op: 'remove', path }];
31
+ }
32
+ if (JSON.stringify(next) === JSON.stringify(existing))
33
+ return [];
34
+ const op = isEmpty(existing) ? 'add' : 'replace';
35
+ return [{ op, path, value: next }];
36
+ }
37
+ //# sourceMappingURL=metadata-patch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metadata-patch.js","sourceRoot":"","sources":["../../src/metadata-patch.ts"],"names":[],"mappings":"AAQA,0EAA0E;AAC1E,SAAS,UAAU,CAAC,KAAa;IAC/B,OAAO,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,EAAE,CAAC;AAC9D,CAAC;AAED,yEAAyE;AACzE,SAAS,SAAS,CAChB,KAAqC;IAErC,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;AACvE,CAAC;AAED,SAAS,OAAO,CAAC,KAAqC;IACpD,IAAI,KAAK,IAAI,IAAI;QAAE,OAAO,IAAI,CAAC;IAC/B,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC;IACpD,OAAO,KAAK,KAAK,EAAE,CAAC;AACtB,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,uBAAuB,CACrC,KAAa,EACb,KAAyB,EACzB,OAAuC;IAEvC,MAAM,IAAI,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC;IAC/B,MAAM,IAAI,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC;IAC9B,MAAM,QAAQ,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;IAEpC,IAAI,IAAI,IAAI,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QAClC,OAAO,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3D,CAAC;IAED,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC;QAAE,OAAO,EAAE,CAAC;IAEjE,MAAM,EAAE,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;IACjD,OAAO,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;AACrC,CAAC","sourcesContent":["/** A metadata field value as MDAPI stores it: one string, or a list of them */\nexport type MetadataFieldValue = string | string[];\n\n/** One JSON Patch (RFC 6902) operation, as MDAPI's write endpoint accepts */\nexport type MetadataPatchOperation =\n | { op: 'add' | 'replace'; path: string; value: unknown }\n | { op: 'remove'; path: string };\n\n/** Escapes a metadata key for use as a JSON Pointer segment (RFC 6901) */\nfunction pointerFor(field: string): string {\n return `/${field.replace(/~/g, '~0').replace(/\\//g, '~1')}`;\n}\n\n/** MDAPI's storage shape: a one-entry list collapses to a bare string */\nfunction normalize(\n value: MetadataFieldValue | undefined,\n): MetadataFieldValue | undefined {\n return Array.isArray(value) && value.length === 1 ? value[0] : value;\n}\n\nfunction isEmpty(value: MetadataFieldValue | undefined): boolean {\n if (value == null) return true;\n if (Array.isArray(value)) return value.length === 0;\n return value === '';\n}\n\n/**\n * Builds the JSON Patch that sets one metadata field to `value`, given the\n * field's current value.\n *\n * A single-entry list is written as a bare string, which is how MDAPI stores\n * a one-value field, and an empty string or list removes the field. Returns\n * an empty patch when nothing changes, since MDAPI rejects a write that\n * doesn't modify the item.\n */\nexport function buildMetadataFieldPatch(\n field: string,\n value: MetadataFieldValue,\n current: MetadataFieldValue | undefined,\n): MetadataPatchOperation[] {\n const path = pointerFor(field);\n const next = normalize(value);\n const existing = normalize(current);\n\n if (next == null || isEmpty(next)) {\n return isEmpty(existing) ? [] : [{ op: 'remove', path }];\n }\n\n if (JSON.stringify(next) === JSON.stringify(existing)) return [];\n\n const op = isEmpty(existing) ? 'add' : 'replace';\n return [{ op, path, value: next }];\n}\n"]}
@@ -2,7 +2,9 @@ export declare enum MetadataServiceErrorType {
2
2
  networkError = "MetadataService.NetworkError",
3
3
  itemNotFound = "MetadataService.ItemNotFound",
4
4
  decodingError = "MetadataService.DecodingError",
5
- searchEngineError = "MetadataService.SearchEngineError"
5
+ searchEngineError = "MetadataService.SearchEngineError",
6
+ writeError = "MetadataService.WriteError",
7
+ writeNotSupported = "MetadataService.WriteNotSupported"
6
8
  }
7
9
  export declare class MetadataServiceError extends Error {
8
10
  type: MetadataServiceErrorType;
@@ -4,6 +4,8 @@ export var MetadataServiceErrorType;
4
4
  MetadataServiceErrorType["itemNotFound"] = "MetadataService.ItemNotFound";
5
5
  MetadataServiceErrorType["decodingError"] = "MetadataService.DecodingError";
6
6
  MetadataServiceErrorType["searchEngineError"] = "MetadataService.SearchEngineError";
7
+ MetadataServiceErrorType["writeError"] = "MetadataService.WriteError";
8
+ MetadataServiceErrorType["writeNotSupported"] = "MetadataService.WriteNotSupported";
7
9
  })(MetadataServiceErrorType || (MetadataServiceErrorType = {}));
8
10
  export class MetadataServiceError extends Error {
9
11
  /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
@@ -1 +1 @@
1
- {"version":3,"file":"metadata-service-error.js","sourceRoot":"","sources":["../../src/metadata-service-error.ts"],"names":[],"mappings":"AAAA,MAAM,CAAN,IAAY,wBAKX;AALD,WAAY,wBAAwB;IAClC,yEAA6C,CAAA;IAC7C,yEAA6C,CAAA;IAC7C,2EAA+C,CAAA;IAC/C,mFAAuD,CAAA;AACzD,CAAC,EALW,wBAAwB,KAAxB,wBAAwB,QAKnC;AAED,MAAM,OAAO,oBAAqB,SAAQ,KAAK;IAM7C,iEAAiE;IACjE,YAAY,IAA8B,EAAE,OAAgB,EAAE,OAAa;QACzE,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;CACF","sourcesContent":["export enum MetadataServiceErrorType {\n networkError = 'MetadataService.NetworkError',\n itemNotFound = 'MetadataService.ItemNotFound',\n decodingError = 'MetadataService.DecodingError',\n searchEngineError = 'MetadataService.SearchEngineError',\n}\n\nexport class MetadataServiceError extends Error {\n type: MetadataServiceErrorType;\n\n /* eslint-disable-next-line @typescript-eslint/no-explicit-any */\n details?: any;\n\n /* eslint-disable-next-line @typescript-eslint/no-explicit-any */\n constructor(type: MetadataServiceErrorType, message?: string, details?: any) {\n super(message);\n this.name = type;\n this.type = type;\n this.details = details;\n }\n}\n"]}
1
+ {"version":3,"file":"metadata-service-error.js","sourceRoot":"","sources":["../../src/metadata-service-error.ts"],"names":[],"mappings":"AAAA,MAAM,CAAN,IAAY,wBAOX;AAPD,WAAY,wBAAwB;IAClC,yEAA6C,CAAA;IAC7C,yEAA6C,CAAA;IAC7C,2EAA+C,CAAA;IAC/C,mFAAuD,CAAA;IACvD,qEAAyC,CAAA;IACzC,mFAAuD,CAAA;AACzD,CAAC,EAPW,wBAAwB,KAAxB,wBAAwB,QAOnC;AAED,MAAM,OAAO,oBAAqB,SAAQ,KAAK;IAM7C,iEAAiE;IACjE,YAAY,IAA8B,EAAE,OAAgB,EAAE,OAAa;QACzE,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;CACF","sourcesContent":["export enum MetadataServiceErrorType {\n networkError = 'MetadataService.NetworkError',\n itemNotFound = 'MetadataService.ItemNotFound',\n decodingError = 'MetadataService.DecodingError',\n searchEngineError = 'MetadataService.SearchEngineError',\n writeError = 'MetadataService.WriteError',\n writeNotSupported = 'MetadataService.WriteNotSupported',\n}\n\nexport class MetadataServiceError extends Error {\n type: MetadataServiceErrorType;\n\n /* eslint-disable-next-line @typescript-eslint/no-explicit-any */\n details?: any;\n\n /* eslint-disable-next-line @typescript-eslint/no-explicit-any */\n constructor(type: MetadataServiceErrorType, message?: string, details?: any) {\n super(message);\n this.name = type;\n this.type = type;\n this.details = details;\n }\n}\n"]}
@@ -1,6 +1,13 @@
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 { MetadataFieldValue, MetadataPatchOperation } from './metadata-patch';
5
+ export type MetadataWriteResult = {
6
+ /** Whether a write was sent. False when the field already had the value. */
7
+ changed: boolean;
8
+ /** The catalog task that will apply the change to the item */
9
+ taskId?: number;
10
+ };
4
11
  export interface MetadataServiceInterface {
5
12
  /**
6
13
  * Fetch metadata for a given identifier
@@ -41,4 +48,39 @@ export interface MetadataServiceInterface {
41
48
  * @param keypath
42
49
  */
43
50
  fetchMetadataValue<T>(identifier: string, keypath: string): Promise<Result<T, MetadataServiceError>>;
51
+ /**
52
+ * Apply a JSON Patch to an item's metadata as the logged-in user.
53
+ *
54
+ * MDAPI applies the change through a queued catalog task. MDAPI reads
55
+ * replay pending tasks' patches ("lookahead"), so they show the change right
56
+ * away, but caches built on MDAPI (search, page services) can lag it. MDAPI
57
+ * rejects a patch that doesn't change the item.
58
+ *
59
+ * ```ts
60
+ * await metadataService.patchMetadata('goody', [
61
+ * { op: 'replace', path: '/title', value: 'Goody Two-Shoes' },
62
+ * ]);
63
+ * ```
64
+ *
65
+ * @param identifier
66
+ * @param patch JSON Patch operations against the item's metadata
67
+ * @param target MDAPI write target, `metadata` by default
68
+ */
69
+ patchMetadata(identifier: string, patch: MetadataPatchOperation[], target?: string): Promise<Result<MetadataWriteResult, MetadataServiceError>>;
70
+ /**
71
+ * Set one metadata field on an item as the logged-in user. An empty string
72
+ * or list removes the field.
73
+ *
74
+ * Reads the item's current metadata from MDAPI first and patches against
75
+ * that, so the right add / replace / remove goes out even when the caller's
76
+ * copy is stale. That read includes this user's still-queued writes through
77
+ * MDAPI's lookahead, except for tasks lookahead skips (e.g. ones held for an
78
+ * admin). Resolves with `changed: false`, without writing, when the field
79
+ * already has the value.
80
+ *
81
+ * @param identifier
82
+ * @param field The metadata key, e.g. `title`
83
+ * @param value
84
+ */
85
+ updateMetadataField(identifier: string, field: string, value: MetadataFieldValue): Promise<Result<MetadataWriteResult, MetadataServiceError>>;
44
86
  }
@@ -1 +1 @@
1
- {"version":3,"file":"metadata-service-interface.js","sourceRoot":"","sources":["../../src/metadata-service-interface.ts"],"names":[],"mappings":"","sourcesContent":["import type { Result } from '@internetarchive/result-type';\nimport type { MetadataServiceError } from './metadata-service-error';\nimport type { MetadataResponse } from './responses/metadata-response';\n\nexport interface MetadataServiceInterface {\n /**\n * Fetch metadata for a given identifier\n *\n * @param {string} identifier\n * @returns {Promise<Result<MetadataResponse, MetadataServiceError>>}\n */\n fetchMetadata(\n identifier: string,\n ): Promise<Result<MetadataResponse, MetadataServiceError>>;\n\n /**\n * Fetch the metadata value for a given identifier and keypath\n *\n * The response from this request can take any form, object, array, string, etc.\n * depending on the query. You can provide return typing in the response by\n * specifying the type. Note, there is no automatic type conversion since it can be anything.\n *\n * For example:\n *\n * ```ts\n * const collection = await searchService.fetchMetadataValue<string>('goody', 'metadata/collection/0');\n * console.debug('collection:', collection); => 'Goody Collection'\n *\n * const files_count = await searchService.fetchMetadataValue<number>('goody', 'files_count');\n * console.debug('files_count:', files_count); => 12\n * ```\n *\n * Keypath examples:\n *\n * /metadata/:identifier/metadata // returns the entire metadata object\n * /metadata/:identifier/server // returns the server for the given identifier\n * /metadata/:identifier/files_count\n * /metadata/:identifier/files?start=1&count=2 // query for files\n * /metadata/:identifier/metadata/collection // all collections\n * /metadata/:identifier/metadata/collection/0 // first collection\n * /metadata/:identifier/metadata/title\n * /metadata/:identifier/files/0/name // first file name\n *\n * @param identifier\n * @param keypath\n */\n fetchMetadataValue<T>(\n identifier: string,\n keypath: string,\n ): Promise<Result<T, MetadataServiceError>>;\n}\n"]}
1
+ {"version":3,"file":"metadata-service-interface.js","sourceRoot":"","sources":["../../src/metadata-service-interface.ts"],"names":[],"mappings":"","sourcesContent":["import type { Result } from '@internetarchive/result-type';\nimport type { MetadataServiceError } from './metadata-service-error';\nimport type { MetadataResponse } from './responses/metadata-response';\nimport type {\n MetadataFieldValue,\n MetadataPatchOperation,\n} from './metadata-patch';\n\nexport type MetadataWriteResult = {\n /** Whether a write was sent. False when the field already had the value. */\n changed: boolean;\n /** The catalog task that will apply the change to the item */\n taskId?: number;\n};\n\nexport interface MetadataServiceInterface {\n /**\n * Fetch metadata for a given identifier\n *\n * @param {string} identifier\n * @returns {Promise<Result<MetadataResponse, MetadataServiceError>>}\n */\n fetchMetadata(\n identifier: string,\n ): Promise<Result<MetadataResponse, MetadataServiceError>>;\n\n /**\n * Fetch the metadata value for a given identifier and keypath\n *\n * The response from this request can take any form, object, array, string, etc.\n * depending on the query. You can provide return typing in the response by\n * specifying the type. Note, there is no automatic type conversion since it can be anything.\n *\n * For example:\n *\n * ```ts\n * const collection = await searchService.fetchMetadataValue<string>('goody', 'metadata/collection/0');\n * console.debug('collection:', collection); => 'Goody Collection'\n *\n * const files_count = await searchService.fetchMetadataValue<number>('goody', 'files_count');\n * console.debug('files_count:', files_count); => 12\n * ```\n *\n * Keypath examples:\n *\n * /metadata/:identifier/metadata // returns the entire metadata object\n * /metadata/:identifier/server // returns the server for the given identifier\n * /metadata/:identifier/files_count\n * /metadata/:identifier/files?start=1&count=2 // query for files\n * /metadata/:identifier/metadata/collection // all collections\n * /metadata/:identifier/metadata/collection/0 // first collection\n * /metadata/:identifier/metadata/title\n * /metadata/:identifier/files/0/name // first file name\n *\n * @param identifier\n * @param keypath\n */\n fetchMetadataValue<T>(\n identifier: string,\n keypath: string,\n ): Promise<Result<T, MetadataServiceError>>;\n\n /**\n * Apply a JSON Patch to an item's metadata as the logged-in user.\n *\n * MDAPI applies the change through a queued catalog task. MDAPI reads\n * replay pending tasks' patches (\"lookahead\"), so they show the change right\n * away, but caches built on MDAPI (search, page services) can lag it. MDAPI\n * rejects a patch that doesn't change the item.\n *\n * ```ts\n * await metadataService.patchMetadata('goody', [\n * { op: 'replace', path: '/title', value: 'Goody Two-Shoes' },\n * ]);\n * ```\n *\n * @param identifier\n * @param patch JSON Patch operations against the item's metadata\n * @param target MDAPI write target, `metadata` by default\n */\n patchMetadata(\n identifier: string,\n patch: MetadataPatchOperation[],\n target?: string,\n ): Promise<Result<MetadataWriteResult, MetadataServiceError>>;\n\n /**\n * Set one metadata field on an item as the logged-in user. An empty string\n * or list removes the field.\n *\n * Reads the item's current metadata from MDAPI first and patches against\n * that, so the right add / replace / remove goes out even when the caller's\n * copy is stale. That read includes this user's still-queued writes through\n * MDAPI's lookahead, except for tasks lookahead skips (e.g. ones held for an\n * admin). Resolves with `changed: false`, without writing, when the field\n * already has the value.\n *\n * @param identifier\n * @param field The metadata key, e.g. `title`\n * @param value\n */\n updateMetadataField(\n identifier: string,\n field: string,\n value: MetadataFieldValue,\n ): Promise<Result<MetadataWriteResult, MetadataServiceError>>;\n}\n"]}
@@ -1,7 +1,8 @@
1
1
  import type { Result } from '@internetarchive/result-type';
2
2
  import { MetadataBackendInterface } from './backend/metadata-backend-interface';
3
3
  import { MetadataServiceError } from './metadata-service-error';
4
- import type { MetadataServiceInterface } from './metadata-service-interface';
4
+ import type { MetadataServiceInterface, MetadataWriteResult } from './metadata-service-interface';
5
+ import { type MetadataFieldValue, type MetadataPatchOperation } from './metadata-patch';
5
6
  import { MetadataResponse } from './responses/metadata-response';
6
7
  /**
7
8
  * The Metadata Service is responsible for taking the raw response provided by
@@ -15,4 +16,8 @@ export declare class MetadataService implements MetadataServiceInterface {
15
16
  fetchMetadata(identifier: string): Promise<Result<MetadataResponse, MetadataServiceError>>;
16
17
  /** @inheritdoc */
17
18
  fetchMetadataValue<T>(identifier: string, keypath: string): Promise<Result<T, MetadataServiceError>>;
19
+ /** @inheritdoc */
20
+ patchMetadata(identifier: string, patch: MetadataPatchOperation[], target?: string): Promise<Result<MetadataWriteResult, MetadataServiceError>>;
21
+ /** @inheritdoc */
22
+ updateMetadataField(identifier: string, field: string, value: MetadataFieldValue): Promise<Result<MetadataWriteResult, MetadataServiceError>>;
18
23
  }
@@ -1,5 +1,6 @@
1
1
  import { DefaultMetadataBackend } from './backend/default-metadata-backend';
2
2
  import { MetadataServiceError, MetadataServiceErrorType, } from './metadata-service-error';
3
+ import { buildMetadataFieldPatch, } from './metadata-patch';
3
4
  import { MetadataResponse } from './responses/metadata-response';
4
5
  /**
5
6
  * The Metadata Service is responsible for taking the raw response provided by
@@ -38,6 +39,33 @@ export class MetadataService {
38
39
  }
39
40
  return { success: result.success.result };
40
41
  }
42
+ /** @inheritdoc */
43
+ async patchMetadata(identifier, patch, target = 'metadata') {
44
+ var _a;
45
+ if (!this.backend.writeMetadata) {
46
+ return {
47
+ error: new MetadataServiceError(MetadataServiceErrorType.writeNotSupported, 'This metadata backend does not support writes'),
48
+ };
49
+ }
50
+ const result = await this.backend.writeMetadata(identifier, target, patch);
51
+ if (result.error) {
52
+ return result;
53
+ }
54
+ return { success: { changed: true, taskId: (_a = result.success) === null || _a === void 0 ? void 0 : _a.task_id } };
55
+ }
56
+ /** @inheritdoc */
57
+ async updateMetadataField(identifier, field, value) {
58
+ var _a;
59
+ const current = await this.fetchMetadataValue(identifier, 'metadata');
60
+ if (current.error) {
61
+ return { error: current.error };
62
+ }
63
+ const patch = buildMetadataFieldPatch(field, value, (_a = current.success) === null || _a === void 0 ? void 0 : _a[field]);
64
+ if (patch.length === 0) {
65
+ return { success: { changed: false } };
66
+ }
67
+ return this.patchMetadata(identifier, patch);
68
+ }
41
69
  }
42
70
  MetadataService.default = new MetadataService(new DefaultMetadataBackend());
43
71
  //# sourceMappingURL=metadata-service.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"metadata-service.js","sourceRoot":"","sources":["../../src/metadata-service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AAE5E,OAAO,EACL,oBAAoB,EACpB,wBAAwB,GACzB,MAAM,0BAA0B,CAAC;AAElC,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAEjE;;;GAGG;AACH,MAAM,OAAO,eAAe;IAO1B,YAAY,OAAiC;QAC3C,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;IAED,kBAAkB;IAClB,KAAK,CAAC,aAAa,CACjB,UAAkB;;QAElB,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC;QACjE,IAAI,WAAW,CAAC,KAAK,EAAE,CAAC;YACtB,OAAO,WAAW,CAAC;QACrB,CAAC;QAED,IAAI,CAAA,MAAA,WAAW,CAAC,OAAO,0CAAE,QAAQ,MAAK,SAAS,EAAE,CAAC;YAChD,OAAO;gBACL,KAAK,EAAE,IAAI,oBAAoB,CAAC,wBAAwB,CAAC,YAAY,CAAC;aACvE,CAAC;QACJ,CAAC;QAED,MAAM,eAAe,GAAG,IAAI,gBAAgB,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;QAClE,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,CAAC;IACtC,CAAC;IAED,kBAAkB;IAClB,KAAK,CAAC,kBAAkB,CACtB,UAAkB,EAClB,OAAe;;QAEf,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;QACrE,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;YACjB,OAAO,MAAM,CAAC;QAChB,CAAC;QAED,IAAI,CAAA,MAAA,MAAM,CAAC,OAAO,0CAAE,MAAM,MAAK,SAAS,EAAE,CAAC;YACzC,OAAO;gBACL,KAAK,EAAE,IAAI,oBAAoB,CAAC,wBAAwB,CAAC,YAAY,CAAC;aACvE,CAAC;QACJ,CAAC;QAED,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;IAC5C,CAAC;;AA9Ca,uBAAO,GAA6B,IAAI,eAAe,CACnE,IAAI,sBAAsB,EAAE,CAC7B,CAAC","sourcesContent":["import type { Result } from '@internetarchive/result-type';\nimport { DefaultMetadataBackend } from './backend/default-metadata-backend';\nimport { MetadataBackendInterface } from './backend/metadata-backend-interface';\nimport {\n MetadataServiceError,\n MetadataServiceErrorType,\n} from './metadata-service-error';\nimport type { MetadataServiceInterface } from './metadata-service-interface';\nimport { MetadataResponse } from './responses/metadata-response';\n\n/**\n * The Metadata Service is responsible for taking the raw response provided by\n * the backend and modeling it as a `MetadataResponse` object.\n */\nexport class MetadataService implements MetadataServiceInterface {\n public static default: MetadataServiceInterface = new MetadataService(\n new DefaultMetadataBackend(),\n );\n\n private backend: MetadataBackendInterface;\n\n constructor(backend: MetadataBackendInterface) {\n this.backend = backend;\n }\n\n /** @inheritdoc */\n async fetchMetadata(\n identifier: string,\n ): Promise<Result<MetadataResponse, MetadataServiceError>> {\n const rawResponse = await this.backend.fetchMetadata(identifier);\n if (rawResponse.error) {\n return rawResponse;\n }\n\n if (rawResponse.success?.metadata === undefined) {\n return {\n error: new MetadataServiceError(MetadataServiceErrorType.itemNotFound),\n };\n }\n\n const modeledResponse = new MetadataResponse(rawResponse.success);\n return { success: modeledResponse };\n }\n\n /** @inheritdoc */\n async fetchMetadataValue<T>(\n identifier: string,\n keypath: string,\n ): Promise<Result<T, MetadataServiceError>> {\n const result = await this.backend.fetchMetadata(identifier, keypath);\n if (result.error) {\n return result;\n }\n\n if (result.success?.result === undefined) {\n return {\n error: new MetadataServiceError(MetadataServiceErrorType.itemNotFound),\n };\n }\n\n return { success: result.success.result };\n }\n}\n"]}
1
+ {"version":3,"file":"metadata-service.js","sourceRoot":"","sources":["../../src/metadata-service.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Result } from '@internetarchive/result-type';\nimport { DefaultMetadataBackend } from './backend/default-metadata-backend';\nimport { MetadataBackendInterface } from './backend/metadata-backend-interface';\nimport {\n MetadataServiceError,\n MetadataServiceErrorType,\n} from './metadata-service-error';\nimport type {\n MetadataServiceInterface,\n MetadataWriteResult,\n} from './metadata-service-interface';\nimport {\n buildMetadataFieldPatch,\n type MetadataFieldValue,\n type MetadataPatchOperation,\n} from './metadata-patch';\nimport { MetadataResponse } from './responses/metadata-response';\n\n/**\n * The Metadata Service is responsible for taking the raw response provided by\n * the backend and modeling it as a `MetadataResponse` object.\n */\nexport class MetadataService implements MetadataServiceInterface {\n public static default: MetadataServiceInterface = new MetadataService(\n new DefaultMetadataBackend(),\n );\n\n private backend: MetadataBackendInterface;\n\n constructor(backend: MetadataBackendInterface) {\n this.backend = backend;\n }\n\n /** @inheritdoc */\n async fetchMetadata(\n identifier: string,\n ): Promise<Result<MetadataResponse, MetadataServiceError>> {\n const rawResponse = await this.backend.fetchMetadata(identifier);\n if (rawResponse.error) {\n return rawResponse;\n }\n\n if (rawResponse.success?.metadata === undefined) {\n return {\n error: new MetadataServiceError(MetadataServiceErrorType.itemNotFound),\n };\n }\n\n const modeledResponse = new MetadataResponse(rawResponse.success);\n return { success: modeledResponse };\n }\n\n /** @inheritdoc */\n async fetchMetadataValue<T>(\n identifier: string,\n keypath: string,\n ): Promise<Result<T, MetadataServiceError>> {\n const result = await this.backend.fetchMetadata(identifier, keypath);\n if (result.error) {\n return result;\n }\n\n if (result.success?.result === undefined) {\n return {\n error: new MetadataServiceError(MetadataServiceErrorType.itemNotFound),\n };\n }\n\n return { success: result.success.result };\n }\n\n /** @inheritdoc */\n async patchMetadata(\n identifier: string,\n patch: MetadataPatchOperation[],\n target = 'metadata',\n ): Promise<Result<MetadataWriteResult, MetadataServiceError>> {\n if (!this.backend.writeMetadata) {\n return {\n error: new MetadataServiceError(\n MetadataServiceErrorType.writeNotSupported,\n 'This metadata backend does not support writes',\n ),\n };\n }\n\n const result = await this.backend.writeMetadata(identifier, target, patch);\n if (result.error) {\n return result;\n }\n\n return { success: { changed: true, taskId: result.success?.task_id } };\n }\n\n /** @inheritdoc */\n async updateMetadataField(\n identifier: string,\n field: string,\n value: MetadataFieldValue,\n ): Promise<Result<MetadataWriteResult, MetadataServiceError>> {\n const current = await this.fetchMetadataValue<\n Record<string, MetadataFieldValue>\n >(identifier, 'metadata');\n if (current.error) {\n return { error: current.error };\n }\n\n const patch = buildMetadataFieldPatch(\n field,\n value,\n current.success?.[field],\n );\n if (patch.length === 0) {\n return { success: { changed: false } };\n }\n\n return this.patchMetadata(identifier, patch);\n }\n}\n"]}
@@ -1,4 +1,21 @@
1
1
  import { File, Metadata, Review, SpeechMusicASREntry } from '@internetarchive/iaux-item-metadata';
2
+ /**
3
+ * A single alternate download location: a server and the directory on it that
4
+ * holds the item's files.
5
+ */
6
+ export type AlternateLocation = {
7
+ server: string;
8
+ dir: string;
9
+ };
10
+ /**
11
+ * Alternate download locations for an item, beyond the primary `server`/`dir`.
12
+ * `servers` lists all known mirrors; `workable` is the subset currently
13
+ * reachable for downloads.
14
+ */
15
+ export type AlternateLocations = {
16
+ servers: AlternateLocation[];
17
+ workable: AlternateLocation[];
18
+ };
2
19
  /**
3
20
  * The main top-level reponse when fetching Metadata
4
21
  *
@@ -21,5 +38,10 @@ export declare class MetadataResponse {
21
38
  readonly workable_servers: string[];
22
39
  readonly speech_vs_music_asr?: SpeechMusicASREntry[];
23
40
  readonly reviews?: Review[];
41
+ readonly alternate_locations?: AlternateLocations;
42
+ readonly clips?: Record<string, unknown>;
43
+ readonly plays?: Record<string, unknown>;
44
+ readonly simplelists?: Record<string, unknown>;
45
+ readonly solo?: boolean;
24
46
  constructor(json: Record<string, any>);
25
47
  }
@@ -27,6 +27,11 @@ export class MetadataResponse {
27
27
  this.reviews = (_b = json.reviews) === null || _b === void 0 ? void 0 : _b.map(
28
28
  /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
29
29
  (entry) => new Review(entry));
30
+ this.alternate_locations = json.alternate_locations;
31
+ this.clips = json.clips;
32
+ this.plays = json.plays;
33
+ this.simplelists = json.simplelists;
34
+ this.solo = json.solo;
30
35
  }
31
36
  }
32
37
  //# sourceMappingURL=metadata-response.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"metadata-response.js","sourceRoot":"","sources":["../../../src/responses/metadata-response.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,IAAI,EACJ,QAAQ,EACR,MAAM,GAEP,MAAM,qCAAqC,CAAC;AAE7C;;;;;GAKG;AACH,MAAM,OAAO,gBAAgB;IAgC3B,iEAAiE;IACjE,YAAY,IAAyB;;QACnC,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;QACxB,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;QAC5B,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC,EAAE,CAAC;QAClB,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC,EAAE,CAAC;QAClB,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC;QACpB,iEAAiE;QACjE,IAAI,CAAC,KAAK,GAAG,MAAA,IAAI,CAAC,KAAK,0CAAE,GAAG,CAAC,CAAC,IAAyB,EAAE,EAAE,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QAC5E,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,WAAW,CAAC;QACpC,IAAI,CAAC,iBAAiB,GAAG,IAAI,CAAC,iBAAiB,CAAC;QAChD,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;QAChC,IAAI,CAAC,QAAQ,GAAG,IAAI,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC5C,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAC1B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACtB,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC,gBAAgB,CAAC;QAC9C,IAAI,CAAC,mBAAmB,GAAG,IAAI,CAAC,mBAAmB,CAAC;QACpD,IAAI,CAAC,OAAO,GAAG,MAAA,IAAI,CAAC,OAAO,0CAAE,GAAG;QAC9B,iEAAiE;QACjE,CAAC,KAA0B,EAAE,EAAE,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,CAClD,CAAC;IACJ,CAAC;CACF","sourcesContent":["import {\n File,\n Metadata,\n Review,\n SpeechMusicASREntry,\n} from '@internetarchive/iaux-item-metadata';\n\n/**\n * The main top-level reponse when fetching Metadata\n *\n * @export\n * @class MetadataResponse\n */\nexport class MetadataResponse {\n /* eslint-disable-next-line @typescript-eslint/no-explicit-any */\n readonly rawResponse: Readonly<Record<string, any>>;\n\n readonly created: number;\n\n readonly d1: string;\n\n readonly d2: string;\n\n readonly dir: string;\n\n readonly files: File[];\n\n readonly files_count: number;\n\n readonly item_last_updated: number;\n\n readonly item_size: number;\n\n readonly metadata: Metadata;\n\n readonly server: string;\n\n readonly uniq: number;\n\n readonly workable_servers: string[];\n\n readonly speech_vs_music_asr?: SpeechMusicASREntry[];\n\n readonly reviews?: Review[];\n\n /* eslint-disable-next-line @typescript-eslint/no-explicit-any */\n constructor(json: Record<string, any>) {\n this.rawResponse = json;\n this.created = json.created;\n this.d1 = json.d1;\n this.d2 = json.d2;\n this.dir = json.dir;\n /* eslint-disable-next-line @typescript-eslint/no-explicit-any */\n this.files = json.files?.map((file: Record<string, any>) => new File(file));\n this.files_count = json.files_count;\n this.item_last_updated = json.item_last_updated;\n this.item_size = json.item_size;\n this.metadata = new Metadata(json.metadata);\n this.server = json.server;\n this.uniq = json.uniq;\n this.workable_servers = json.workable_servers;\n this.speech_vs_music_asr = json.speech_vs_music_asr;\n this.reviews = json.reviews?.map(\n /* eslint-disable-next-line @typescript-eslint/no-explicit-any */\n (entry: Record<string, any>) => new Review(entry),\n );\n }\n}\n"]}
1
+ {"version":3,"file":"metadata-response.js","sourceRoot":"","sources":["../../../src/responses/metadata-response.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,IAAI,EACJ,QAAQ,EACR,MAAM,GAEP,MAAM,qCAAqC,CAAC;AAqB7C;;;;;GAKG;AACH,MAAM,OAAO,gBAAgB;IA0C3B,iEAAiE;IACjE,YAAY,IAAyB;;QACnC,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;QACxB,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;QAC5B,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC,EAAE,CAAC;QAClB,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC,EAAE,CAAC;QAClB,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC;QACpB,iEAAiE;QACjE,IAAI,CAAC,KAAK,GAAG,MAAA,IAAI,CAAC,KAAK,0CAAE,GAAG,CAAC,CAAC,IAAyB,EAAE,EAAE,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QAC5E,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,WAAW,CAAC;QACpC,IAAI,CAAC,iBAAiB,GAAG,IAAI,CAAC,iBAAiB,CAAC;QAChD,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;QAChC,IAAI,CAAC,QAAQ,GAAG,IAAI,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC5C,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAC1B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACtB,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC,gBAAgB,CAAC;QAC9C,IAAI,CAAC,mBAAmB,GAAG,IAAI,CAAC,mBAAmB,CAAC;QACpD,IAAI,CAAC,OAAO,GAAG,MAAA,IAAI,CAAC,OAAO,0CAAE,GAAG;QAC9B,iEAAiE;QACjE,CAAC,KAA0B,EAAE,EAAE,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,CAClD,CAAC;QACF,IAAI,CAAC,mBAAmB,GAAG,IAAI,CAAC,mBAAmB,CAAC;QACpD,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;QACxB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;QACxB,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,WAAW,CAAC;QACpC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;IACxB,CAAC;CACF","sourcesContent":["import {\n File,\n Metadata,\n Review,\n SpeechMusicASREntry,\n} from '@internetarchive/iaux-item-metadata';\n\n/**\n * A single alternate download location: a server and the directory on it that\n * holds the item's files.\n */\nexport type AlternateLocation = {\n server: string;\n dir: string;\n};\n\n/**\n * Alternate download locations for an item, beyond the primary `server`/`dir`.\n * `servers` lists all known mirrors; `workable` is the subset currently\n * reachable for downloads.\n */\nexport type AlternateLocations = {\n servers: AlternateLocation[];\n workable: AlternateLocation[];\n};\n\n/**\n * The main top-level reponse when fetching Metadata\n *\n * @export\n * @class MetadataResponse\n */\nexport class MetadataResponse {\n /* eslint-disable-next-line @typescript-eslint/no-explicit-any */\n readonly rawResponse: Readonly<Record<string, any>>;\n\n readonly created: number;\n\n readonly d1: string;\n\n readonly d2: string;\n\n readonly dir: string;\n\n readonly files: File[];\n\n readonly files_count: number;\n\n readonly item_last_updated: number;\n\n readonly item_size: number;\n\n readonly metadata: Metadata;\n\n readonly server: string;\n\n readonly uniq: number;\n\n readonly workable_servers: string[];\n\n readonly speech_vs_music_asr?: SpeechMusicASREntry[];\n\n readonly reviews?: Review[];\n\n readonly alternate_locations?: AlternateLocations;\n\n readonly clips?: Record<string, unknown>;\n\n readonly plays?: Record<string, unknown>;\n\n readonly simplelists?: Record<string, unknown>;\n\n readonly solo?: boolean;\n\n /* eslint-disable-next-line @typescript-eslint/no-explicit-any */\n constructor(json: Record<string, any>) {\n this.rawResponse = json;\n this.created = json.created;\n this.d1 = json.d1;\n this.d2 = json.d2;\n this.dir = json.dir;\n /* eslint-disable-next-line @typescript-eslint/no-explicit-any */\n this.files = json.files?.map((file: Record<string, any>) => new File(file));\n this.files_count = json.files_count;\n this.item_last_updated = json.item_last_updated;\n this.item_size = json.item_size;\n this.metadata = new Metadata(json.metadata);\n this.server = json.server;\n this.uniq = json.uniq;\n this.workable_servers = json.workable_servers;\n this.speech_vs_music_asr = json.speech_vs_music_asr;\n this.reviews = json.reviews?.map(\n /* eslint-disable-next-line @typescript-eslint/no-explicit-any */\n (entry: Record<string, any>) => new Review(entry),\n );\n this.alternate_locations = json.alternate_locations;\n this.clips = json.clips;\n this.plays = json.plays;\n this.simplelists = json.simplelists;\n this.solo = json.solo;\n }\n}\n"]}
@@ -87,4 +87,56 @@ describe('DefaultMetadataBackend', () => {
87
87
  window.fetch = fetchBackup;
88
88
  });
89
89
  });
90
+ describe('DefaultMetadataBackend writeMetadata', () => {
91
+ const fetchBackup = window.fetch;
92
+ afterEach(() => {
93
+ window.fetch = fetchBackup;
94
+ });
95
+ it('posts the patch as a form to the item endpoint', async () => {
96
+ var _a, _b;
97
+ let requestUrl = '';
98
+ let requestInit;
99
+ window.fetch = async (input, init) => {
100
+ requestUrl = String(input);
101
+ requestInit = init;
102
+ return new Response('{ "success": true, "task_id": 7, "log": "x" }');
103
+ };
104
+ const backend = new DefaultMetadataBackend({ includeCredentials: true });
105
+ const patch = [{ op: 'replace', path: '/title', value: 'New' }];
106
+ const result = await backend.writeMetadata('foo', 'metadata', patch);
107
+ expect((_a = result.success) === null || _a === void 0 ? void 0 : _a.task_id).to.equal(7);
108
+ expect(requestUrl).to.equal('https://archive.org/metadata/foo');
109
+ expect(requestInit === null || requestInit === void 0 ? void 0 : requestInit.method).to.equal('POST');
110
+ expect(requestInit === null || requestInit === void 0 ? void 0 : requestInit.credentials).to.equal('include');
111
+ const body = requestInit === null || requestInit === void 0 ? void 0 : requestInit.body;
112
+ expect(body.get('-target')).to.equal('metadata');
113
+ expect(JSON.parse((_b = body.get('-patch')) !== null && _b !== void 0 ? _b : '')).to.deep.equal(patch);
114
+ });
115
+ it('returns a writeError with MDAPI’s message when the write is rejected', async () => {
116
+ var _a, _b;
117
+ window.fetch = async () => new Response('{ "success": false, "error": "Authorization failed" }', {
118
+ status: 401,
119
+ });
120
+ const backend = new DefaultMetadataBackend();
121
+ const result = await backend.writeMetadata('foo', 'metadata', []);
122
+ expect((_a = result.error) === null || _a === void 0 ? void 0 : _a.type).to.equal(MetadataServiceErrorType.writeError);
123
+ expect((_b = result.error) === null || _b === void 0 ? void 0 : _b.message).to.equal('Authorization failed');
124
+ });
125
+ it('returns a writeError when MDAPI does not report success', async () => {
126
+ var _a;
127
+ window.fetch = async () => new Response('{}');
128
+ const backend = new DefaultMetadataBackend();
129
+ const result = await backend.writeMetadata('foo', 'metadata', []);
130
+ expect((_a = result.error) === null || _a === void 0 ? void 0 : _a.type).to.equal(MetadataServiceErrorType.writeError);
131
+ });
132
+ it('returns a networkError when the request fails', async () => {
133
+ var _a;
134
+ window.fetch = async () => {
135
+ throw new Error('offline');
136
+ };
137
+ const backend = new DefaultMetadataBackend();
138
+ const result = await backend.writeMetadata('foo', 'metadata', []);
139
+ expect((_a = result.error) === null || _a === void 0 ? void 0 : _a.type).to.equal(MetadataServiceErrorType.networkError);
140
+ });
141
+ });
90
142
  //# sourceMappingURL=default-metadata-backend.test.js.map