@byline/core 4.16.0 → 4.18.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/dist/@types/admin-types.d.ts +86 -31
- package/dist/@types/admin-types.js +12 -0
- package/dist/@types/collection-types.d.ts +211 -29
- package/dist/@types/collection-types.js +68 -8
- package/dist/@types/db-types.d.ts +50 -0
- package/dist/@types/form-admin-config.test.node.d.ts +6 -0
- package/dist/@types/form-admin-config.test.node.js +26 -0
- package/dist/@types/search-types.d.ts +12 -0
- package/dist/@types/singleton-admin-config.test.node.js +66 -0
- package/dist/@types/singleton-definition.test.node.d.ts +8 -0
- package/dist/@types/singleton-definition.test.node.js +75 -0
- package/dist/@types/site-config.d.ts +9 -8
- package/dist/auth/apply-before-read.d.ts +2 -1
- package/dist/auth/apply-before-read.js +13 -3
- package/dist/auth/apply-before-read.test.node.js +44 -0
- package/dist/auth/assert-actor-can-perform.d.ts +4 -3
- package/dist/auth/assert-actor-can-perform.js +9 -7
- package/dist/auth/assert-actor-can-perform.test.node.js +32 -11
- package/dist/auth/filter-readable-collections.d.ts +6 -6
- package/dist/auth/filter-readable-collections.js +8 -8
- package/dist/auth/filter-readable-collections.test.node.js +26 -1
- package/dist/auth/index.d.ts +1 -1
- package/dist/auth/index.js +1 -1
- package/dist/auth/register-collection-abilities.d.ts +34 -30
- package/dist/auth/register-collection-abilities.js +81 -77
- package/dist/auth/register-collection-abilities.test.node.js +86 -8
- package/dist/codegen/fixtures/all-fields.expected.d.ts +3 -0
- package/dist/codegen/index.d.ts +1 -1
- package/dist/codegen/index.js +32 -16
- package/dist/codegen/index.test.node.js +35 -2
- package/dist/config/attach-hooks.d.ts +3 -3
- package/dist/config/attach-hooks.js +8 -0
- package/dist/config/attach-hooks.test.node.js +59 -0
- package/dist/config/config-hooks.test.node.js +34 -1
- package/dist/config/config.d.ts +2 -1
- package/dist/config/config.js +9 -1
- package/dist/config/group-collections.d.ts +10 -9
- package/dist/config/group-collections.js +4 -4
- package/dist/config/group-collections.test.node.js +14 -0
- package/dist/config/validate-admin-configs.d.ts +4 -4
- package/dist/config/validate-admin-configs.js +40 -7
- package/dist/config/validate-admin-configs.test.node.js +44 -0
- package/dist/config/validate-collections.js +43 -0
- package/dist/config/validate-collections.test.node.js +75 -0
- package/dist/core.d.ts +2 -3
- package/dist/core.js +3 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/lib/errors.d.ts +5 -3
- package/dist/lib/errors.js +1 -0
- package/dist/services/collection-bootstrap.js +12 -0
- package/dist/services/collection-bootstrap.test.node.js +63 -6
- package/dist/services/discover-counter-groups.test.node.js +6 -0
- package/dist/services/document-lifecycle/copy-to-locale.js +9 -17
- package/dist/services/document-lifecycle/create.js +6 -11
- package/dist/services/document-lifecycle/delete-locale.js +1 -1
- package/dist/services/document-lifecycle/delete.js +1 -1
- package/dist/services/document-lifecycle/duplicate.js +7 -18
- package/dist/services/document-lifecycle/persistence.d.ts +29 -0
- package/dist/services/document-lifecycle/persistence.js +42 -0
- package/dist/services/document-lifecycle/restore.js +9 -17
- package/dist/services/document-lifecycle/scheduled-publish.js +5 -5
- package/dist/services/document-lifecycle/status.js +3 -3
- package/dist/services/document-lifecycle/system-fields.js +1 -1
- package/dist/services/document-lifecycle/tree.js +2 -2
- package/dist/services/document-lifecycle/tree.test.node.js +6 -1
- package/dist/services/document-lifecycle/update.js +20 -36
- package/dist/services/document-lifecycle.test.node.js +35 -0
- package/dist/services/field-upload.d.ts +1 -1
- package/dist/services/field-upload.js +20 -6
- package/dist/services/field-upload.test.node.js +125 -9
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/services/populate.js +1 -1
- package/dist/services/populate.test.node.js +9 -0
- package/dist/services/richtext-populate.js +1 -1
- package/dist/services/singleton-lifecycle/copy-to-locale.d.ts +16 -0
- package/dist/services/singleton-lifecycle/copy-to-locale.js +100 -0
- package/dist/services/singleton-lifecycle/index.d.ts +14 -0
- package/dist/services/singleton-lifecycle/index.js +11 -0
- package/dist/services/singleton-lifecycle/internals.d.ts +49 -0
- package/dist/services/singleton-lifecycle/internals.js +118 -0
- package/dist/services/singleton-lifecycle/restore.d.ts +13 -0
- package/dist/services/singleton-lifecycle/restore.js +93 -0
- package/dist/services/singleton-lifecycle/update.d.ts +16 -0
- package/dist/services/singleton-lifecycle/update.js +107 -0
- package/dist/services/singleton-lifecycle.test.node.d.ts +8 -0
- package/dist/services/singleton-lifecycle.test.node.js +396 -0
- package/dist/storage/collection-fingerprint.js +18 -6
- package/dist/storage/collection-fingerprint.test.node.js +15 -0
- package/package.json +5 -5
- package/dist/config/resolve-item-view-columns.test.node.js +0 -30
- /package/dist/{config/resolve-item-view-columns.test.node.d.ts → @types/singleton-admin-config.test.node.d.ts} +0 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
import { ERR_NOT_FOUND, ERR_VALIDATION } from '../../lib/errors.js';
|
|
9
|
+
import { withLogContext } from '../../lib/logger.js';
|
|
10
|
+
import { getDefaultStatus } from '../../workflow/workflow.js';
|
|
11
|
+
import { applyRichTextEmbed } from '../document-lifecycle/internals.js';
|
|
12
|
+
import { mergeLocaleData } from '../document-lifecycle/merge-locale-data.js';
|
|
13
|
+
import { persistExistingDocumentVersion } from '../document-lifecycle/persistence.js';
|
|
14
|
+
import { authorizeSingletonUpdate, commitSingletonSave } from './internals.js';
|
|
15
|
+
/** Copy localized singleton fields to another locale on the mapped document. */
|
|
16
|
+
export async function copySingletonToLocale(ctx, params) {
|
|
17
|
+
return withLogContext({ domain: 'services', module: 'singleton-lifecycle', function: 'copySingletonToLocale' }, async () => {
|
|
18
|
+
const definition = authorizeSingletonUpdate(ctx);
|
|
19
|
+
if (params.sourceLocale === params.targetLocale) {
|
|
20
|
+
throw ERR_VALIDATION({
|
|
21
|
+
message: 'sourceLocale and targetLocale must differ',
|
|
22
|
+
details: { sourceLocale: params.sourceLocale, targetLocale: params.targetLocale },
|
|
23
|
+
}).log(ctx.logger);
|
|
24
|
+
}
|
|
25
|
+
return commitSingletonSave({
|
|
26
|
+
ctx,
|
|
27
|
+
definition,
|
|
28
|
+
operation: { type: 'copyToLocale', ...params },
|
|
29
|
+
prepare: async (slot) => {
|
|
30
|
+
if (slot.documentId == null || slot.currentVersion == null) {
|
|
31
|
+
throw ERR_NOT_FOUND({
|
|
32
|
+
message: `singleton '${ctx.collectionPath}' has not been saved`,
|
|
33
|
+
details: { singletonPath: ctx.collectionPath },
|
|
34
|
+
}).log(ctx.logger);
|
|
35
|
+
}
|
|
36
|
+
const source = await ctx.db.queries.documents.getDocumentById({
|
|
37
|
+
collection_id: ctx.collectionId,
|
|
38
|
+
document_id: slot.documentId,
|
|
39
|
+
locale: params.sourceLocale,
|
|
40
|
+
reconstruct: true,
|
|
41
|
+
lenient: true,
|
|
42
|
+
onMissingLocale: 'omit',
|
|
43
|
+
requestContext: ctx.requestContext,
|
|
44
|
+
});
|
|
45
|
+
if (source == null) {
|
|
46
|
+
throw ERR_NOT_FOUND({
|
|
47
|
+
message: 'singleton not found in source locale',
|
|
48
|
+
details: { singletonPath: ctx.collectionPath, sourceLocale: params.sourceLocale },
|
|
49
|
+
}).log(ctx.logger);
|
|
50
|
+
}
|
|
51
|
+
const existingTarget = await ctx.db.queries.documents.getDocumentById({
|
|
52
|
+
collection_id: ctx.collectionId,
|
|
53
|
+
document_id: slot.documentId,
|
|
54
|
+
locale: params.targetLocale,
|
|
55
|
+
reconstruct: true,
|
|
56
|
+
lenient: true,
|
|
57
|
+
onMissingLocale: 'omit',
|
|
58
|
+
requestContext: ctx.requestContext,
|
|
59
|
+
});
|
|
60
|
+
const target = existingTarget ??
|
|
61
|
+
(await ctx.db.queries.documents.getDocumentById({
|
|
62
|
+
collection_id: ctx.collectionId,
|
|
63
|
+
document_id: slot.documentId,
|
|
64
|
+
locale: params.targetLocale,
|
|
65
|
+
reconstruct: true,
|
|
66
|
+
lenient: true,
|
|
67
|
+
requestContext: ctx.requestContext,
|
|
68
|
+
}));
|
|
69
|
+
if (target == null) {
|
|
70
|
+
throw ERR_NOT_FOUND({
|
|
71
|
+
message: 'mapped singleton document not found',
|
|
72
|
+
details: { singletonPath: ctx.collectionPath, documentId: slot.documentId },
|
|
73
|
+
}).log(ctx.logger);
|
|
74
|
+
}
|
|
75
|
+
const sourceFields = source.fields ?? {};
|
|
76
|
+
const targetFields = target.fields ?? {};
|
|
77
|
+
const originalData = existingTarget == null
|
|
78
|
+
? null
|
|
79
|
+
: (existingTarget.fields ?? {});
|
|
80
|
+
const merged = mergeLocaleData(definition.fields, sourceFields, targetFields, params.overwrite);
|
|
81
|
+
return {
|
|
82
|
+
data: merged.data,
|
|
83
|
+
originalData,
|
|
84
|
+
locale: params.targetLocale,
|
|
85
|
+
write: async () => {
|
|
86
|
+
await applyRichTextEmbed(ctx, merged.data);
|
|
87
|
+
return persistExistingDocumentVersion(ctx, {
|
|
88
|
+
documentId: slot.documentId,
|
|
89
|
+
action: 'copy_to_locale',
|
|
90
|
+
documentData: merged.data,
|
|
91
|
+
status: getDefaultStatus(definition),
|
|
92
|
+
locale: params.targetLocale,
|
|
93
|
+
previousVersionId: slot.currentVersion?.document_version_id,
|
|
94
|
+
});
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
export { copySingletonToLocale } from './copy-to-locale.js';
|
|
9
|
+
export { resolveSingletonDocumentId } from './internals.js';
|
|
10
|
+
export { restoreSingletonVersion } from './restore.js';
|
|
11
|
+
export { updateSingleton } from './update.js';
|
|
12
|
+
export type { CopySingletonToLocaleParams } from './copy-to-locale.js';
|
|
13
|
+
export type { SingletonSaveResult } from './internals.js';
|
|
14
|
+
export type { UpdateSingletonParams } from './update.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
export { copySingletonToLocale } from './copy-to-locale.js';
|
|
9
|
+
export { resolveSingletonDocumentId } from './internals.js';
|
|
10
|
+
export { restoreSingletonVersion } from './restore.js';
|
|
11
|
+
export { updateSingleton } from './update.js';
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
import type { IDocumentCommands, IDocumentQueries, SingletonDefinition, SingletonSaveOperation } from '../../@types/index.js';
|
|
9
|
+
import type { DocumentLifecycleContext } from '../document-lifecycle/context.js';
|
|
10
|
+
type VersionWriteResult = Awaited<ReturnType<IDocumentCommands['createDocumentVersion']>>;
|
|
11
|
+
type CurrentVersionMetadata = NonNullable<Awaited<ReturnType<IDocumentQueries['getCurrentVersionMetadata']>>>;
|
|
12
|
+
export interface SingletonSaveResult {
|
|
13
|
+
documentId: string;
|
|
14
|
+
documentVersionId: string;
|
|
15
|
+
}
|
|
16
|
+
export interface LockedSingletonSlot {
|
|
17
|
+
documentId: string | null;
|
|
18
|
+
currentVersion: CurrentVersionMetadata | null;
|
|
19
|
+
}
|
|
20
|
+
interface PreparedSingletonSave {
|
|
21
|
+
data: Record<string, any>;
|
|
22
|
+
originalData: Record<string, any> | null;
|
|
23
|
+
locale: string;
|
|
24
|
+
write: () => Promise<VersionWriteResult>;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Assert the kind-aware singleton update ability without touching storage.
|
|
28
|
+
* The runtime kind guard keeps this internal service from becoming an
|
|
29
|
+
* alternate write path for an ordinary collection.
|
|
30
|
+
*/
|
|
31
|
+
export declare function authorizeSingletonUpdate(ctx: DocumentLifecycleContext): SingletonDefinition;
|
|
32
|
+
/**
|
|
33
|
+
* Resolve the mapped logical document id without running read authorization
|
|
34
|
+
* or hooks. Client-facing callers must authorize before invoking this resolver.
|
|
35
|
+
*/
|
|
36
|
+
export declare function resolveSingletonDocumentId(ctx: DocumentLifecycleContext): Promise<string | null>;
|
|
37
|
+
/**
|
|
38
|
+
* Coordinate one singleton version write under the registered-slot lock.
|
|
39
|
+
* `prepare` runs after the mapping and current version have been read while
|
|
40
|
+
* locked. The before hook and write remain inside the outer transaction; the
|
|
41
|
+
* after hook cannot run until that transaction has committed.
|
|
42
|
+
*/
|
|
43
|
+
export declare function commitSingletonSave(params: {
|
|
44
|
+
ctx: DocumentLifecycleContext;
|
|
45
|
+
definition: SingletonDefinition;
|
|
46
|
+
operation: SingletonSaveOperation;
|
|
47
|
+
prepare: (slot: LockedSingletonSlot) => Promise<PreparedSingletonSave>;
|
|
48
|
+
}): Promise<SingletonSaveResult>;
|
|
49
|
+
export {};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
import { resolveHooks } from '../../@types/index.js';
|
|
9
|
+
import { assertActorCanPerform } from '../../auth/assert-actor-can-perform.js';
|
|
10
|
+
import { DbErrorCodes, ERR_CONFLICT, ERR_VALIDATION } from '../../lib/errors.js';
|
|
11
|
+
import { extractDocumentId, extractVersionId } from '../document-lifecycle/internals.js';
|
|
12
|
+
/**
|
|
13
|
+
* Assert the kind-aware singleton update ability without touching storage.
|
|
14
|
+
* The runtime kind guard keeps this internal service from becoming an
|
|
15
|
+
* alternate write path for an ordinary collection.
|
|
16
|
+
*/
|
|
17
|
+
export function authorizeSingletonUpdate(ctx) {
|
|
18
|
+
if (ctx.definition.singleton !== true) {
|
|
19
|
+
throw ERR_VALIDATION({
|
|
20
|
+
message: `singleton lifecycle requires a singleton definition; '${ctx.collectionPath}' is a collection`,
|
|
21
|
+
details: { path: ctx.collectionPath, expectedKind: 'singleton' },
|
|
22
|
+
}).log(ctx.logger);
|
|
23
|
+
}
|
|
24
|
+
assertActorCanPerform(ctx.requestContext, ctx.definition, 'update');
|
|
25
|
+
return ctx.definition;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Resolve the mapped logical document id without running read authorization
|
|
29
|
+
* or hooks. Client-facing callers must authorize before invoking this resolver.
|
|
30
|
+
*/
|
|
31
|
+
export function resolveSingletonDocumentId(ctx) {
|
|
32
|
+
return ctx.db.queries.singletons.getMappedDocumentId(ctx.collectionId);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Coordinate one singleton version write under the registered-slot lock.
|
|
36
|
+
* `prepare` runs after the mapping and current version have been read while
|
|
37
|
+
* locked. The before hook and write remain inside the outer transaction; the
|
|
38
|
+
* after hook cannot run until that transaction has committed.
|
|
39
|
+
*/
|
|
40
|
+
export async function commitSingletonSave(params) {
|
|
41
|
+
const { ctx, definition, operation } = params;
|
|
42
|
+
const hooks = await resolveHooks(definition);
|
|
43
|
+
const requestContext = ctx.requestContext;
|
|
44
|
+
if (requestContext == null) {
|
|
45
|
+
throw new Error('singleton authorization completed without a request context');
|
|
46
|
+
}
|
|
47
|
+
const committed = await ctx.db.withTransaction(async () => {
|
|
48
|
+
await ctx.db.commands.singletons.lockSlot(ctx.collectionId);
|
|
49
|
+
const mappedDocumentId = await resolveSingletonDocumentId(ctx);
|
|
50
|
+
const currentVersion = mappedDocumentId == null
|
|
51
|
+
? null
|
|
52
|
+
: await ctx.db.queries.documents.getCurrentVersionMetadata({
|
|
53
|
+
collection_id: ctx.collectionId,
|
|
54
|
+
document_id: mappedDocumentId,
|
|
55
|
+
});
|
|
56
|
+
if (mappedDocumentId != null && currentVersion == null) {
|
|
57
|
+
throw ERR_CONFLICT({
|
|
58
|
+
message: `singleton '${ctx.collectionPath}' is mapped to a deleted or unavailable document; ` +
|
|
59
|
+
'clear the mapping deliberately before rematerialising the slot',
|
|
60
|
+
details: { singletonPath: ctx.collectionPath, documentId: mappedDocumentId },
|
|
61
|
+
}).log(ctx.logger);
|
|
62
|
+
}
|
|
63
|
+
const slot = { documentId: mappedDocumentId, currentVersion };
|
|
64
|
+
const prepared = await params.prepare(slot);
|
|
65
|
+
const isInitialSave = mappedDocumentId == null;
|
|
66
|
+
const beforeSaveContext = {
|
|
67
|
+
data: prepared.data,
|
|
68
|
+
originalData: prepared.originalData,
|
|
69
|
+
singletonPath: ctx.collectionPath,
|
|
70
|
+
locale: prepared.locale,
|
|
71
|
+
requestContext,
|
|
72
|
+
isInitialSave,
|
|
73
|
+
operation,
|
|
74
|
+
documentId: mappedDocumentId,
|
|
75
|
+
};
|
|
76
|
+
await invokeSingletonHook(hooks?.beforeSave, beforeSaveContext);
|
|
77
|
+
const writeResult = await prepared.write();
|
|
78
|
+
const documentId = extractDocumentId(writeResult.document) || mappedDocumentId || '';
|
|
79
|
+
const documentVersionId = extractVersionId(writeResult.document);
|
|
80
|
+
if (documentId === '' || documentVersionId === '') {
|
|
81
|
+
throw new Error('singleton persistence did not return document and version ids');
|
|
82
|
+
}
|
|
83
|
+
if (isInitialSave) {
|
|
84
|
+
try {
|
|
85
|
+
await ctx.db.commands.singletons.setMapping(ctx.collectionId, documentId);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (ctx.db.classifyError?.(error)?.code === DbErrorCodes.UNIQUE_VIOLATION) {
|
|
89
|
+
throw ERR_CONFLICT({
|
|
90
|
+
message: `singleton '${ctx.collectionPath}' was materialised concurrently`,
|
|
91
|
+
details: { singletonPath: ctx.collectionPath },
|
|
92
|
+
}).log(ctx.logger);
|
|
93
|
+
}
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
documentId,
|
|
99
|
+
documentVersionId,
|
|
100
|
+
afterSaveContext: {
|
|
101
|
+
...beforeSaveContext,
|
|
102
|
+
documentId,
|
|
103
|
+
documentVersionId,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
await invokeSingletonHook(hooks?.afterSave, committed.afterSaveContext);
|
|
108
|
+
return {
|
|
109
|
+
documentId: committed.documentId,
|
|
110
|
+
documentVersionId: committed.documentVersionId,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
async function invokeSingletonHook(hook, context) {
|
|
114
|
+
const hooks = hook == null ? [] : Array.isArray(hook) ? hook : [hook];
|
|
115
|
+
for (const invoke of hooks) {
|
|
116
|
+
await invoke(context);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
import type { DocumentLifecycleContext } from '../document-lifecycle/context.js';
|
|
9
|
+
import type { SingletonSaveResult } from './internals.js';
|
|
10
|
+
/** Restore one historical singleton version as the new current version. */
|
|
11
|
+
export declare function restoreSingletonVersion(ctx: DocumentLifecycleContext, params: {
|
|
12
|
+
sourceVersionId: string;
|
|
13
|
+
}): Promise<SingletonSaveResult>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
import { ERR_INVALID_TRANSITION, ERR_NOT_FOUND, ERR_VALIDATION } from '../../lib/errors.js';
|
|
9
|
+
import { withLogContext } from '../../lib/logger.js';
|
|
10
|
+
import { getDefaultStatus } from '../../workflow/workflow.js';
|
|
11
|
+
import { applyRichTextEmbed } from '../document-lifecycle/internals.js';
|
|
12
|
+
import { persistExistingDocumentVersion } from '../document-lifecycle/persistence.js';
|
|
13
|
+
import { authorizeSingletonUpdate, commitSingletonSave } from './internals.js';
|
|
14
|
+
/** Restore one historical singleton version as the new current version. */
|
|
15
|
+
export async function restoreSingletonVersion(ctx, params) {
|
|
16
|
+
return withLogContext({ domain: 'services', module: 'singleton-lifecycle', function: 'restoreSingletonVersion' }, async () => {
|
|
17
|
+
const definition = authorizeSingletonUpdate(ctx);
|
|
18
|
+
return commitSingletonSave({
|
|
19
|
+
ctx,
|
|
20
|
+
definition,
|
|
21
|
+
operation: { type: 'restore', sourceVersionId: params.sourceVersionId },
|
|
22
|
+
prepare: async (slot) => {
|
|
23
|
+
if (slot.documentId == null || slot.currentVersion == null) {
|
|
24
|
+
throw ERR_NOT_FOUND({
|
|
25
|
+
message: `singleton '${ctx.collectionPath}' has not been saved`,
|
|
26
|
+
details: { singletonPath: ctx.collectionPath },
|
|
27
|
+
}).log(ctx.logger);
|
|
28
|
+
}
|
|
29
|
+
const source = await ctx.db.queries.documents.getDocumentByVersion({
|
|
30
|
+
document_version_id: params.sourceVersionId,
|
|
31
|
+
collection_id: ctx.collectionId,
|
|
32
|
+
locale: 'all',
|
|
33
|
+
});
|
|
34
|
+
if (source == null) {
|
|
35
|
+
throw ERR_NOT_FOUND({
|
|
36
|
+
message: 'source version not found',
|
|
37
|
+
details: { sourceVersionId: params.sourceVersionId },
|
|
38
|
+
}).log(ctx.logger);
|
|
39
|
+
}
|
|
40
|
+
const sourceRecord = source;
|
|
41
|
+
if (sourceRecord.document_id !== slot.documentId) {
|
|
42
|
+
throw ERR_VALIDATION({
|
|
43
|
+
message: 'source version does not belong to the singleton document',
|
|
44
|
+
details: {
|
|
45
|
+
singletonPath: ctx.collectionPath,
|
|
46
|
+
sourceVersionId: params.sourceVersionId,
|
|
47
|
+
sourceDocumentId: sourceRecord.document_id,
|
|
48
|
+
},
|
|
49
|
+
}).log(ctx.logger);
|
|
50
|
+
}
|
|
51
|
+
if (slot.currentVersion.document_version_id === params.sourceVersionId) {
|
|
52
|
+
throw ERR_INVALID_TRANSITION({
|
|
53
|
+
message: 'source version is already the current singleton version',
|
|
54
|
+
details: {
|
|
55
|
+
singletonPath: ctx.collectionPath,
|
|
56
|
+
sourceVersionId: params.sourceVersionId,
|
|
57
|
+
},
|
|
58
|
+
}).log(ctx.logger);
|
|
59
|
+
}
|
|
60
|
+
const current = await ctx.db.queries.documents.getDocumentById({
|
|
61
|
+
collection_id: ctx.collectionId,
|
|
62
|
+
document_id: slot.documentId,
|
|
63
|
+
locale: 'all',
|
|
64
|
+
reconstruct: true,
|
|
65
|
+
});
|
|
66
|
+
if (current == null) {
|
|
67
|
+
throw ERR_NOT_FOUND({
|
|
68
|
+
message: 'current singleton version not found',
|
|
69
|
+
details: { singletonPath: ctx.collectionPath, documentId: slot.documentId },
|
|
70
|
+
}).log(ctx.logger);
|
|
71
|
+
}
|
|
72
|
+
const data = sourceRecord.fields ?? {};
|
|
73
|
+
const originalData = current.fields ?? {};
|
|
74
|
+
return {
|
|
75
|
+
data,
|
|
76
|
+
originalData,
|
|
77
|
+
locale: 'all',
|
|
78
|
+
write: async () => {
|
|
79
|
+
await applyRichTextEmbed(ctx, data);
|
|
80
|
+
return persistExistingDocumentVersion(ctx, {
|
|
81
|
+
documentId: slot.documentId,
|
|
82
|
+
action: 'restore',
|
|
83
|
+
documentData: data,
|
|
84
|
+
status: getDefaultStatus(definition),
|
|
85
|
+
locale: 'all',
|
|
86
|
+
previousVersionId: slot.currentVersion?.document_version_id,
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
import type { DocumentLifecycleContext } from '../document-lifecycle/context.js';
|
|
9
|
+
import type { SingletonSaveResult } from './internals.js';
|
|
10
|
+
export interface UpdateSingletonParams {
|
|
11
|
+
data: Record<string, any>;
|
|
12
|
+
locale?: string;
|
|
13
|
+
expectedVersionId?: string;
|
|
14
|
+
}
|
|
15
|
+
/** Materialise or update a singleton slot as one immutable document version. */
|
|
16
|
+
export declare function updateSingleton(ctx: DocumentLifecycleContext, params: UpdateSingletonParams): Promise<SingletonSaveResult>;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
import { ERR_CONFLICT, ERR_VALIDATION } from '../../lib/errors.js';
|
|
9
|
+
import { withLogContext } from '../../lib/logger.js';
|
|
10
|
+
import { normaliseDateFields } from '../../utils/normalise-dates.js';
|
|
11
|
+
import { getDefaultStatus } from '../../workflow/workflow.js';
|
|
12
|
+
import { assignCounterValues } from '../assign-counter-values.js';
|
|
13
|
+
import { applyRichTextEmbed } from '../document-lifecycle/internals.js';
|
|
14
|
+
import { persistExistingDocumentVersion, persistInitialDocumentVersion, } from '../document-lifecycle/persistence.js';
|
|
15
|
+
import { normalizeNumericFields } from '../normalize-numeric-fields.js';
|
|
16
|
+
import { authorizeSingletonUpdate, commitSingletonSave } from './internals.js';
|
|
17
|
+
/** Materialise or update a singleton slot as one immutable document version. */
|
|
18
|
+
export async function updateSingleton(ctx, params) {
|
|
19
|
+
return withLogContext({ domain: 'services', module: 'singleton-lifecycle', function: 'updateSingleton' }, async () => {
|
|
20
|
+
const definition = authorizeSingletonUpdate(ctx);
|
|
21
|
+
const locale = params.locale ?? ctx.defaultLocale;
|
|
22
|
+
const data = params.data;
|
|
23
|
+
return commitSingletonSave({
|
|
24
|
+
ctx,
|
|
25
|
+
definition,
|
|
26
|
+
operation: { type: 'save' },
|
|
27
|
+
prepare: async (slot) => {
|
|
28
|
+
if (slot.documentId == null && locale !== ctx.defaultLocale) {
|
|
29
|
+
throw ERR_VALIDATION({
|
|
30
|
+
message: `a singleton's first save must use the default content locale ` +
|
|
31
|
+
`('${ctx.defaultLocale}'); received '${locale}'`,
|
|
32
|
+
details: {
|
|
33
|
+
singletonPath: ctx.collectionPath,
|
|
34
|
+
defaultLocale: ctx.defaultLocale,
|
|
35
|
+
providedLocale: locale,
|
|
36
|
+
},
|
|
37
|
+
}).log(ctx.logger);
|
|
38
|
+
}
|
|
39
|
+
if (params.expectedVersionId != null) {
|
|
40
|
+
const currentVersionId = slot.currentVersion?.document_version_id;
|
|
41
|
+
if (currentVersionId !== params.expectedVersionId) {
|
|
42
|
+
throw ERR_CONFLICT({
|
|
43
|
+
message: 'singleton version conflict',
|
|
44
|
+
details: {
|
|
45
|
+
singletonPath: ctx.collectionPath,
|
|
46
|
+
expectedVersionId: params.expectedVersionId,
|
|
47
|
+
currentVersionId: currentVersionId ?? null,
|
|
48
|
+
},
|
|
49
|
+
}).log(ctx.logger);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const current = slot.documentId == null
|
|
53
|
+
? null
|
|
54
|
+
: await ctx.db.queries.documents.getDocumentById({
|
|
55
|
+
collection_id: ctx.collectionId,
|
|
56
|
+
document_id: slot.documentId,
|
|
57
|
+
locale,
|
|
58
|
+
reconstruct: true,
|
|
59
|
+
lenient: true,
|
|
60
|
+
requestContext: ctx.requestContext,
|
|
61
|
+
});
|
|
62
|
+
if (slot.documentId != null && current == null) {
|
|
63
|
+
throw ERR_CONFLICT({
|
|
64
|
+
message: `singleton '${ctx.collectionPath}' could not reconstruct its mapped document`,
|
|
65
|
+
details: { singletonPath: ctx.collectionPath, documentId: slot.documentId },
|
|
66
|
+
}).log(ctx.logger);
|
|
67
|
+
}
|
|
68
|
+
const currentRecord = current;
|
|
69
|
+
const originalData = currentRecord?.fields ?? null;
|
|
70
|
+
normaliseDateFields(data);
|
|
71
|
+
normalizeNumericFields(definition.fields, data);
|
|
72
|
+
return {
|
|
73
|
+
data,
|
|
74
|
+
originalData,
|
|
75
|
+
locale,
|
|
76
|
+
write: async () => {
|
|
77
|
+
normalizeNumericFields(definition.fields, data);
|
|
78
|
+
await assignCounterValues({
|
|
79
|
+
fields: definition.fields,
|
|
80
|
+
data,
|
|
81
|
+
previousData: originalData ?? undefined,
|
|
82
|
+
counters: ctx.db.commands.counters,
|
|
83
|
+
});
|
|
84
|
+
await applyRichTextEmbed(ctx, data);
|
|
85
|
+
if (slot.documentId == null) {
|
|
86
|
+
return persistInitialDocumentVersion(ctx, {
|
|
87
|
+
action: 'create',
|
|
88
|
+
documentData: data,
|
|
89
|
+
path: crypto.randomUUID(),
|
|
90
|
+
status: getDefaultStatus(definition),
|
|
91
|
+
locale,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return persistExistingDocumentVersion(ctx, {
|
|
95
|
+
documentId: slot.documentId,
|
|
96
|
+
action: 'update',
|
|
97
|
+
documentData: data,
|
|
98
|
+
status: getDefaultStatus(definition),
|
|
99
|
+
locale,
|
|
100
|
+
previousVersionId: slot.currentVersion?.document_version_id,
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
export {};
|