@forge-cms/runtime 0.5.0 → 0.6.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/auth-managed.d.ts +19 -0
- package/dist/auth-managed.d.ts.map +1 -0
- package/dist/auth-managed.js +24 -0
- package/dist/auth-managed.js.map +1 -0
- package/dist/concurrency.d.ts +8 -0
- package/dist/concurrency.d.ts.map +1 -0
- package/dist/concurrency.js +15 -0
- package/dist/concurrency.js.map +1 -0
- package/dist/context.d.ts +5 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/errors.d.ts +29 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +38 -0
- package/dist/errors.js.map +1 -1
- package/dist/files.d.ts +15 -1
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +55 -14
- package/dist/files.js.map +1 -1
- package/dist/globals.d.ts +21 -3
- package/dist/globals.d.ts.map +1 -1
- package/dist/globals.js +216 -28
- package/dist/globals.js.map +1 -1
- package/dist/handlers.d.ts +3 -2
- package/dist/handlers.d.ts.map +1 -1
- package/dist/handlers.js +36 -47
- package/dist/handlers.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/localization.d.ts +17 -0
- package/dist/localization.d.ts.map +1 -1
- package/dist/localization.js +56 -0
- package/dist/localization.js.map +1 -1
- package/dist/operations.d.ts +51 -12
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +728 -125
- package/dist/operations.js.map +1 -1
- package/dist/relation-integrity.d.ts +31 -13
- package/dist/relation-integrity.d.ts.map +1 -1
- package/dist/relation-integrity.js +62 -6
- package/dist/relation-integrity.js.map +1 -1
- package/dist/relation-lifecycle.d.ts +95 -0
- package/dist/relation-lifecycle.d.ts.map +1 -0
- package/dist/relation-lifecycle.js +424 -0
- package/dist/relation-lifecycle.js.map +1 -0
- package/dist/runtime.d.ts +21 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +112 -20
- package/dist/runtime.js.map +1 -1
- package/dist/storage-intents.d.ts +96 -0
- package/dist/storage-intents.d.ts.map +1 -0
- package/dist/storage-intents.js +217 -0
- package/dist/storage-intents.js.map +1 -0
- package/dist/system-fields.d.ts +27 -0
- package/dist/system-fields.d.ts.map +1 -0
- package/dist/system-fields.js +74 -0
- package/dist/system-fields.js.map +1 -0
- package/dist/versions.d.ts +73 -1
- package/dist/versions.d.ts.map +1 -1
- package/dist/versions.js +204 -17
- package/dist/versions.js.map +1 -1
- package/package.json +9 -7
package/dist/runtime.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isReferenced, noReferenceAssertions, validateRelationSchema } from './relation-lifecycle.js';
|
|
2
2
|
import * as operations from './operations.js';
|
|
3
|
+
import { validateLocalizationSchema } from './localization.js';
|
|
4
|
+
import { hasUploadCollections, reconcileStorage, storageIntentsDefinition } from './storage-intents.js';
|
|
3
5
|
import * as globalOps from './globals.js';
|
|
4
6
|
import * as versionOps from './versions.js';
|
|
5
7
|
/**
|
|
@@ -19,8 +21,59 @@ export class ForgeCmsRuntime {
|
|
|
19
21
|
config;
|
|
20
22
|
adapters;
|
|
21
23
|
constructor(config) {
|
|
24
|
+
// Reference shapes relation integrity cannot enforce are refused here, at startup, instead of being
|
|
25
|
+
// accepted and silently ignored on every delete (spec 064 §2).
|
|
26
|
+
const relationErrors = validateRelationSchema(config.collections, config.globals ?? [], (slug) => config.adapters.auth.managesCollection?.(slug) === true);
|
|
27
|
+
if (relationErrors.length > 0) {
|
|
28
|
+
throw new Error(`Unsupported relation configuration:\n${relationErrors.join('\n')}`);
|
|
29
|
+
}
|
|
30
|
+
// Global options that could never apply are refused too, instead of silently ignored (spec 066).
|
|
31
|
+
const globalErrors = [
|
|
32
|
+
...globalOps.validateGlobalSchema(config.globals ?? []),
|
|
33
|
+
...validateLocalizationSchema([
|
|
34
|
+
...config.collections.map((c) => ({
|
|
35
|
+
label: `Collection '${c.slug}'`,
|
|
36
|
+
fields: c.fields,
|
|
37
|
+
...(c.locales !== undefined && { locales: c.locales })
|
|
38
|
+
})),
|
|
39
|
+
...(config.globals ?? []).map((g) => ({
|
|
40
|
+
label: `Global '${g.slug}'`,
|
|
41
|
+
fields: g.fields,
|
|
42
|
+
...(g.locales !== undefined && { locales: g.locales })
|
|
43
|
+
}))
|
|
44
|
+
])
|
|
45
|
+
];
|
|
46
|
+
if (globalErrors.length > 0) {
|
|
47
|
+
throw new Error(`Unsupported global/localization configuration:\n${globalErrors.join('\n')}`);
|
|
48
|
+
}
|
|
22
49
|
this.config = config;
|
|
23
50
|
this.adapters = config.adapters;
|
|
51
|
+
this.wireManagedDeleteGuards();
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Hands every auth-managed collection's relation guard to the auth adapter (spec 065), so its own
|
|
55
|
+
* user delete commits "nothing references this document" in the same batch — no setup code needed.
|
|
56
|
+
* An adapter that manages a referenced collection but cannot enforce the guard is refused here: it
|
|
57
|
+
* would otherwise delete documents that content still references.
|
|
58
|
+
*/
|
|
59
|
+
wireManagedDeleteGuards() {
|
|
60
|
+
const auth = this.adapters.auth;
|
|
61
|
+
for (const { slug } of this.config.collections) {
|
|
62
|
+
if (auth.managesCollection?.(slug) !== true)
|
|
63
|
+
continue;
|
|
64
|
+
const referenced = isReferenced(this, slug);
|
|
65
|
+
const enforced = auth.setManagedDeleteGuard?.(slug, {
|
|
66
|
+
database: this.adapters.database,
|
|
67
|
+
assertions: (id) => noReferenceAssertions(this, slug, [id])
|
|
68
|
+
}) === true;
|
|
69
|
+
if (referenced && !enforced) {
|
|
70
|
+
throw new Error(`Unsupported relation configuration: collection '${slug}' is managed by the auth adapter ` +
|
|
71
|
+
`'${auth.name}' and referenced by relation/upload fields, but that adapter cannot enforce ` +
|
|
72
|
+
`those references when it deletes a document (it does not implement setManagedDeleteGuard, ` +
|
|
73
|
+
`spec 065), so a deletion could leave them dangling. Use an adapter that supports it (e.g. ` +
|
|
74
|
+
`UsersCollectionAuthAdapter), or store the id in a text field as an explicit unchecked reference.`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
24
77
|
}
|
|
25
78
|
/** Initialise all adapters with the runtime environment */
|
|
26
79
|
init() {
|
|
@@ -34,6 +87,10 @@ export class ForgeCmsRuntime {
|
|
|
34
87
|
async syncSchema() {
|
|
35
88
|
await this.adapters.database.syncSchema(this.config.collections);
|
|
36
89
|
await this.adapters.auth.syncSchema?.();
|
|
90
|
+
// Durable storage-cleanup intents (spec 067), only where uploads exist.
|
|
91
|
+
if (hasUploadCollections(this.config.collections)) {
|
|
92
|
+
await this.adapters.database.syncSchema([storageIntentsDefinition()]);
|
|
93
|
+
}
|
|
37
94
|
for (const global of this.config.globals ?? []) {
|
|
38
95
|
await this.adapters.database.syncSchema([
|
|
39
96
|
{
|
|
@@ -43,27 +100,53 @@ export class ForgeCmsRuntime {
|
|
|
43
100
|
}
|
|
44
101
|
]);
|
|
45
102
|
}
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
createdAt: defineField.date({ required: true }),
|
|
55
|
-
createdBy: defineField.text(),
|
|
56
|
-
autosave: defineField.boolean(),
|
|
57
|
-
label: defineField.text()
|
|
58
|
-
};
|
|
59
|
-
await this.adapters.database.syncSchema([
|
|
60
|
-
{
|
|
61
|
-
slug: `_versions_${collection.slug}`,
|
|
62
|
-
fields: versionFields
|
|
63
|
-
}
|
|
64
|
-
]);
|
|
103
|
+
// Version tables for collections with versions enabled (spec 062 §1/§8).
|
|
104
|
+
const versioned = this.config.collections.filter((c) => versionOps.versionsEnabled(c));
|
|
105
|
+
if (versioned.length > 0) {
|
|
106
|
+
const database = this.adapters.database;
|
|
107
|
+
if (typeof database.atomicWrite !== 'function') {
|
|
108
|
+
throw new Error(`Collections with versions enabled (${versioned.map((c) => `'${c.slug}'`).join(', ')}) ` +
|
|
109
|
+
`require a DatabaseAdapter implementing atomicWrite() — a document and its version ` +
|
|
110
|
+
`snapshot must commit together (specs 060/062); '${String(database.name)}' does not.`);
|
|
65
111
|
}
|
|
66
112
|
}
|
|
113
|
+
for (const collection of versioned) {
|
|
114
|
+
await this.syncVersionTable(collection);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Creates/extends one `_versions_<slug>` table additively. Adding its unique
|
|
119
|
+
* `(documentId, versionNumber)` index fails on a database that already holds duplicate version
|
|
120
|
+
* identities (only the pre-062 read-then-insert race produced them): history is then reported, never
|
|
121
|
+
* deleted, renumbered or merged — the operator decides (spec 062 §8, roadmap 0.7 / M03).
|
|
122
|
+
*/
|
|
123
|
+
async syncVersionTable(collection) {
|
|
124
|
+
const definition = versionOps.versionCollectionDefinition(collection.slug);
|
|
125
|
+
try {
|
|
126
|
+
await this.adapters.database.syncSchema([definition]);
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
let duplicates;
|
|
130
|
+
try {
|
|
131
|
+
duplicates = await versionOps.findDuplicateVersionIdentities(this.adapters.database, definition.slug);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
throw err;
|
|
135
|
+
}
|
|
136
|
+
if (duplicates.length === 0)
|
|
137
|
+
throw err;
|
|
138
|
+
const examples = duplicates
|
|
139
|
+
.slice(0, 5)
|
|
140
|
+
.map((d) => `document "${d.documentId}" version ${d.versionNumber} (${d.rows} rows)`)
|
|
141
|
+
.join('; ');
|
|
142
|
+
throw new Error(`Cannot add the unique (documentId, versionNumber) index to "${definition.slug}": it already ` +
|
|
143
|
+
`contains ${duplicates.length} duplicate version ${duplicates.length === 1 ? 'identity' : 'identities'} ` +
|
|
144
|
+
`— e.g. ${examples}. They were produced by concurrent updates before ForgeCMS enforced ` +
|
|
145
|
+
`version identity. ForgeCMS will not delete, renumber or merge version history automatically. ` +
|
|
146
|
+
`Inspect them with: SELECT "documentId", "versionNumber", COUNT(*) FROM "${definition.slug}" ` +
|
|
147
|
+
`GROUP BY "documentId", "versionNumber" HAVING COUNT(*) > 1; decide which rows to keep ` +
|
|
148
|
+
`(renumber or delete the extras after taking a backup), then restart.`, { cause: err });
|
|
149
|
+
}
|
|
67
150
|
}
|
|
68
151
|
/** Find a collection definition by slug */
|
|
69
152
|
getCollection(slug) {
|
|
@@ -133,6 +216,15 @@ export class ForgeCmsRuntime {
|
|
|
133
216
|
createVersion(args) {
|
|
134
217
|
return versionOps.createVersion(this, args);
|
|
135
218
|
}
|
|
219
|
+
// --- Storage ----------------------------------------------------------------------------
|
|
220
|
+
/**
|
|
221
|
+
* Deletes the stored objects that crashed or failed uploads and deletes left owned by no document, as
|
|
222
|
+
* recorded by their durable storage intents (spec 067). Safe to run repeatedly and concurrently; run it
|
|
223
|
+
* from a scheduled job or an operator script. See `reconcileStorage` for the exact guarantees.
|
|
224
|
+
*/
|
|
225
|
+
reconcileStorage(options) {
|
|
226
|
+
return reconcileStorage(this, options);
|
|
227
|
+
}
|
|
136
228
|
// --- Preview ----------------------------------------------------------------------------
|
|
137
229
|
/**
|
|
138
230
|
* A non-persistent simulation of a permitted create/update (spec 058 §3) — merges stored data with
|
package/dist/runtime.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { CollectionDefinition } from '@forge-cms/core';
|
|
2
|
+
import type { AtomicWriteOperation, DatabaseAdapter } from '@forge-cms/db';
|
|
3
|
+
import type { OperationContext } from './context.js';
|
|
4
|
+
/**
|
|
5
|
+
* Durable storage-cleanup intents (spec 067). A database transaction cannot include object storage, so
|
|
6
|
+
* every step where an object can end up owned by no document leaves a row here **first**, in the
|
|
7
|
+
* database, and removes it only once the object is accounted for:
|
|
8
|
+
*
|
|
9
|
+
* - **Upload:** an `upload` intent is written *before* the object is stored, and removed in the **same
|
|
10
|
+
* batch** that creates the owning document. A process that dies between storing the object and
|
|
11
|
+
* committing the document leaves the intent behind.
|
|
12
|
+
* - **Delete:** a `delete` intent is written in the **same batch** that deletes the owning document, and
|
|
13
|
+
* removed once the object is deleted. A failed or interrupted object delete leaves it behind.
|
|
14
|
+
*
|
|
15
|
+
* A remaining intent means exactly "this object belongs to no document; delete it". {@link reconcileStorage}
|
|
16
|
+
* works them off. Nothing here is transactional with the bucket; the row is what survives a crash.
|
|
17
|
+
*/
|
|
18
|
+
export declare const STORAGE_INTENTS_COLLECTION = "_forge_storage_intents";
|
|
19
|
+
/** How long an `upload` intent is left alone: its upload may still be committing its document. */
|
|
20
|
+
export declare const DEFAULT_UPLOAD_GRACE_MS: number;
|
|
21
|
+
/**
|
|
22
|
+
* The internal collection, built without `defineCollection()` because its identifier validation
|
|
23
|
+
* reserves the `_forge_` prefix for Forge's own tables — the same pattern as `_forge_bootstrap`.
|
|
24
|
+
*/
|
|
25
|
+
export declare function storageIntentsDefinition(): CollectionDefinition;
|
|
26
|
+
export declare function hasUploadCollections(collections: readonly CollectionDefinition[]): boolean;
|
|
27
|
+
/** Records, before the object is stored, that `key` may end up owned by nothing. Returns the intent id. */
|
|
28
|
+
export declare function recordUploadIntent(database: DatabaseAdapter, collection: string, key: string): Promise<string>;
|
|
29
|
+
/**
|
|
30
|
+
* The batch operation that turns an upload intent into ownership: it joins the document's own create
|
|
31
|
+
* batch, and must apply — if reconciliation already claimed the intent (it is deleting the object), the
|
|
32
|
+
* document is not created either.
|
|
33
|
+
*/
|
|
34
|
+
export declare function claimUploadIntent(intentId: string): AtomicWriteOperation;
|
|
35
|
+
/**
|
|
36
|
+
* After a failed upload create. The object belongs to nothing **only if this call can claim the
|
|
37
|
+
* intent**: the document batch removes it on commit, so a claim that does not apply means the document
|
|
38
|
+
* committed (a later step failed) or a reconciler took over, and either way the object is left alone.
|
|
39
|
+
* Claim-first, like {@link reconcileStorage}: a batch whose outcome the driver could not report (a lost
|
|
40
|
+
* connection) and that commits afterwards finds its own claim gone and rolls back, so no document can
|
|
41
|
+
* point at the object deleted here. A failed object delete puts a `delete` intent back for reconciliation.
|
|
42
|
+
*/
|
|
43
|
+
export declare function settleFailedUpload(ctx: OperationContext, intentId: string, key: string): Promise<void>;
|
|
44
|
+
/** The `delete` intent a document delete writes in its own batch. */
|
|
45
|
+
export declare function deletionIntent(collection: string, key: string): AtomicWriteOperation;
|
|
46
|
+
/** After the delete committed: remove the object, then its intent; a failure leaves the intent. */
|
|
47
|
+
export declare function finishDeletion(ctx: OperationContext, intentId: string, key: string): Promise<void>;
|
|
48
|
+
export interface ReconcileStorageOptions {
|
|
49
|
+
/** `upload` intents younger than this are skipped — their upload may still be committing. Default 1 hour. */
|
|
50
|
+
uploadGraceMs?: number;
|
|
51
|
+
/** At most this many intents per call. Default 100. */
|
|
52
|
+
limit?: number;
|
|
53
|
+
/** "Now", for tests. */
|
|
54
|
+
now?: Date;
|
|
55
|
+
}
|
|
56
|
+
export interface ReconcileStorageReport {
|
|
57
|
+
/** Objects deleted (or already absent), whose intents are now gone. */
|
|
58
|
+
deleted: string[];
|
|
59
|
+
/** Intents dropped without touching storage, because their object turned out to be owned. */
|
|
60
|
+
kept: string[];
|
|
61
|
+
/** `upload` intents still inside their grace period. */
|
|
62
|
+
pending: number;
|
|
63
|
+
/** Object deletes that failed; their intents remain for the next run. Messages only, no secrets. */
|
|
64
|
+
failed: {
|
|
65
|
+
key: string;
|
|
66
|
+
error: string;
|
|
67
|
+
}[];
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Works off the storage intents left by crashed or failed uploads and deletes (spec 067): each remaining
|
|
71
|
+
* object that belongs to no document is deleted, and its intent removed. Safe to run at any time and
|
|
72
|
+
* repeatedly, from several processes at once:
|
|
73
|
+
*
|
|
74
|
+
* - An intent is first **claimed** — deleted by a conditional delete that only one caller can apply — so
|
|
75
|
+
* no two reconcilers, and no still-committing upload, can both act on it (the upload's own claim then
|
|
76
|
+
* fails and its document is not created).
|
|
77
|
+
* - An object is never deleted while a document owns it: an intent whose key a document of its
|
|
78
|
+
* collection records as `_storageKey` is dropped and the object kept.
|
|
79
|
+
* - A failed object delete puts the intent back for the next run. Only a process that dies between the
|
|
80
|
+
* claim and the object delete can leak that one object (a stored object with no intent); it can never
|
|
81
|
+
* leave a document pointing at a deleted object.
|
|
82
|
+
*
|
|
83
|
+
* Run it from a scheduled job (e.g. a Cloudflare Cron Trigger) or an operator script. It is not called
|
|
84
|
+
* automatically.
|
|
85
|
+
*/
|
|
86
|
+
export declare function reconcileStorage(ctx: OperationContext, options?: ReconcileStorageOptions): Promise<ReconcileStorageReport>;
|
|
87
|
+
/**
|
|
88
|
+
* The upload document that owns `key`: in the upload-enabled collection `collection` (default: the one
|
|
89
|
+
* the key is namespaced under, `<collection>/…`), the document whose Forge-recorded `_storageKey` is
|
|
90
|
+
* `key`. The single definition of file ownership (spec 067), used by `handleFile` and reconciliation.
|
|
91
|
+
*/
|
|
92
|
+
export declare function findStorageOwner(ctx: OperationContext, key: string, collection?: string | undefined): Promise<{
|
|
93
|
+
collection: string;
|
|
94
|
+
id: string;
|
|
95
|
+
} | null>;
|
|
96
|
+
//# sourceMappingURL=storage-intents.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage-intents.d.ts","sourceRoot":"","sources":["../src/storage-intents.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAE5D,OAAO,KAAK,EAAE,oBAAoB,EAAE,eAAe,EAAkB,MAAM,eAAe,CAAC;AAC3F,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,0BAA0B,2BAA2B,CAAC;AAEnE,kGAAkG;AAClG,eAAO,MAAM,uBAAuB,QAAiB,CAAC;AAEtD;;;GAGG;AACH,wBAAgB,wBAAwB,IAAI,oBAAoB,CAe/D;AAED,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,SAAS,oBAAoB,EAAE,GAAG,OAAO,CAE1F;AAED,2GAA2G;AAC3G,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,eAAe,EACzB,UAAU,EAAE,MAAM,EAClB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,MAAM,CAAC,CAOjB;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,oBAAoB,CAQxE;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,gBAAgB,EACrB,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,IAAI,CAAC,CAcf;AAED,qEAAqE;AACrE,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,oBAAoB,CAMpF;AAED,mGAAmG;AACnG,wBAAsB,cAAc,CAClC,GAAG,EAAE,gBAAgB,EACrB,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,IAAI,CAAC,CAOf;AAED,MAAM,WAAW,uBAAuB;IACtC,6GAA6G;IAC7G,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,uDAAuD;IACvD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wBAAwB;IACxB,GAAG,CAAC,EAAE,IAAI,CAAC;CACZ;AAED,MAAM,WAAW,sBAAsB;IACrC,uEAAuE;IACvE,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,6FAA6F;IAC7F,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,wDAAwD;IACxD,OAAO,EAAE,MAAM,CAAC;IAChB,oGAAoG;IACpG,MAAM,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC1C;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,gBAAgB,EACrB,OAAO,GAAE,uBAA4B,GACpC,OAAO,CAAC,sBAAsB,CAAC,CA0CjC;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,gBAAgB,EACrB,GAAG,EAAE,MAAM,EACX,UAAU,GAAE,MAAM,GAAG,SAA6B,GACjD,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CASpD"}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { defineField, getLogger } from '@forge-cms/core';
|
|
2
|
+
/**
|
|
3
|
+
* Durable storage-cleanup intents (spec 067). A database transaction cannot include object storage, so
|
|
4
|
+
* every step where an object can end up owned by no document leaves a row here **first**, in the
|
|
5
|
+
* database, and removes it only once the object is accounted for:
|
|
6
|
+
*
|
|
7
|
+
* - **Upload:** an `upload` intent is written *before* the object is stored, and removed in the **same
|
|
8
|
+
* batch** that creates the owning document. A process that dies between storing the object and
|
|
9
|
+
* committing the document leaves the intent behind.
|
|
10
|
+
* - **Delete:** a `delete` intent is written in the **same batch** that deletes the owning document, and
|
|
11
|
+
* removed once the object is deleted. A failed or interrupted object delete leaves it behind.
|
|
12
|
+
*
|
|
13
|
+
* A remaining intent means exactly "this object belongs to no document; delete it". {@link reconcileStorage}
|
|
14
|
+
* works them off. Nothing here is transactional with the bucket; the row is what survives a crash.
|
|
15
|
+
*/
|
|
16
|
+
export const STORAGE_INTENTS_COLLECTION = '_forge_storage_intents';
|
|
17
|
+
/** How long an `upload` intent is left alone: its upload may still be committing its document. */
|
|
18
|
+
export const DEFAULT_UPLOAD_GRACE_MS = 60 * 60 * 1000;
|
|
19
|
+
/**
|
|
20
|
+
* The internal collection, built without `defineCollection()` because its identifier validation
|
|
21
|
+
* reserves the `_forge_` prefix for Forge's own tables — the same pattern as `_forge_bootstrap`.
|
|
22
|
+
*/
|
|
23
|
+
export function storageIntentsDefinition() {
|
|
24
|
+
return {
|
|
25
|
+
slug: STORAGE_INTENTS_COLLECTION,
|
|
26
|
+
access: {
|
|
27
|
+
read: () => false,
|
|
28
|
+
create: () => false,
|
|
29
|
+
update: () => false,
|
|
30
|
+
delete: () => false
|
|
31
|
+
},
|
|
32
|
+
fields: {
|
|
33
|
+
key: defineField.text({ required: true, index: true }),
|
|
34
|
+
reason: defineField.text({ required: true }),
|
|
35
|
+
collection: defineField.text({ required: true })
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function hasUploadCollections(collections) {
|
|
40
|
+
return collections.some((collection) => collection.upload === true);
|
|
41
|
+
}
|
|
42
|
+
/** Records, before the object is stored, that `key` may end up owned by nothing. Returns the intent id. */
|
|
43
|
+
export async function recordUploadIntent(database, collection, key) {
|
|
44
|
+
const intent = await database.create(STORAGE_INTENTS_COLLECTION, {
|
|
45
|
+
key,
|
|
46
|
+
reason: 'upload',
|
|
47
|
+
collection
|
|
48
|
+
});
|
|
49
|
+
return intent.id;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The batch operation that turns an upload intent into ownership: it joins the document's own create
|
|
53
|
+
* batch, and must apply — if reconciliation already claimed the intent (it is deleting the object), the
|
|
54
|
+
* document is not created either.
|
|
55
|
+
*/
|
|
56
|
+
export function claimUploadIntent(intentId) {
|
|
57
|
+
return {
|
|
58
|
+
type: 'deleteIf',
|
|
59
|
+
collection: STORAGE_INTENTS_COLLECTION,
|
|
60
|
+
id: intentId,
|
|
61
|
+
condition: {},
|
|
62
|
+
requireApplied: true
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* After a failed upload create. The object belongs to nothing **only if this call can claim the
|
|
67
|
+
* intent**: the document batch removes it on commit, so a claim that does not apply means the document
|
|
68
|
+
* committed (a later step failed) or a reconciler took over, and either way the object is left alone.
|
|
69
|
+
* Claim-first, like {@link reconcileStorage}: a batch whose outcome the driver could not report (a lost
|
|
70
|
+
* connection) and that commits afterwards finds its own claim gone and rolls back, so no document can
|
|
71
|
+
* point at the object deleted here. A failed object delete puts a `delete` intent back for reconciliation.
|
|
72
|
+
*/
|
|
73
|
+
export async function settleFailedUpload(ctx, intentId, key) {
|
|
74
|
+
const database = ctx.adapters.database;
|
|
75
|
+
let claimed = false;
|
|
76
|
+
try {
|
|
77
|
+
claimed = (await database.deleteIf(STORAGE_INTENTS_COLLECTION, intentId, {})).applied;
|
|
78
|
+
if (!claimed)
|
|
79
|
+
return;
|
|
80
|
+
await ctx.adapters.storage.delete(key);
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
if (!claimed) {
|
|
84
|
+
logCleanupFailure(key, 'after a failed upload', err, 'kept');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
await restoreIntent(ctx, key, keyCollection(key), err, 'after a failed upload');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** The `delete` intent a document delete writes in its own batch. */
|
|
91
|
+
export function deletionIntent(collection, key) {
|
|
92
|
+
return {
|
|
93
|
+
type: 'create',
|
|
94
|
+
collection: STORAGE_INTENTS_COLLECTION,
|
|
95
|
+
data: { key, reason: 'delete', collection }
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/** After the delete committed: remove the object, then its intent; a failure leaves the intent. */
|
|
99
|
+
export async function finishDeletion(ctx, intentId, key) {
|
|
100
|
+
try {
|
|
101
|
+
await ctx.adapters.storage.delete(key);
|
|
102
|
+
await ctx.adapters.database.delete(STORAGE_INTENTS_COLLECTION, intentId);
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
logCleanupFailure(key, 'after document deletion', err, 'kept');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Works off the storage intents left by crashed or failed uploads and deletes (spec 067): each remaining
|
|
110
|
+
* object that belongs to no document is deleted, and its intent removed. Safe to run at any time and
|
|
111
|
+
* repeatedly, from several processes at once:
|
|
112
|
+
*
|
|
113
|
+
* - An intent is first **claimed** — deleted by a conditional delete that only one caller can apply — so
|
|
114
|
+
* no two reconcilers, and no still-committing upload, can both act on it (the upload's own claim then
|
|
115
|
+
* fails and its document is not created).
|
|
116
|
+
* - An object is never deleted while a document owns it: an intent whose key a document of its
|
|
117
|
+
* collection records as `_storageKey` is dropped and the object kept.
|
|
118
|
+
* - A failed object delete puts the intent back for the next run. Only a process that dies between the
|
|
119
|
+
* claim and the object delete can leak that one object (a stored object with no intent); it can never
|
|
120
|
+
* leave a document pointing at a deleted object.
|
|
121
|
+
*
|
|
122
|
+
* Run it from a scheduled job (e.g. a Cloudflare Cron Trigger) or an operator script. It is not called
|
|
123
|
+
* automatically.
|
|
124
|
+
*/
|
|
125
|
+
export async function reconcileStorage(ctx, options = {}) {
|
|
126
|
+
const database = ctx.adapters.database;
|
|
127
|
+
const now = (options.now ?? new Date()).getTime();
|
|
128
|
+
const grace = options.uploadGraceMs ?? DEFAULT_UPLOAD_GRACE_MS;
|
|
129
|
+
const report = { deleted: [], kept: [], pending: 0, failed: [] };
|
|
130
|
+
const intents = await database.findMany({
|
|
131
|
+
collection: STORAGE_INTENTS_COLLECTION,
|
|
132
|
+
limit: options.limit ?? 100,
|
|
133
|
+
sort: 'created_at',
|
|
134
|
+
order: 'asc'
|
|
135
|
+
});
|
|
136
|
+
for (const intent of intents) {
|
|
137
|
+
const key = intent.key;
|
|
138
|
+
const collection = intent.collection;
|
|
139
|
+
if (intent.reason === 'upload' && now - createdAt(intent) < grace) {
|
|
140
|
+
report.pending++;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
let claimed = false;
|
|
144
|
+
try {
|
|
145
|
+
// Ownership first: if a document records the key, the object stays. Checked before the claim, so
|
|
146
|
+
// a failing read leaves the intent in place; a commit racing in between consumes the intent and
|
|
147
|
+
// our claim below then does not apply.
|
|
148
|
+
const owned = (await findStorageOwner(ctx, key, collection)) !== null;
|
|
149
|
+
claimed = (await database.deleteIf(STORAGE_INTENTS_COLLECTION, intent.id, {}))
|
|
150
|
+
.applied;
|
|
151
|
+
if (!claimed)
|
|
152
|
+
continue; // another reconciler, or the upload's own commit, got it first
|
|
153
|
+
if (owned) {
|
|
154
|
+
report.kept.push(key);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
await ctx.adapters.storage.delete(key);
|
|
158
|
+
report.deleted.push(key);
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
report.failed.push({ key, error: messageOf(err) });
|
|
162
|
+
if (claimed)
|
|
163
|
+
await restoreIntent(ctx, key, collection, err, 'during reconciliation');
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return report;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* The upload document that owns `key`: in the upload-enabled collection `collection` (default: the one
|
|
170
|
+
* the key is namespaced under, `<collection>/…`), the document whose Forge-recorded `_storageKey` is
|
|
171
|
+
* `key`. The single definition of file ownership (spec 067), used by `handleFile` and reconciliation.
|
|
172
|
+
*/
|
|
173
|
+
export async function findStorageOwner(ctx, key, collection = key.split('/')[0]) {
|
|
174
|
+
const definition = collection ? ctx.getCollection(collection) : undefined;
|
|
175
|
+
if (!definition || definition.upload !== true)
|
|
176
|
+
return null;
|
|
177
|
+
const [owner] = await ctx.adapters.database.findMany({
|
|
178
|
+
collection: definition.slug,
|
|
179
|
+
where: { _storageKey: key },
|
|
180
|
+
limit: 1
|
|
181
|
+
});
|
|
182
|
+
return owner ? { collection: definition.slug, id: owner.id } : null;
|
|
183
|
+
}
|
|
184
|
+
function createdAt(intent) {
|
|
185
|
+
const value = typeof intent.created_at === 'string' ? Date.parse(intent.created_at) : NaN;
|
|
186
|
+
return Number.isNaN(value) ? 0 : value;
|
|
187
|
+
}
|
|
188
|
+
function messageOf(err) {
|
|
189
|
+
return err instanceof Error ? err.message : String(err);
|
|
190
|
+
}
|
|
191
|
+
/** Puts a `delete` intent back after a claimed object could not be deleted, so the next run retries. */
|
|
192
|
+
async function restoreIntent(ctx, key, collection, err, when) {
|
|
193
|
+
try {
|
|
194
|
+
await ctx.adapters.database.create(STORAGE_INTENTS_COLLECTION, {
|
|
195
|
+
key,
|
|
196
|
+
reason: 'delete',
|
|
197
|
+
collection
|
|
198
|
+
});
|
|
199
|
+
logCleanupFailure(key, when, err, 'kept');
|
|
200
|
+
}
|
|
201
|
+
catch (restoreErr) {
|
|
202
|
+
logCleanupFailure(key, when, err, 'lost');
|
|
203
|
+
getLogger().error(`Could not restore the storage intent for '${key}': ${messageOf(restoreErr)}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function keyCollection(key) {
|
|
207
|
+
return key.split('/')[0] ?? '';
|
|
208
|
+
}
|
|
209
|
+
/** The key and the error's message only — never the error object, which can carry request/credential details. */
|
|
210
|
+
function logCleanupFailure(key, when, err, intent) {
|
|
211
|
+
getLogger().error(`Failed to delete storage object '${key}' ${when}: ${messageOf(err)}. ` +
|
|
212
|
+
(intent === 'kept'
|
|
213
|
+
? `Its storage intent remains; run reconcileStorage() to retry (spec 067).`
|
|
214
|
+
: `Its storage intent could not be kept, so the object is now orphaned and must be removed by ` +
|
|
215
|
+
`hand (spec 067).`));
|
|
216
|
+
}
|
|
217
|
+
//# sourceMappingURL=storage-intents.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage-intents.js","sourceRoot":"","sources":["../src/storage-intents.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forge-owned document metadata (spec 063): never content, never written through the CMS mutation
|
|
3
|
+
* pipeline by a caller or a hook. `_status` is deliberately absent — on a `drafts: true` collection it
|
|
4
|
+
* is lifecycle input and stays writable. Raw `DatabaseAdapter` writes are outside this boundary.
|
|
5
|
+
*/
|
|
6
|
+
export declare const FORGE_OWNED_KEYS: readonly string[];
|
|
7
|
+
/**
|
|
8
|
+
* The caller-input screen for an update (or an update-mode preview / an existing global): echoes of
|
|
9
|
+
* `existing` are dropped, any other Forge-owned value is a `400`.
|
|
10
|
+
*/
|
|
11
|
+
export declare function screenUpdateInput(data: Record<string, unknown>, existing: Record<string, unknown>): Record<string, unknown>;
|
|
12
|
+
/**
|
|
13
|
+
* The caller-input screen for a create. A trusted caller (`overrideAccess` not `false`) may choose the
|
|
14
|
+
* new document's `id` — a non-empty string, returned separately so hooks never see or change it; every
|
|
15
|
+
* other Forge-owned key (and an untrusted caller's `id`) is a `400`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function screenCreateInput(data: Record<string, unknown>, trusted: boolean): {
|
|
18
|
+
content: Record<string, unknown>;
|
|
19
|
+
id: string | undefined;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Screens what a hook stage returned: hooks may change content and `_status`, never Forge-owned
|
|
23
|
+
* metadata. Echoes of `stored` (e.g. a hook returning `{ ...previousData, ...data }`) are dropped; any
|
|
24
|
+
* other value is a server-code bug, reported as a plain `Error` (500) rather than the caller's `400`.
|
|
25
|
+
*/
|
|
26
|
+
export declare function screenHookOutput(data: Record<string, unknown>, stored: Record<string, unknown>, stage: string, target: string): Record<string, unknown>;
|
|
27
|
+
//# sourceMappingURL=system-fields.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"system-fields.d.ts","sourceRoot":"","sources":["../src/system-fields.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,EAAE,SAAS,MAAM,EAK7C,CAAC;AAgCF;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAChC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAEzB;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,EAAE,OAAO,GACf;IAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,EAAE,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAY9D;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAUzB"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { InvalidInputError } from './errors.js';
|
|
2
|
+
/**
|
|
3
|
+
* Forge-owned document metadata (spec 063): never content, never written through the CMS mutation
|
|
4
|
+
* pipeline by a caller or a hook. `_status` is deliberately absent — on a `drafts: true` collection it
|
|
5
|
+
* is lifecycle input and stays writable. Raw `DatabaseAdapter` writes are outside this boundary.
|
|
6
|
+
*/
|
|
7
|
+
export const FORGE_OWNED_KEYS = [
|
|
8
|
+
'id',
|
|
9
|
+
'created_at',
|
|
10
|
+
'updated_at',
|
|
11
|
+
'_storageKey'
|
|
12
|
+
];
|
|
13
|
+
/** `null` and absent are the same stored value — echoing a missing key back as `null` is a no-op. */
|
|
14
|
+
function sameValue(a, b) {
|
|
15
|
+
return (a ?? null) === (b ?? null);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Returns `data` without the Forge-owned keys that merely echo `stored` (the document as it is — `{}`
|
|
19
|
+
* for a document that does not exist yet), calling `reject(key)` for any other value. An echo is a
|
|
20
|
+
* no-op, so a client round-tripping the document it read keeps working; a changed value is a write
|
|
21
|
+
* attempt and is refused rather than silently stripped.
|
|
22
|
+
*/
|
|
23
|
+
function screen(data, stored, reject) {
|
|
24
|
+
let screened = data;
|
|
25
|
+
for (const key of FORGE_OWNED_KEYS) {
|
|
26
|
+
if (!Object.hasOwn(data, key))
|
|
27
|
+
continue;
|
|
28
|
+
if (!sameValue(data[key], stored[key]))
|
|
29
|
+
throw reject(key);
|
|
30
|
+
if (screened === data)
|
|
31
|
+
screened = { ...data };
|
|
32
|
+
delete screened[key];
|
|
33
|
+
}
|
|
34
|
+
return screened;
|
|
35
|
+
}
|
|
36
|
+
function callerRejection(key) {
|
|
37
|
+
return new InvalidInputError(`Field '${key}' is managed by Forge and cannot be written`);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The caller-input screen for an update (or an update-mode preview / an existing global): echoes of
|
|
41
|
+
* `existing` are dropped, any other Forge-owned value is a `400`.
|
|
42
|
+
*/
|
|
43
|
+
export function screenUpdateInput(data, existing) {
|
|
44
|
+
return screen(data, existing, callerRejection);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The caller-input screen for a create. A trusted caller (`overrideAccess` not `false`) may choose the
|
|
48
|
+
* new document's `id` — a non-empty string, returned separately so hooks never see or change it; every
|
|
49
|
+
* other Forge-owned key (and an untrusted caller's `id`) is a `400`.
|
|
50
|
+
*/
|
|
51
|
+
export function screenCreateInput(data, trusted) {
|
|
52
|
+
const { id, ...rest } = data;
|
|
53
|
+
if (id !== undefined && id !== null) {
|
|
54
|
+
if (!trusted)
|
|
55
|
+
throw callerRejection('id');
|
|
56
|
+
if (typeof id !== 'string' || id === '') {
|
|
57
|
+
throw new InvalidInputError(`Field 'id' must be a non-empty string`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
content: screen(rest, {}, callerRejection),
|
|
62
|
+
id: typeof id === 'string' ? id : undefined
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Screens what a hook stage returned: hooks may change content and `_status`, never Forge-owned
|
|
67
|
+
* metadata. Echoes of `stored` (e.g. a hook returning `{ ...previousData, ...data }`) are dropped; any
|
|
68
|
+
* other value is a server-code bug, reported as a plain `Error` (500) rather than the caller's `400`.
|
|
69
|
+
*/
|
|
70
|
+
export function screenHookOutput(data, stored, stage, target) {
|
|
71
|
+
return screen(data, stored, (key) => new Error(`${stage} hook on '${target}' set the Forge-owned field '${key}'; hooks may only change ` +
|
|
72
|
+
`content fields and _status (spec 063)`));
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=system-fields.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"system-fields.js","sourceRoot":"","sources":["../src/system-fields.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEhD;;;;GAIG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAsB;IACjD,IAAI;IACJ,YAAY;IACZ,YAAY;IACZ,aAAa;CACd,CAAC;AAEF,qGAAqG;AACrG,SAAS,SAAS,CAAC,CAAU,EAAE,CAAU;IACvC,OAAO,CAAC,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC;AACrC,CAAC;AAED;;;;;GAKG;AACH,SAAS,MAAM,CACb,IAA6B,EAC7B,MAA+B,EAC/B,MAA8B;IAE9B,IAAI,QAAQ,GAAG,IAAI,CAAC;IACpB,KAAK,MAAM,GAAG,IAAI,gBAAgB,EAAE,CAAC;QACnC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC;YAAE,SAAS;QACxC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC;YAAE,MAAM,MAAM,CAAC,GAAG,CAAC,CAAC;QAC1D,IAAI,QAAQ,KAAK,IAAI;YAAE,QAAQ,GAAG,EAAE,GAAG,IAAI,EAAE,CAAC;QAC9C,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC;IACvB,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,SAAS,eAAe,CAAC,GAAW;IAClC,OAAO,IAAI,iBAAiB,CAAC,UAAU,GAAG,6CAA6C,CAAC,CAAC;AAC3F,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAC/B,IAA6B,EAC7B,QAAiC;IAEjC,OAAO,MAAM,CAAC,IAAI,EAAE,QAAQ,EAAE,eAAe,CAAC,CAAC;AACjD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAC/B,IAA6B,EAC7B,OAAgB;IAEhB,MAAM,EAAE,EAAE,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC;IAC7B,IAAI,EAAE,KAAK,SAAS,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC;QACpC,IAAI,CAAC,OAAO;YAAE,MAAM,eAAe,CAAC,IAAI,CAAC,CAAC;QAC1C,IAAI,OAAO,EAAE,KAAK,QAAQ,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;YACxC,MAAM,IAAI,iBAAiB,CAAC,uCAAuC,CAAC,CAAC;QACvE,CAAC;IACH,CAAC;IACD,OAAO;QACL,OAAO,EAAE,MAAM,CAAC,IAAI,EAAE,EAAE,EAAE,eAAe,CAAC;QAC1C,EAAE,EAAE,OAAO,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS;KAC5C,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAC9B,IAA6B,EAC7B,MAA+B,EAC/B,KAAa,EACb,MAAc;IAEd,OAAO,MAAM,CACX,IAAI,EACJ,MAAM,EACN,CAAC,GAAG,EAAE,EAAE,CACN,IAAI,KAAK,CACP,GAAG,KAAK,aAAa,MAAM,gCAAgC,GAAG,2BAA2B;QACvF,uCAAuC,CAC1C,CACJ,CAAC;AACJ,CAAC"}
|