@vercel/factory 0.0.16 → 0.0.18
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/CHANGELOG.md +52 -0
- package/dist/api-contracts.d.mts +665 -715
- package/dist/api-contracts.mjs +91 -4
- package/dist/api-contracts.mjs.map +1 -1
- package/dist/api-task-graph.mjs +14 -5
- package/dist/api-task-graph.mjs.map +1 -1
- package/dist/api.d.mts +58 -11
- package/dist/api.mjs +715 -399
- package/dist/api.mjs.map +1 -1
- package/dist/approval-access.d.mts +1 -1
- package/dist/approval-contracts.d.mts +3 -4
- package/dist/approval-contracts.mjs +1 -2
- package/dist/approval-contracts.mjs.map +1 -1
- package/dist/blob/index.d.mts +1 -1
- package/dist/budget.mjs +20 -3
- package/dist/budget.mjs.map +1 -1
- package/dist/change-verification/brief.d.mts +1 -1
- package/dist/change-verification/dispatch.mjs +8 -1
- package/dist/change-verification/dispatch.mjs.map +1 -1
- package/dist/changes/eve-record-change.mjs +4 -1
- package/dist/changes/eve-record-change.mjs.map +1 -1
- package/dist/client-stream.d.mts +19 -2
- package/dist/client-stream.mjs +115 -53
- package/dist/client-stream.mjs.map +1 -1
- package/dist/client-transcript.d.mts +25 -2
- package/dist/client-transcript.mjs +95 -13
- package/dist/client-transcript.mjs.map +1 -1
- package/dist/client.d.mts +145 -19
- package/dist/client.mjs +89 -16
- package/dist/client.mjs.map +1 -1
- package/dist/code-review/contracts.d.mts +1 -0
- package/dist/code-review/eve-post-review.d.mts +4 -2
- package/dist/code-review/eve-post-review.mjs +8 -0
- package/dist/code-review/eve-post-review.mjs.map +1 -1
- package/dist/code-review/github-reporter.d.mts +6 -2
- package/dist/code-review/github-reporter.mjs +86 -35
- package/dist/code-review/github-reporter.mjs.map +1 -1
- package/dist/code-review/preparation.d.mts +2 -1
- package/dist/code-review/preparation.mjs +2 -1
- package/dist/code-review/preparation.mjs.map +1 -1
- package/dist/code-review/stall.mjs +11 -2
- package/dist/code-review/stall.mjs.map +1 -1
- package/dist/connectors/github.d.mts +1 -1
- package/dist/connectors/index.d.mts +1 -1
- package/dist/connectors/slack.d.mts +1 -1
- package/dist/delivery-metrics/metrics.d.mts +1 -1
- package/dist/dispatch.mjs +3 -3
- package/dist/dispatch.mjs.map +1 -1
- package/dist/eve/index.d.mts +1 -1
- package/dist/eve/index.mjs +8 -1
- package/dist/eve/index.mjs.map +1 -1
- package/dist/eve/task-execution.d.mts +4 -4
- package/dist/eve/transcript.d.mts +3 -1
- package/dist/eve/transcript.mjs.map +1 -1
- package/dist/finding-remediation/admission.d.mts +1 -1
- package/dist/finding-remediation/brief.d.mts +1 -1
- package/dist/github-app.d.mts +1 -1
- package/dist/github-publication.d.mts +1 -1
- package/dist/github.d.mts +9 -1
- package/dist/github.mjs +29 -11
- package/dist/github.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/intake-contracts.d.mts +1 -0
- package/dist/intake-contracts.mjs +2 -1
- package/dist/intake-contracts.mjs.map +1 -1
- package/dist/integrations/github-issues.d.mts +53 -0
- package/dist/integrations/github-issues.mjs +170 -0
- package/dist/integrations/github-issues.mjs.map +1 -0
- package/dist/integrations/github.d.mts +2 -1
- package/dist/integrations/github.mjs +2 -1
- package/dist/multi-repository-changes/coordination.mjs +1 -1
- package/dist/planning/reconcile.mjs +15 -2
- package/dist/planning/reconcile.mjs.map +1 -1
- package/dist/postgres/index.d.mts +2 -1
- package/dist/postgres/index.mjs +11 -5
- package/dist/postgres/index.mjs.map +1 -1
- package/dist/postgres/indexed-codec.mjs +137 -0
- package/dist/postgres/indexed-codec.mjs.map +1 -0
- package/dist/postgres/indexed-schema.mjs +99 -0
- package/dist/postgres/indexed-schema.mjs.map +1 -0
- package/dist/postgres/indexed.d.mts +28 -0
- package/dist/postgres/indexed.mjs +277 -0
- package/dist/postgres/indexed.mjs.map +1 -0
- package/dist/postgres/record-predicates.mjs +23 -0
- package/dist/postgres/record-predicates.mjs.map +1 -0
- package/dist/presets/software-development/deepsec-brief.d.mts +1 -1
- package/dist/presets/software-development/dispatch.mjs +1 -1
- package/dist/presets/software-development/dispatch.mjs.map +1 -1
- package/dist/presets/software-development/recovery.mjs +9 -2
- package/dist/presets/software-development/recovery.mjs.map +1 -1
- package/dist/pull-requests/eve-fetch.d.mts +4 -1
- package/dist/pull-requests/eve-fetch.mjs +9 -0
- package/dist/pull-requests/eve-fetch.mjs.map +1 -1
- package/dist/pull-requests/github-publisher.d.mts +1 -1
- package/dist/pull-requests/github-publisher.mjs +19 -7
- package/dist/pull-requests/github-publisher.mjs.map +1 -1
- package/dist/repositories.mjs +20 -13
- package/dist/repositories.mjs.map +1 -1
- package/dist/sandbox/execution-run-ledger.d.mts +2 -2
- package/dist/sandbox/execution-run-ledger.mjs.map +1 -1
- package/dist/sandbox/execution.d.mts +2 -2
- package/dist/sandbox/execution.mjs.map +1 -1
- package/dist/schema/catalog.mjs +1 -1
- package/dist/schema/change.d.mts +1 -1
- package/dist/schema/communication.d.mts +2 -2
- package/dist/schema/factory-config.d.mts +1 -1
- package/dist/schema/outbox.d.mts +6 -6
- package/dist/schema/session-summary.d.mts +218 -0
- package/dist/schema/session-summary.mjs +68 -0
- package/dist/schema/session-summary.mjs.map +1 -0
- package/dist/schema/session.d.mts +39 -59
- package/dist/schema/session.mjs +6 -6
- package/dist/schema/session.mjs.map +1 -1
- package/dist/schema/task-graph.d.mts +24 -24
- package/dist/schema/transcript.d.mts +141 -3
- package/dist/schema/transcript.mjs +19 -3
- package/dist/schema/transcript.mjs.map +1 -1
- package/dist/schema/transitions.d.mts +1 -1
- package/dist/schema/transitions.mjs +1 -1
- package/dist/schema/transitions.mjs.map +1 -1
- package/dist/signals.d.mts +1 -1
- package/dist/stall.mjs +10 -2
- package/dist/stall.mjs.map +1 -1
- package/dist/storage.d.mts +10 -1
- package/dist/storage.mjs +7 -1
- package/dist/store/approvals.d.mts +14 -0
- package/dist/store/approvals.mjs +78 -0
- package/dist/store/approvals.mjs.map +1 -0
- package/dist/store/conversations.d.mts +59 -0
- package/dist/store/conversations.mjs +549 -0
- package/dist/store/conversations.mjs.map +1 -0
- package/dist/store/driver.d.mts +1 -1
- package/dist/store/driver.mjs.map +1 -1
- package/dist/store/engine.d.mts +38 -16
- package/dist/store/engine.mjs +229 -106
- package/dist/store/engine.mjs.map +1 -1
- package/dist/store/indexed-engine.d.mts +57 -0
- package/dist/store/indexed-engine.mjs +155 -0
- package/dist/store/indexed-engine.mjs.map +1 -0
- package/dist/store/indexed-memory.d.mts +7 -0
- package/dist/store/indexed-memory.mjs +200 -0
- package/dist/store/indexed-memory.mjs.map +1 -0
- package/dist/store/indexed-projections.d.mts +280 -0
- package/dist/store/indexed-projections.mjs +481 -0
- package/dist/store/indexed-projections.mjs.map +1 -0
- package/dist/store/indexed.d.mts +101 -0
- package/dist/store/indexed.mjs +19 -0
- package/dist/store/indexed.mjs.map +1 -0
- package/dist/store/iterate.d.mts +9 -0
- package/dist/store/iterate.mjs +23 -0
- package/dist/store/iterate.mjs.map +1 -0
- package/dist/store/query-scope.mjs +62 -0
- package/dist/store/query-scope.mjs.map +1 -0
- package/dist/store/query.d.mts +32 -0
- package/dist/store/query.mjs +103 -0
- package/dist/store/query.mjs.map +1 -0
- package/dist/store/session-history.mjs +24 -0
- package/dist/store/session-history.mjs.map +1 -0
- package/dist/store/slack-pr-notifications.d.mts +2 -2
- package/dist/store/slack-pr-notifications.mjs.map +1 -1
- package/dist/store/state.d.mts +26 -0
- package/dist/store/state.mjs +95 -0
- package/dist/store/state.mjs.map +1 -0
- package/dist/store/task-effects.d.mts +1 -1
- package/dist/store/task-graphs.d.mts +1 -1
- package/dist/store/task-messages.d.mts +1 -1
- package/dist/store/task-work.d.mts +1 -1
- package/dist/store/task-work.mjs +1 -1
- package/dist/store/transcripts.mjs +308 -0
- package/dist/store/transcripts.mjs.map +1 -0
- package/dist/sweep.d.mts +1 -1
- package/dist/sweep.mjs +12 -7
- package/dist/sweep.mjs.map +1 -1
- package/dist/task-communication.mjs +15 -1
- package/dist/task-communication.mjs.map +1 -1
- package/dist/task-protocols.d.mts +3 -3
- package/dist/tasks.d.mts +1 -1
- package/dist/vercel.d.mts +1 -1
- package/dist/work-triage/find-existing-work.d.mts +1 -1
- package/dist/work-triage/find-existing-work.mjs +12 -2
- package/dist/work-triage/find-existing-work.mjs.map +1 -1
- package/docs/index.md +4 -0
- package/docs/recipes/persistence-recovery.md +15 -9
- package/docs/recipes/postgres-indexed-engine.md +108 -0
- package/docs/recipes/postgres-indexed-storage.md +145 -0
- package/package.json +6 -1
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { InvalidStoreQueryError } from "../store/indexed.mjs";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
//#region src/postgres/indexed-codec.ts
|
|
5
|
+
const collections = {
|
|
6
|
+
approvalRecords: true,
|
|
7
|
+
applicationState: true,
|
|
8
|
+
applicationChunks: true,
|
|
9
|
+
conversations: true,
|
|
10
|
+
conversationQuestions: true,
|
|
11
|
+
conversationExecutions: true,
|
|
12
|
+
repositories: true,
|
|
13
|
+
signals: true,
|
|
14
|
+
tasks: true,
|
|
15
|
+
changes: true,
|
|
16
|
+
receipts: true,
|
|
17
|
+
budgets: true,
|
|
18
|
+
answers: true,
|
|
19
|
+
sessions: true,
|
|
20
|
+
externalSessions: true,
|
|
21
|
+
sessionContinuations: true,
|
|
22
|
+
sessionNotifications: true,
|
|
23
|
+
transcripts: true,
|
|
24
|
+
transcriptBlocks: true,
|
|
25
|
+
transcriptChunks: true,
|
|
26
|
+
images: true,
|
|
27
|
+
questionRoutes: true,
|
|
28
|
+
taskCatalogs: true,
|
|
29
|
+
changeCatalogs: true,
|
|
30
|
+
sessionCatalogs: true,
|
|
31
|
+
externalSessionCatalogs: true,
|
|
32
|
+
taskRoutes: true,
|
|
33
|
+
outboxQueues: true,
|
|
34
|
+
outboxDeliveries: true,
|
|
35
|
+
communicationSettings: true,
|
|
36
|
+
taskGraphs: true,
|
|
37
|
+
taskMessages: true,
|
|
38
|
+
taskEffects: true,
|
|
39
|
+
taskInboxes: true,
|
|
40
|
+
taskMessageProcessing: true,
|
|
41
|
+
taskMessageDecisions: true,
|
|
42
|
+
taskMessageDiscards: true,
|
|
43
|
+
plannerActivations: true,
|
|
44
|
+
plannerCancellations: true
|
|
45
|
+
};
|
|
46
|
+
const collectionNameSchema = z.custom((value) => typeof value === "string" && Object.hasOwn(collections, value), "Expected a Factory storage collection");
|
|
47
|
+
function textKey(maxBytes) {
|
|
48
|
+
return z.string().min(1).max(maxBytes).refine((value) => !/[\0\uD800-\uDFFF]/u.test(value) && Buffer.byteLength(value) <= maxBytes, "Storage key exceeds its UTF-8 limit or contains invalid PostgreSQL text");
|
|
49
|
+
}
|
|
50
|
+
const namespaceSchema = textKey(128);
|
|
51
|
+
const indexKeySchema = textKey(400);
|
|
52
|
+
const recordIdSchema = textKey(200);
|
|
53
|
+
const sortKeySchema = textKey(128);
|
|
54
|
+
const versionSchema = z.number().int().min(1).max(Number.MAX_SAFE_INTEGER);
|
|
55
|
+
function digest(value) {
|
|
56
|
+
return createHash("sha256").update(value).digest("hex");
|
|
57
|
+
}
|
|
58
|
+
function canonicalJson(value, maxBytes) {
|
|
59
|
+
const ancestors = /* @__PURE__ */ new Set();
|
|
60
|
+
let nodes = 0;
|
|
61
|
+
let bytes = 0;
|
|
62
|
+
function addBytes(count) {
|
|
63
|
+
bytes += count;
|
|
64
|
+
if (bytes > maxBytes) throw new Error(`Storage JSON exceeds ${maxBytes} bytes`);
|
|
65
|
+
}
|
|
66
|
+
function stringBytes(value) {
|
|
67
|
+
if (value.length > maxBytes) throw new Error(`Storage JSON exceeds ${maxBytes} bytes`);
|
|
68
|
+
return Buffer.byteLength(JSON.stringify(value));
|
|
69
|
+
}
|
|
70
|
+
function visit(entry, depth) {
|
|
71
|
+
if (++nodes > maxBytes || depth > 32) throw new Error("JSON exceeds storage bounds");
|
|
72
|
+
if (entry === null || typeof entry === "boolean" || typeof entry === "number" && Number.isFinite(entry)) {
|
|
73
|
+
addBytes(JSON.stringify(entry).length);
|
|
74
|
+
return entry;
|
|
75
|
+
}
|
|
76
|
+
if (typeof entry === "string" && !/[\0\uD800-\uDFFF]/u.test(entry)) {
|
|
77
|
+
addBytes(stringBytes(entry));
|
|
78
|
+
return entry;
|
|
79
|
+
}
|
|
80
|
+
if (typeof entry !== "object" || entry === null || ancestors.has(entry)) throw new Error("Storage values must be finite, acyclic JSON");
|
|
81
|
+
const array = Array.isArray(entry);
|
|
82
|
+
const prototype = Object.getPrototypeOf(entry);
|
|
83
|
+
if (array ? prototype !== Array.prototype : prototype !== Object.prototype && prototype !== null) throw new Error("Storage values must contain only JSON objects and arrays");
|
|
84
|
+
ancestors.add(entry);
|
|
85
|
+
addBytes(2);
|
|
86
|
+
const keys = Reflect.ownKeys(entry);
|
|
87
|
+
if (keys.length > maxBytes) throw new Error("JSON exceeds storage bounds");
|
|
88
|
+
if (array && (entry.length > maxBytes || keys.length !== entry.length + 1)) throw new Error("Storage JSON cannot contain sparse or augmented arrays");
|
|
89
|
+
const entries = [];
|
|
90
|
+
for (const key of keys) {
|
|
91
|
+
if (array && key === "length") continue;
|
|
92
|
+
if (typeof key !== "string" || /[\0\uD800-\uDFFF]/u.test(key)) throw new Error("Storage JSON properties must be valid PostgreSQL text");
|
|
93
|
+
const descriptor = Object.getOwnPropertyDescriptor(entry, key);
|
|
94
|
+
if (!descriptor.enumerable || !("value" in descriptor)) throw new Error("Storage JSON cannot contain accessors or non-enumerable properties");
|
|
95
|
+
if (entries.length > 0) addBytes(1);
|
|
96
|
+
if (!array) addBytes(stringBytes(key) + 1);
|
|
97
|
+
entries.push([key, visit(descriptor.value, depth + 1)]);
|
|
98
|
+
}
|
|
99
|
+
let result;
|
|
100
|
+
if (array) {
|
|
101
|
+
for (let index = 0; index < entry.length; index++) if (!Object.hasOwn(entry, index)) throw new Error("Storage JSON cannot contain array holes");
|
|
102
|
+
result = entries.map(([, item]) => item);
|
|
103
|
+
} else {
|
|
104
|
+
entries.sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
|
|
105
|
+
result = Object.fromEntries(entries);
|
|
106
|
+
}
|
|
107
|
+
ancestors.delete(entry);
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
return JSON.stringify(visit(value, 0));
|
|
111
|
+
}
|
|
112
|
+
const cursorSchema = z.strictObject({
|
|
113
|
+
version: z.literal(1),
|
|
114
|
+
namespace: namespaceSchema,
|
|
115
|
+
index: indexKeySchema,
|
|
116
|
+
order: z.enum(["asc", "desc"]),
|
|
117
|
+
sortKey: sortKeySchema,
|
|
118
|
+
collection: collectionNameSchema,
|
|
119
|
+
id: recordIdSchema
|
|
120
|
+
});
|
|
121
|
+
function encodeCursor(value) {
|
|
122
|
+
return Buffer.from(JSON.stringify(value)).toString("base64url");
|
|
123
|
+
}
|
|
124
|
+
function decodeCursor(cursor, namespace, index, order) {
|
|
125
|
+
try {
|
|
126
|
+
if (cursor.length > 8192 || !/^[A-Za-z0-9_-]+$/u.test(cursor)) throw new InvalidStoreQueryError();
|
|
127
|
+
const decoded = cursorSchema.parse(JSON.parse(Buffer.from(cursor, "base64url").toString("utf8")));
|
|
128
|
+
if (decoded.namespace !== namespace || decoded.index !== index || decoded.order !== order) throw new InvalidStoreQueryError();
|
|
129
|
+
return decoded;
|
|
130
|
+
} catch {
|
|
131
|
+
throw new InvalidStoreQueryError();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
//#endregion
|
|
135
|
+
export { canonicalJson, collectionNameSchema, decodeCursor, digest, encodeCursor, indexKeySchema, namespaceSchema, recordIdSchema, sortKeySchema, versionSchema };
|
|
136
|
+
|
|
137
|
+
//# sourceMappingURL=indexed-codec.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"indexed-codec.mjs","names":[],"sources":["../../src/postgres/indexed-codec.ts"],"sourcesContent":["import { InvalidStoreQueryError } from \"../store/indexed\";\nimport { createHash } from \"node:crypto\";\nimport { z } from \"zod\";\nimport type { CollectionName } from \"../store/driver\";\n\nconst collections = {\n approvalRecords: true,\n applicationState: true,\n applicationChunks: true,\n conversations: true,\n conversationQuestions: true,\n conversationExecutions: true,\n repositories: true,\n signals: true,\n tasks: true,\n changes: true,\n receipts: true,\n budgets: true,\n answers: true,\n sessions: true,\n externalSessions: true,\n sessionContinuations: true,\n sessionNotifications: true,\n transcripts: true,\n transcriptBlocks: true,\n transcriptChunks: true,\n images: true,\n questionRoutes: true,\n taskCatalogs: true,\n changeCatalogs: true,\n sessionCatalogs: true,\n externalSessionCatalogs: true,\n taskRoutes: true,\n outboxQueues: true,\n outboxDeliveries: true,\n communicationSettings: true,\n taskGraphs: true,\n taskMessages: true,\n taskEffects: true,\n taskInboxes: true,\n taskMessageProcessing: true,\n taskMessageDecisions: true,\n taskMessageDiscards: true,\n plannerActivations: true,\n plannerCancellations: true,\n} satisfies Record<CollectionName, true>;\n\nexport const collectionNameSchema = z.custom<CollectionName>(\n (value) => typeof value === \"string\" && Object.hasOwn(collections, value),\n \"Expected a Factory storage collection\",\n);\n\nfunction textKey(maxBytes: number) {\n return z\n .string()\n .min(1)\n .max(maxBytes)\n .refine(\n (value) => !/[\\0\\uD800-\\uDFFF]/u.test(value) && Buffer.byteLength(value) <= maxBytes,\n \"Storage key exceeds its UTF-8 limit or contains invalid PostgreSQL text\",\n );\n}\n\nexport const namespaceSchema = textKey(128);\nexport const indexKeySchema = textKey(400);\nexport const recordIdSchema = textKey(200);\nexport const sortKeySchema = textKey(128);\nexport const versionSchema = z.number().int().min(1).max(Number.MAX_SAFE_INTEGER);\n\nexport function digest(value: string): string {\n return createHash(\"sha256\").update(value).digest(\"hex\");\n}\n\nexport function canonicalJson(value: unknown, maxBytes: number): string {\n const ancestors = new Set<object>();\n let nodes = 0;\n let bytes = 0;\n function addBytes(count: number) {\n bytes += count;\n if (bytes > maxBytes) throw new Error(`Storage JSON exceeds ${maxBytes} bytes`);\n }\n function stringBytes(value: string) {\n if (value.length > maxBytes) throw new Error(`Storage JSON exceeds ${maxBytes} bytes`);\n return Buffer.byteLength(JSON.stringify(value));\n }\n function visit(entry: unknown, depth: number): unknown {\n if (++nodes > maxBytes || depth > 32) throw new Error(\"JSON exceeds storage bounds\");\n if (\n entry === null ||\n typeof entry === \"boolean\" ||\n (typeof entry === \"number\" && Number.isFinite(entry))\n ) {\n addBytes(JSON.stringify(entry).length);\n return entry;\n }\n if (typeof entry === \"string\" && !/[\\0\\uD800-\\uDFFF]/u.test(entry)) {\n addBytes(stringBytes(entry));\n return entry;\n }\n if (typeof entry !== \"object\" || entry === null || ancestors.has(entry))\n throw new Error(\"Storage values must be finite, acyclic JSON\");\n const array = Array.isArray(entry);\n const prototype: unknown = Object.getPrototypeOf(entry);\n if (\n array ? prototype !== Array.prototype : prototype !== Object.prototype && prototype !== null\n )\n throw new Error(\"Storage values must contain only JSON objects and arrays\");\n ancestors.add(entry);\n addBytes(2);\n const keys = Reflect.ownKeys(entry);\n if (keys.length > maxBytes) throw new Error(\"JSON exceeds storage bounds\");\n if (array && (entry.length > maxBytes || keys.length !== entry.length + 1))\n throw new Error(\"Storage JSON cannot contain sparse or augmented arrays\");\n const entries: [string, unknown][] = [];\n for (const key of keys) {\n if (array && key === \"length\") continue;\n if (typeof key !== \"string\" || /[\\0\\uD800-\\uDFFF]/u.test(key))\n throw new Error(\"Storage JSON properties must be valid PostgreSQL text\");\n const descriptor = Object.getOwnPropertyDescriptor(entry, key)!;\n if (!descriptor.enumerable || !(\"value\" in descriptor))\n throw new Error(\"Storage JSON cannot contain accessors or non-enumerable properties\");\n if (entries.length > 0) addBytes(1);\n if (!array) addBytes(stringBytes(key) + 1);\n entries.push([key, visit(descriptor.value, depth + 1)]);\n }\n let result: unknown;\n if (array) {\n for (let index = 0; index < entry.length; index++)\n if (!Object.hasOwn(entry, index))\n throw new Error(\"Storage JSON cannot contain array holes\");\n result = entries.map(([, item]) => item);\n } else {\n entries.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0));\n result = Object.fromEntries(entries);\n }\n ancestors.delete(entry);\n return result;\n }\n return JSON.stringify(visit(value, 0));\n}\n\nconst cursorSchema = z.strictObject({\n version: z.literal(1),\n namespace: namespaceSchema,\n index: indexKeySchema,\n order: z.enum([\"asc\", \"desc\"]),\n sortKey: sortKeySchema,\n collection: collectionNameSchema,\n id: recordIdSchema,\n});\n\nexport function encodeCursor(value: z.infer<typeof cursorSchema>): string {\n return Buffer.from(JSON.stringify(value)).toString(\"base64url\");\n}\n\nexport function decodeCursor(cursor: string, namespace: string, index: string, order: string) {\n try {\n if (cursor.length > 8192 || !/^[A-Za-z0-9_-]+$/u.test(cursor))\n throw new InvalidStoreQueryError();\n const decoded = cursorSchema.parse(\n JSON.parse(Buffer.from(cursor, \"base64url\").toString(\"utf8\")),\n );\n if (decoded.namespace !== namespace || decoded.index !== index || decoded.order !== order)\n throw new InvalidStoreQueryError();\n return decoded;\n } catch {\n throw new InvalidStoreQueryError();\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { bigint, jsonb, pgTable, primaryKey, text, uniqueIndex } from "drizzle-orm/pg-core";
|
|
2
|
+
//#region src/postgres/indexed-schema.ts
|
|
3
|
+
const indexedRecords = pgTable("factory_indexed_records", {
|
|
4
|
+
namespace: text("namespace").notNull(),
|
|
5
|
+
collection: text("collection").$type().notNull(),
|
|
6
|
+
id: text("id").notNull(),
|
|
7
|
+
value: jsonb("value").$type().notNull(),
|
|
8
|
+
version: bigint("version", { mode: "number" }).notNull()
|
|
9
|
+
}, (table) => [primaryKey({ columns: [
|
|
10
|
+
table.namespace,
|
|
11
|
+
table.collection,
|
|
12
|
+
table.id
|
|
13
|
+
] })]);
|
|
14
|
+
const indexedEntries = pgTable("factory_indexed_entries", {
|
|
15
|
+
namespace: text("namespace").notNull(),
|
|
16
|
+
index: text("index_key").notNull(),
|
|
17
|
+
sortKey: text("sort_key").notNull(),
|
|
18
|
+
collection: text("collection").$type().notNull(),
|
|
19
|
+
id: text("id").notNull(),
|
|
20
|
+
version: bigint("version", { mode: "number" }).notNull(),
|
|
21
|
+
summary: jsonb("summary").$type().notNull()
|
|
22
|
+
}, (table) => [primaryKey({ columns: [
|
|
23
|
+
table.namespace,
|
|
24
|
+
table.index,
|
|
25
|
+
table.sortKey,
|
|
26
|
+
table.collection,
|
|
27
|
+
table.id
|
|
28
|
+
] }), uniqueIndex("factory_indexed_entries_record").on(table.namespace, table.collection, table.id, table.index)]);
|
|
29
|
+
const indexedCounts = pgTable("factory_indexed_counts", {
|
|
30
|
+
namespace: text("namespace").notNull(),
|
|
31
|
+
index: text("index_key").notNull(),
|
|
32
|
+
count: bigint("count", { mode: "number" }).notNull(),
|
|
33
|
+
group: text("group_key"),
|
|
34
|
+
attributes: jsonb("attributes").$type()
|
|
35
|
+
}, (table) => [primaryKey({ columns: [table.namespace, table.index] })]);
|
|
36
|
+
const indexedOperations = pgTable("factory_indexed_operations", {
|
|
37
|
+
namespace: text("namespace").notNull(),
|
|
38
|
+
operationId: text("operation_id").notNull(),
|
|
39
|
+
fingerprint: text("fingerprint").notNull(),
|
|
40
|
+
version: bigint("version", { mode: "number" }).notNull()
|
|
41
|
+
}, (table) => [primaryKey({ columns: [table.namespace, table.operationId] })]);
|
|
42
|
+
const indexedMigration = {
|
|
43
|
+
tag: "0001_indexed_storage",
|
|
44
|
+
statements: [
|
|
45
|
+
`CREATE TABLE IF NOT EXISTS "factory_indexed_records" (
|
|
46
|
+
"namespace" text COLLATE "C" NOT NULL,
|
|
47
|
+
"collection" text COLLATE "C" NOT NULL,
|
|
48
|
+
"id" text COLLATE "C" NOT NULL,
|
|
49
|
+
"value" jsonb NOT NULL,
|
|
50
|
+
"version" bigint NOT NULL CHECK ("version" BETWEEN 1 AND 9007199254740991),
|
|
51
|
+
PRIMARY KEY ("namespace", "collection", "id")
|
|
52
|
+
)`,
|
|
53
|
+
`CREATE TABLE IF NOT EXISTS "factory_indexed_entries" (
|
|
54
|
+
"namespace" text COLLATE "C" NOT NULL,
|
|
55
|
+
"index_key" text COLLATE "C" NOT NULL,
|
|
56
|
+
"sort_key" text COLLATE "C" NOT NULL,
|
|
57
|
+
"collection" text COLLATE "C" NOT NULL,
|
|
58
|
+
"id" text COLLATE "C" NOT NULL,
|
|
59
|
+
"version" bigint NOT NULL CHECK ("version" BETWEEN 1 AND 9007199254740991),
|
|
60
|
+
"summary" jsonb NOT NULL,
|
|
61
|
+
PRIMARY KEY ("namespace", "index_key", "sort_key", "collection", "id"),
|
|
62
|
+
FOREIGN KEY ("namespace", "collection", "id")
|
|
63
|
+
REFERENCES "factory_indexed_records" ("namespace", "collection", "id")
|
|
64
|
+
)`,
|
|
65
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS "factory_indexed_entries_record"
|
|
66
|
+
ON "factory_indexed_entries" ("namespace", "collection", "id", "index_key")`,
|
|
67
|
+
`CREATE TABLE IF NOT EXISTS "factory_indexed_counts" (
|
|
68
|
+
"namespace" text COLLATE "C" NOT NULL,
|
|
69
|
+
"index_key" text COLLATE "C" NOT NULL,
|
|
70
|
+
"count" bigint NOT NULL CHECK ("count" BETWEEN 0 AND 9007199254740991),
|
|
71
|
+
PRIMARY KEY ("namespace", "index_key")
|
|
72
|
+
)`,
|
|
73
|
+
`CREATE TABLE IF NOT EXISTS "factory_indexed_operations" (
|
|
74
|
+
"namespace" text COLLATE "C" NOT NULL,
|
|
75
|
+
"operation_id" text COLLATE "C" NOT NULL,
|
|
76
|
+
"fingerprint" text NOT NULL,
|
|
77
|
+
"version" bigint NOT NULL CHECK ("version" BETWEEN 1 AND 9007199254740991),
|
|
78
|
+
PRIMARY KEY ("namespace", "operation_id")
|
|
79
|
+
)`
|
|
80
|
+
]
|
|
81
|
+
};
|
|
82
|
+
const indexedReadsMigration = {
|
|
83
|
+
tag: "0002_indexed_reads",
|
|
84
|
+
statements: [
|
|
85
|
+
`ALTER TABLE "factory_indexed_counts" ADD COLUMN IF NOT EXISTS "group_key" text COLLATE "C"`,
|
|
86
|
+
`ALTER TABLE "factory_indexed_counts" ADD COLUMN IF NOT EXISTS "attributes" jsonb`,
|
|
87
|
+
`CREATE INDEX IF NOT EXISTS "factory_indexed_counts_group" ON "factory_indexed_counts" ("namespace", "group_key")`,
|
|
88
|
+
`CREATE INDEX IF NOT EXISTS "factory_indexed_counts_attributes" ON "factory_indexed_counts" USING gin ("attributes" jsonb_path_ops)`,
|
|
89
|
+
`CREATE INDEX IF NOT EXISTS "factory_conversation_search"
|
|
90
|
+
ON "factory_indexed_records" USING gin (to_tsvector('simple', coalesce("value"->>'searchText', '')))
|
|
91
|
+
WHERE "collection" = 'conversations'`,
|
|
92
|
+
`CREATE INDEX IF NOT EXISTS "factory_indexed_records_value"
|
|
93
|
+
ON "factory_indexed_records" USING gin ("value" jsonb_path_ops)`
|
|
94
|
+
]
|
|
95
|
+
};
|
|
96
|
+
//#endregion
|
|
97
|
+
export { indexedCounts, indexedEntries, indexedMigration, indexedOperations, indexedReadsMigration, indexedRecords };
|
|
98
|
+
|
|
99
|
+
//# sourceMappingURL=indexed-schema.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"indexed-schema.mjs","names":[],"sources":["../../src/postgres/indexed-schema.ts"],"sourcesContent":["import { bigint, jsonb, pgTable, primaryKey, text, uniqueIndex } from \"drizzle-orm/pg-core\";\nimport type { CollectionName } from \"../store/driver\";\nimport type { FactoryMigration } from \"./index\";\n\nexport const indexedRecords = pgTable(\n \"factory_indexed_records\",\n {\n namespace: text(\"namespace\").notNull(),\n collection: text(\"collection\").$type<CollectionName>().notNull(),\n id: text(\"id\").notNull(),\n value: jsonb(\"value\").$type<unknown>().notNull(),\n version: bigint(\"version\", { mode: \"number\" }).notNull(),\n },\n (table) => [primaryKey({ columns: [table.namespace, table.collection, table.id] })],\n);\n\nexport const indexedEntries = pgTable(\n \"factory_indexed_entries\",\n {\n namespace: text(\"namespace\").notNull(),\n index: text(\"index_key\").notNull(),\n sortKey: text(\"sort_key\").notNull(),\n collection: text(\"collection\").$type<CollectionName>().notNull(),\n id: text(\"id\").notNull(),\n version: bigint(\"version\", { mode: \"number\" }).notNull(),\n summary: jsonb(\"summary\").$type<unknown>().notNull(),\n },\n (table) => [\n primaryKey({\n columns: [table.namespace, table.index, table.sortKey, table.collection, table.id],\n }),\n uniqueIndex(\"factory_indexed_entries_record\").on(\n table.namespace,\n table.collection,\n table.id,\n table.index,\n ),\n ],\n);\n\nexport const indexedCounts = pgTable(\n \"factory_indexed_counts\",\n {\n namespace: text(\"namespace\").notNull(),\n index: text(\"index_key\").notNull(),\n count: bigint(\"count\", { mode: \"number\" }).notNull(),\n group: text(\"group_key\"),\n attributes: jsonb(\"attributes\").$type<unknown>(),\n },\n (table) => [primaryKey({ columns: [table.namespace, table.index] })],\n);\n\nexport const indexedOperations = pgTable(\n \"factory_indexed_operations\",\n {\n namespace: text(\"namespace\").notNull(),\n operationId: text(\"operation_id\").notNull(),\n fingerprint: text(\"fingerprint\").notNull(),\n version: bigint(\"version\", { mode: \"number\" }).notNull(),\n },\n (table) => [primaryKey({ columns: [table.namespace, table.operationId] })],\n);\n\nexport const indexedMigration: FactoryMigration = {\n tag: \"0001_indexed_storage\",\n statements: [\n `CREATE TABLE IF NOT EXISTS \"factory_indexed_records\" (\n \"namespace\" text COLLATE \"C\" NOT NULL,\n \"collection\" text COLLATE \"C\" NOT NULL,\n \"id\" text COLLATE \"C\" NOT NULL,\n \"value\" jsonb NOT NULL,\n \"version\" bigint NOT NULL CHECK (\"version\" BETWEEN 1 AND 9007199254740991),\n PRIMARY KEY (\"namespace\", \"collection\", \"id\")\n )`,\n `CREATE TABLE IF NOT EXISTS \"factory_indexed_entries\" (\n \"namespace\" text COLLATE \"C\" NOT NULL,\n \"index_key\" text COLLATE \"C\" NOT NULL,\n \"sort_key\" text COLLATE \"C\" NOT NULL,\n \"collection\" text COLLATE \"C\" NOT NULL,\n \"id\" text COLLATE \"C\" NOT NULL,\n \"version\" bigint NOT NULL CHECK (\"version\" BETWEEN 1 AND 9007199254740991),\n \"summary\" jsonb NOT NULL,\n PRIMARY KEY (\"namespace\", \"index_key\", \"sort_key\", \"collection\", \"id\"),\n FOREIGN KEY (\"namespace\", \"collection\", \"id\")\n REFERENCES \"factory_indexed_records\" (\"namespace\", \"collection\", \"id\")\n )`,\n `CREATE UNIQUE INDEX IF NOT EXISTS \"factory_indexed_entries_record\"\n ON \"factory_indexed_entries\" (\"namespace\", \"collection\", \"id\", \"index_key\")`,\n `CREATE TABLE IF NOT EXISTS \"factory_indexed_counts\" (\n \"namespace\" text COLLATE \"C\" NOT NULL,\n \"index_key\" text COLLATE \"C\" NOT NULL,\n \"count\" bigint NOT NULL CHECK (\"count\" BETWEEN 0 AND 9007199254740991),\n PRIMARY KEY (\"namespace\", \"index_key\")\n )`,\n `CREATE TABLE IF NOT EXISTS \"factory_indexed_operations\" (\n \"namespace\" text COLLATE \"C\" NOT NULL,\n \"operation_id\" text COLLATE \"C\" NOT NULL,\n \"fingerprint\" text NOT NULL,\n \"version\" bigint NOT NULL CHECK (\"version\" BETWEEN 1 AND 9007199254740991),\n PRIMARY KEY (\"namespace\", \"operation_id\")\n )`,\n ],\n};\n\nexport const indexedReadsMigration: FactoryMigration = {\n tag: \"0002_indexed_reads\",\n statements: [\n `ALTER TABLE \"factory_indexed_counts\" ADD COLUMN IF NOT EXISTS \"group_key\" text COLLATE \"C\"`,\n `ALTER TABLE \"factory_indexed_counts\" ADD COLUMN IF NOT EXISTS \"attributes\" jsonb`,\n `CREATE INDEX IF NOT EXISTS \"factory_indexed_counts_group\" ON \"factory_indexed_counts\" (\"namespace\", \"group_key\")`,\n `CREATE INDEX IF NOT EXISTS \"factory_indexed_counts_attributes\" ON \"factory_indexed_counts\" USING gin (\"attributes\" jsonb_path_ops)`,\n `CREATE INDEX IF NOT EXISTS \"factory_conversation_search\"\n ON \"factory_indexed_records\" USING gin (to_tsvector('simple', coalesce(\"value\"->>'searchText', '')))\n WHERE \"collection\" = 'conversations'`,\n `CREATE INDEX IF NOT EXISTS \"factory_indexed_records_value\"\n ON \"factory_indexed_records\" USING gin (\"value\" jsonb_path_ops)`,\n ],\n};\n"],"mappings":";;AAIA,MAAa,iBAAiB,QAC5B,2BACA;CACE,WAAW,KAAK,WAAW,CAAC,CAAC,QAAQ;CACrC,YAAY,KAAK,YAAY,CAAC,CAAC,MAAsB,CAAC,CAAC,QAAQ;CAC/D,IAAI,KAAK,IAAI,CAAC,CAAC,QAAQ;CACvB,OAAO,MAAM,OAAO,CAAC,CAAC,MAAe,CAAC,CAAC,QAAQ;CAC/C,SAAS,OAAO,WAAW,EAAE,MAAM,SAAS,CAAC,CAAC,CAAC,QAAQ;AACzD,IACC,UAAU,CAAC,WAAW,EAAE,SAAS;CAAC,MAAM;CAAW,MAAM;CAAY,MAAM;AAAE,EAAE,CAAC,CAAC,CACpF;AAEA,MAAa,iBAAiB,QAC5B,2BACA;CACE,WAAW,KAAK,WAAW,CAAC,CAAC,QAAQ;CACrC,OAAO,KAAK,WAAW,CAAC,CAAC,QAAQ;CACjC,SAAS,KAAK,UAAU,CAAC,CAAC,QAAQ;CAClC,YAAY,KAAK,YAAY,CAAC,CAAC,MAAsB,CAAC,CAAC,QAAQ;CAC/D,IAAI,KAAK,IAAI,CAAC,CAAC,QAAQ;CACvB,SAAS,OAAO,WAAW,EAAE,MAAM,SAAS,CAAC,CAAC,CAAC,QAAQ;CACvD,SAAS,MAAM,SAAS,CAAC,CAAC,MAAe,CAAC,CAAC,QAAQ;AACrD,IACC,UAAU,CACT,WAAW,EACT,SAAS;CAAC,MAAM;CAAW,MAAM;CAAO,MAAM;CAAS,MAAM;CAAY,MAAM;AAAE,EACnF,CAAC,GACD,YAAY,gCAAgC,CAAC,CAAC,GAC5C,MAAM,WACN,MAAM,YACN,MAAM,IACN,MAAM,KACR,CACF,CACF;AAEA,MAAa,gBAAgB,QAC3B,0BACA;CACE,WAAW,KAAK,WAAW,CAAC,CAAC,QAAQ;CACrC,OAAO,KAAK,WAAW,CAAC,CAAC,QAAQ;CACjC,OAAO,OAAO,SAAS,EAAE,MAAM,SAAS,CAAC,CAAC,CAAC,QAAQ;CACnD,OAAO,KAAK,WAAW;CACvB,YAAY,MAAM,YAAY,CAAC,CAAC,MAAe;AACjD,IACC,UAAU,CAAC,WAAW,EAAE,SAAS,CAAC,MAAM,WAAW,MAAM,KAAK,EAAE,CAAC,CAAC,CACrE;AAEA,MAAa,oBAAoB,QAC/B,8BACA;CACE,WAAW,KAAK,WAAW,CAAC,CAAC,QAAQ;CACrC,aAAa,KAAK,cAAc,CAAC,CAAC,QAAQ;CAC1C,aAAa,KAAK,aAAa,CAAC,CAAC,QAAQ;CACzC,SAAS,OAAO,WAAW,EAAE,MAAM,SAAS,CAAC,CAAC,CAAC,QAAQ;AACzD,IACC,UAAU,CAAC,WAAW,EAAE,SAAS,CAAC,MAAM,WAAW,MAAM,WAAW,EAAE,CAAC,CAAC,CAC3E;AAEA,MAAa,mBAAqC;CAChD,KAAK;CACL,YAAY;EACV;;;;;;;;EAQA;;;;;;;;;;;;EAYA;;EAEA;;;;;;EAMA;;;;;;;CAOF;AACF;AAEA,MAAa,wBAA0C;CACrD,KAAK;CACL,YAAY;EACV;EACA;EACA;EACA;EACA;;;EAGA;;CAEF;AACF"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { IndexedStore } from "../store/indexed.mjs";
|
|
2
|
+
import { FactoryDatabase } from "./index.mjs";
|
|
3
|
+
//#region src/postgres/indexed.d.ts
|
|
4
|
+
/** Customer-owned transactional Postgres connection and isolated generation of Factory state. */
|
|
5
|
+
interface PostgresIndexedStoreOptions {
|
|
6
|
+
/** Drizzle database supporting interactive transactions, such as node-postgres or Neon Pool. */
|
|
7
|
+
readonly db: FactoryDatabase;
|
|
8
|
+
/** Separate environments and fresh cutovers must use distinct namespaces. */
|
|
9
|
+
readonly namespace: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Creates bounded Postgres persistence with atomic record, summary, count, and retry-receipt writes.
|
|
13
|
+
*
|
|
14
|
+
* @remarks
|
|
15
|
+
* Apply `applyFactoryMigrations` explicitly using a migration role first. Runtime access needs only
|
|
16
|
+
* SELECT/INSERT on all four indexed tables, UPDATE on records/counts, and DELETE on entries.
|
|
17
|
+
* Use a primary connection with interactive transactions; Neon HTTP is not supported. Callers own
|
|
18
|
+
* connection limits/timeouts, schema validation, authorization, and complete index memberships.
|
|
19
|
+
* Records are limited to 256 KiB, summaries to 8 KiB, and memberships to 32 per record. Queries
|
|
20
|
+
* fetch at most limit + 1 compact rows using a scoped B-tree seek; counts use a point read.
|
|
21
|
+
* Operation receipts persist indefinitely. Retry an uncertain write with the identical operation
|
|
22
|
+
* ID and intent. Pages/counts from separate calls are not a shared snapshot. This opt-in store
|
|
23
|
+
* has no list/scan, automatic migration, or Blob fallback and cannot replace StoreDriver unchanged.
|
|
24
|
+
*/
|
|
25
|
+
declare function createPostgresIndexedStore(options: PostgresIndexedStoreOptions): IndexedStore;
|
|
26
|
+
//#endregion
|
|
27
|
+
export { PostgresIndexedStoreOptions, createPostgresIndexedStore };
|
|
28
|
+
//# sourceMappingURL=indexed.d.mts.map
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { RecordExistsError, RecordNotFoundError, VersionConflictError } from "../store/driver.mjs";
|
|
2
|
+
import { compareStorageText, queryIdentity, recordQuerySchema } from "../store/query.mjs";
|
|
3
|
+
import { StoreOperationConflictError } from "../store/indexed.mjs";
|
|
4
|
+
import { canonicalJson, collectionNameSchema, decodeCursor, digest, encodeCursor, indexKeySchema, namespaceSchema, recordIdSchema, sortKeySchema, versionSchema } from "./indexed-codec.mjs";
|
|
5
|
+
import { indexedCounts, indexedEntries, indexedOperations, indexedRecords } from "./indexed-schema.mjs";
|
|
6
|
+
import { recordPredicates } from "./record-predicates.mjs";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { and, asc, count, desc, eq, gte, inArray, lt, or, sql } from "drizzle-orm";
|
|
9
|
+
import { setTimeout } from "node:timers/promises";
|
|
10
|
+
//#region src/postgres/indexed.ts
|
|
11
|
+
/**
|
|
12
|
+
* Creates bounded Postgres persistence with atomic record, summary, count, and retry-receipt writes.
|
|
13
|
+
*
|
|
14
|
+
* @remarks
|
|
15
|
+
* Apply `applyFactoryMigrations` explicitly using a migration role first. Runtime access needs only
|
|
16
|
+
* SELECT/INSERT on all four indexed tables, UPDATE on records/counts, and DELETE on entries.
|
|
17
|
+
* Use a primary connection with interactive transactions; Neon HTTP is not supported. Callers own
|
|
18
|
+
* connection limits/timeouts, schema validation, authorization, and complete index memberships.
|
|
19
|
+
* Records are limited to 256 KiB, summaries to 8 KiB, and memberships to 32 per record. Queries
|
|
20
|
+
* fetch at most limit + 1 compact rows using a scoped B-tree seek; counts use a point read.
|
|
21
|
+
* Operation receipts persist indefinitely. Retry an uncertain write with the identical operation
|
|
22
|
+
* ID and intent. Pages/counts from separate calls are not a shared snapshot. This opt-in store
|
|
23
|
+
* has no list/scan, automatic migration, or Blob fallback and cannot replace StoreDriver unchanged.
|
|
24
|
+
*/
|
|
25
|
+
function createPostgresIndexedStore(options) {
|
|
26
|
+
return createIndexedStore(options);
|
|
27
|
+
}
|
|
28
|
+
function retryableTransaction(error) {
|
|
29
|
+
const seen = /* @__PURE__ */ new Set();
|
|
30
|
+
let current = error;
|
|
31
|
+
while (current !== null && typeof current === "object" && !seen.has(current)) {
|
|
32
|
+
seen.add(current);
|
|
33
|
+
const code = Reflect.get(current, "code");
|
|
34
|
+
if (code === "40001" || code === "40P01") return true;
|
|
35
|
+
current = Reflect.get(current, "cause");
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
function createIndexedStore(options, transactional = false) {
|
|
40
|
+
const { db } = options;
|
|
41
|
+
const namespace = namespaceSchema.parse(options.namespace);
|
|
42
|
+
const recordWhere = (collection, id) => and(eq(indexedRecords.namespace, namespace), eq(indexedRecords.collection, collectionNameSchema.parse(collection)), eq(indexedRecords.id, recordIdSchema.parse(id)));
|
|
43
|
+
const operationWhere = (operationId) => and(eq(indexedOperations.namespace, namespace), eq(indexedOperations.operationId, operationId));
|
|
44
|
+
function replay(receipt, fingerprint, operationId) {
|
|
45
|
+
if (receipt.fingerprint !== fingerprint) throw new StoreOperationConflictError(operationId);
|
|
46
|
+
return { version: versionSchema.parse(receipt.version) };
|
|
47
|
+
}
|
|
48
|
+
const store = {
|
|
49
|
+
name: "postgres",
|
|
50
|
+
async transaction(run) {
|
|
51
|
+
if (transactional) return run(store);
|
|
52
|
+
for (let attempt = 0;; attempt++) try {
|
|
53
|
+
return await db.transaction((tx) => run(createIndexedStore({
|
|
54
|
+
...options,
|
|
55
|
+
db: tx
|
|
56
|
+
}, true)), { isolationLevel: "serializable" });
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (attempt >= 4 || !retryableTransaction(error)) throw error;
|
|
59
|
+
await setTimeout(5 * 2 ** attempt);
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
async get(collection, id) {
|
|
63
|
+
const [record] = await db.select({
|
|
64
|
+
value: indexedRecords.value,
|
|
65
|
+
version: indexedRecords.version
|
|
66
|
+
}).from(indexedRecords).where(recordWhere(collection, id)).limit(1);
|
|
67
|
+
return record ?? null;
|
|
68
|
+
},
|
|
69
|
+
async write(input) {
|
|
70
|
+
const collection = collectionNameSchema.parse(input.collection);
|
|
71
|
+
const id = recordIdSchema.parse(input.id);
|
|
72
|
+
const operationId = recordIdSchema.parse(input.operationId);
|
|
73
|
+
const expectedVersion = z.number().int().min(0).max(Number.MAX_SAFE_INTEGER - 1).parse(input.expectedVersion);
|
|
74
|
+
if (input.indexes.length > 32) throw new Error("A record can belong to at most 32 indexes");
|
|
75
|
+
const indexes = input.indexes.map((entry) => ({
|
|
76
|
+
index: indexKeySchema.parse(entry.index),
|
|
77
|
+
sortKey: sortKeySchema.parse(entry.sortKey),
|
|
78
|
+
summary: canonicalJson(entry.summary, 8192),
|
|
79
|
+
...entry.countGroup ? { countGroup: {
|
|
80
|
+
group: indexKeySchema.parse(entry.countGroup.group),
|
|
81
|
+
attributes: canonicalJson(entry.countGroup.attributes, 8192)
|
|
82
|
+
} } : {}
|
|
83
|
+
})).sort((left, right) => left.index < right.index ? -1 : left.index > right.index ? 1 : 0);
|
|
84
|
+
if (new Set(indexes.map((entry) => entry.index)).size !== indexes.length) throw new Error("Duplicate index membership");
|
|
85
|
+
const value = canonicalJson(input.value, 262144);
|
|
86
|
+
const fingerprint = digest(JSON.stringify({
|
|
87
|
+
collection,
|
|
88
|
+
id,
|
|
89
|
+
expectedVersion,
|
|
90
|
+
value,
|
|
91
|
+
indexes
|
|
92
|
+
}));
|
|
93
|
+
const version = expectedVersion + 1;
|
|
94
|
+
try {
|
|
95
|
+
const commit = async (tx) => {
|
|
96
|
+
if ((await tx.insert(indexedOperations).values({
|
|
97
|
+
namespace,
|
|
98
|
+
operationId,
|
|
99
|
+
fingerprint,
|
|
100
|
+
version
|
|
101
|
+
}).onConflictDoNothing().returning({ version: indexedOperations.version })).length === 0) {
|
|
102
|
+
const [receipt] = await tx.select().from(indexedOperations).where(operationWhere(operationId)).limit(1);
|
|
103
|
+
if (!receipt) throw new Error("Committed operation receipt is missing");
|
|
104
|
+
return replay(receipt, fingerprint, operationId);
|
|
105
|
+
}
|
|
106
|
+
if (expectedVersion === 0) {
|
|
107
|
+
if ((await tx.insert(indexedRecords).values({
|
|
108
|
+
namespace,
|
|
109
|
+
collection,
|
|
110
|
+
id,
|
|
111
|
+
version,
|
|
112
|
+
value: sql`${value}::jsonb`
|
|
113
|
+
}).onConflictDoNothing().returning({ id: indexedRecords.id })).length === 0) throw new RecordExistsError(collection, id);
|
|
114
|
+
} else if ((await tx.update(indexedRecords).set({
|
|
115
|
+
value: sql`${value}::jsonb`,
|
|
116
|
+
version
|
|
117
|
+
}).where(and(recordWhere(collection, id), eq(indexedRecords.version, expectedVersion))).returning({ id: indexedRecords.id })).length === 0) {
|
|
118
|
+
const [existing] = await tx.select({ id: indexedRecords.id }).from(indexedRecords).where(recordWhere(collection, id)).limit(1);
|
|
119
|
+
if (!existing) throw new RecordNotFoundError(collection, id);
|
|
120
|
+
throw new VersionConflictError(collection, id, expectedVersion);
|
|
121
|
+
}
|
|
122
|
+
const entryWhere = and(eq(indexedEntries.namespace, namespace), eq(indexedEntries.collection, collection), eq(indexedEntries.id, id));
|
|
123
|
+
const previous = await tx.select({ index: indexedEntries.index }).from(indexedEntries).where(entryWhere).limit(33);
|
|
124
|
+
if (previous.length > 32) throw new Error("Stored index memberships exceed the write bound");
|
|
125
|
+
const oldIndexes = new Set(previous.map((entry) => entry.index));
|
|
126
|
+
const newIndexes = new Set(indexes.map((entry) => entry.index));
|
|
127
|
+
const retained = indexes.filter((entry) => oldIndexes.has(entry.index));
|
|
128
|
+
if (retained.length) {
|
|
129
|
+
const counts = await tx.select({
|
|
130
|
+
index: indexedCounts.index,
|
|
131
|
+
group: indexedCounts.group,
|
|
132
|
+
attributes: indexedCounts.attributes
|
|
133
|
+
}).from(indexedCounts).where(and(eq(indexedCounts.namespace, namespace), inArray(indexedCounts.index, retained.map((entry) => entry.index))));
|
|
134
|
+
for (const entry of retained) {
|
|
135
|
+
const count = counts.find((row) => row.index === entry.index);
|
|
136
|
+
if (!count || count.group !== (entry.countGroup?.group ?? null) || canonicalJson(count.attributes, 8192) !== (entry.countGroup?.attributes ?? "null")) throw new Error("Index count attributes are immutable");
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
for (const index of [.../* @__PURE__ */ new Set([...oldIndexes, ...newIndexes])].sort()) {
|
|
140
|
+
if (oldIndexes.has(index) && newIndexes.has(index)) continue;
|
|
141
|
+
if (newIndexes.has(index)) {
|
|
142
|
+
const facet = indexes.find((entry) => entry.index === index)?.countGroup;
|
|
143
|
+
const group = facet?.group ?? null;
|
|
144
|
+
const attributes = facet ? sql`${facet.attributes}::jsonb` : sql`NULL::jsonb`;
|
|
145
|
+
const increment = oldIndexes.has(index) ? 0 : 1;
|
|
146
|
+
if ((await tx.insert(indexedCounts).values({
|
|
147
|
+
namespace,
|
|
148
|
+
index,
|
|
149
|
+
count: increment,
|
|
150
|
+
group,
|
|
151
|
+
attributes
|
|
152
|
+
}).onConflictDoUpdate({
|
|
153
|
+
target: [indexedCounts.namespace, indexedCounts.index],
|
|
154
|
+
set: { count: sql`${indexedCounts.count} + ${increment}` },
|
|
155
|
+
setWhere: and(sql`${indexedCounts.group} IS NOT DISTINCT FROM ${group}::text`, sql`${indexedCounts.attributes} IS NOT DISTINCT FROM ${attributes}`)
|
|
156
|
+
}).returning({ index: indexedCounts.index })).length !== 1) throw new Error("Index count attributes are immutable");
|
|
157
|
+
} else if ((await tx.update(indexedCounts).set({ count: sql`${indexedCounts.count} - 1` }).where(and(eq(indexedCounts.namespace, namespace), eq(indexedCounts.index, index), gte(indexedCounts.count, 1))).returning({ count: indexedCounts.count })).length !== 1) throw new Error("Index count invariant failed");
|
|
158
|
+
}
|
|
159
|
+
await tx.delete(indexedEntries).where(entryWhere);
|
|
160
|
+
if (indexes.length) await tx.insert(indexedEntries).values(indexes.map((entry) => ({
|
|
161
|
+
namespace,
|
|
162
|
+
collection,
|
|
163
|
+
id,
|
|
164
|
+
version,
|
|
165
|
+
index: entry.index,
|
|
166
|
+
sortKey: entry.sortKey,
|
|
167
|
+
summary: sql`${entry.summary}::jsonb`
|
|
168
|
+
})));
|
|
169
|
+
return { version };
|
|
170
|
+
};
|
|
171
|
+
return transactional ? await commit(db) : await db.transaction(commit, { isolationLevel: "read committed" });
|
|
172
|
+
} catch (error) {
|
|
173
|
+
if (transactional) throw error;
|
|
174
|
+
const [receipt] = await db.select().from(indexedOperations).where(operationWhere(operationId)).limit(1).catch(() => []);
|
|
175
|
+
if (receipt) return replay(receipt, fingerprint, operationId);
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
async query(input) {
|
|
180
|
+
const index = indexKeySchema.parse(input.index);
|
|
181
|
+
const limit = z.number().int().min(1).max(100).parse(input.limit);
|
|
182
|
+
const order = z.enum(["asc", "desc"]).parse(input.order ?? "desc");
|
|
183
|
+
const cursor = input.cursor === void 0 ? void 0 : decodeCursor(input.cursor, namespace, index, order);
|
|
184
|
+
const position = sql`(${indexedEntries.sortKey}, ${indexedEntries.collection}, ${indexedEntries.id})`;
|
|
185
|
+
const boundary = cursor ? sql`(${cursor.sortKey}, ${cursor.collection}, ${cursor.id})` : void 0;
|
|
186
|
+
const direction = order === "asc" ? asc : desc;
|
|
187
|
+
const rows = await db.select({
|
|
188
|
+
index: indexedEntries.index,
|
|
189
|
+
sortKey: indexedEntries.sortKey,
|
|
190
|
+
collection: indexedEntries.collection,
|
|
191
|
+
id: indexedEntries.id,
|
|
192
|
+
version: indexedEntries.version,
|
|
193
|
+
summary: indexedEntries.summary
|
|
194
|
+
}).from(indexedEntries).where(and(eq(indexedEntries.namespace, namespace), eq(indexedEntries.index, index), boundary ? order === "asc" ? sql`${position} > ${boundary}` : sql`${position} < ${boundary}` : void 0)).orderBy(direction(indexedEntries.sortKey), direction(indexedEntries.collection), direction(indexedEntries.id)).limit(limit + 1);
|
|
195
|
+
const entries = rows.slice(0, limit);
|
|
196
|
+
const last = entries.at(-1);
|
|
197
|
+
return {
|
|
198
|
+
entries,
|
|
199
|
+
nextCursor: rows.length > limit && last ? encodeCursor({
|
|
200
|
+
version: 1,
|
|
201
|
+
namespace,
|
|
202
|
+
index,
|
|
203
|
+
order,
|
|
204
|
+
sortKey: last.sortKey,
|
|
205
|
+
collection: last.collection,
|
|
206
|
+
id: last.id
|
|
207
|
+
}) : null
|
|
208
|
+
};
|
|
209
|
+
},
|
|
210
|
+
async queryRecords({ sources: sourceScopes, ...input }) {
|
|
211
|
+
const sources = z.array(z.strictObject({
|
|
212
|
+
collection: collectionNameSchema,
|
|
213
|
+
index: indexKeySchema
|
|
214
|
+
})).min(1).max(8).parse(sourceScopes).sort((a, b) => compareStorageText(a.collection, b.collection));
|
|
215
|
+
if (new Set(sources.map((source) => source.collection)).size !== sources.length) throw new Error("Record queries require exactly one source index per collection");
|
|
216
|
+
const query = recordQuerySchema.parse(input);
|
|
217
|
+
const cursorScope = `records:${queryIdentity(JSON.stringify(sources), query)}`;
|
|
218
|
+
const cursor = query.cursor === void 0 ? void 0 : decodeCursor(query.cursor, namespace, cursorScope, query.order);
|
|
219
|
+
const direction = query.order === "asc" ? asc : desc;
|
|
220
|
+
const position = sql`(${indexedEntries.sortKey}, ${indexedEntries.collection}, ${indexedEntries.id})`;
|
|
221
|
+
const boundary = cursor ? sql`(${cursor.sortKey}, ${cursor.collection}, ${cursor.id})` : void 0;
|
|
222
|
+
const membershipConditions = and(eq(indexedEntries.namespace, namespace), or(...sources.map(({ collection, index }) => and(eq(indexedEntries.collection, collection), eq(indexedEntries.index, index)))), query.since ? gte(indexedEntries.sortKey, query.since) : void 0, query.until ? lt(indexedEntries.sortKey, query.until) : void 0);
|
|
223
|
+
const conditions = and(membershipConditions, recordPredicates(indexedRecords.value, query));
|
|
224
|
+
const [{ totalCount } = {}] = query.includeTotal ? await db.select({ totalCount: count() }).from(indexedEntries).innerJoin(indexedRecords, and(eq(indexedRecords.namespace, indexedEntries.namespace), eq(indexedRecords.collection, indexedEntries.collection), eq(indexedRecords.id, indexedEntries.id))).where(conditions) : [];
|
|
225
|
+
const selectedRecord = db.select({
|
|
226
|
+
value: indexedRecords.value,
|
|
227
|
+
version: indexedRecords.version
|
|
228
|
+
}).from(indexedRecords).where(and(eq(indexedRecords.namespace, indexedEntries.namespace), eq(indexedRecords.collection, indexedEntries.collection), eq(indexedRecords.id, indexedEntries.id), recordPredicates(indexedRecords.value, query))).limit(1).as("selected_record");
|
|
229
|
+
const rows = await db.select({
|
|
230
|
+
id: indexedEntries.id,
|
|
231
|
+
collection: indexedEntries.collection,
|
|
232
|
+
sortKey: indexedEntries.sortKey,
|
|
233
|
+
value: selectedRecord.value,
|
|
234
|
+
version: selectedRecord.version
|
|
235
|
+
}).from(indexedEntries).innerJoinLateral(selectedRecord, sql`true`).where(and(membershipConditions, boundary ? query.order === "asc" ? sql`${position} > ${boundary}` : sql`${position} < ${boundary}` : void 0)).orderBy(direction(indexedEntries.sortKey), direction(indexedEntries.collection), direction(indexedEntries.id)).limit(query.limit + 1);
|
|
236
|
+
const page = rows.slice(0, query.limit);
|
|
237
|
+
const last = page.at(-1);
|
|
238
|
+
return {
|
|
239
|
+
...totalCount === void 0 ? {} : { totalCount },
|
|
240
|
+
entries: page.map(({ collection, id, value, version }) => ({
|
|
241
|
+
collection,
|
|
242
|
+
id,
|
|
243
|
+
record: {
|
|
244
|
+
value,
|
|
245
|
+
version
|
|
246
|
+
}
|
|
247
|
+
})),
|
|
248
|
+
nextCursor: rows.length > page.length && last ? encodeCursor({
|
|
249
|
+
version: 1,
|
|
250
|
+
namespace,
|
|
251
|
+
index: cursorScope,
|
|
252
|
+
order: query.order,
|
|
253
|
+
collection: last.collection,
|
|
254
|
+
sortKey: last.sortKey,
|
|
255
|
+
id: last.id
|
|
256
|
+
}) : null
|
|
257
|
+
};
|
|
258
|
+
},
|
|
259
|
+
async sumCounts(group, input) {
|
|
260
|
+
const query = recordQuerySchema.parse({
|
|
261
|
+
...input,
|
|
262
|
+
limit: 1
|
|
263
|
+
});
|
|
264
|
+
const [row] = await db.select({ total: sql`coalesce(sum(${indexedCounts.count}), 0)` }).from(indexedCounts).where(and(eq(indexedCounts.namespace, namespace), eq(indexedCounts.group, indexKeySchema.parse(group)), recordPredicates(indexedCounts.attributes, query)));
|
|
265
|
+
return z.coerce.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).parse(row?.total ?? 0);
|
|
266
|
+
},
|
|
267
|
+
async count(index) {
|
|
268
|
+
const [row] = await db.select({ count: indexedCounts.count }).from(indexedCounts).where(and(eq(indexedCounts.namespace, namespace), eq(indexedCounts.index, indexKeySchema.parse(index)))).limit(1);
|
|
269
|
+
return row?.count ?? 0;
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
return store;
|
|
273
|
+
}
|
|
274
|
+
//#endregion
|
|
275
|
+
export { createPostgresIndexedStore };
|
|
276
|
+
|
|
277
|
+
//# sourceMappingURL=indexed.mjs.map
|