@relayfile/adapter-core 0.1.20 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/src/atomic-index/index.d.ts +117 -0
  2. package/dist/src/atomic-index/index.d.ts.map +1 -0
  3. package/dist/src/atomic-index/index.js +168 -0
  4. package/dist/src/atomic-index/index.js.map +1 -0
  5. package/dist/src/index.d.ts +3 -0
  6. package/dist/src/index.d.ts.map +1 -1
  7. package/dist/src/index.js +3 -0
  8. package/dist/src/index.js.map +1 -1
  9. package/dist/src/runtime/file-native-router.d.ts +98 -0
  10. package/dist/src/runtime/file-native-router.d.ts.map +1 -0
  11. package/dist/src/runtime/file-native-router.js +399 -0
  12. package/dist/src/runtime/file-native-router.js.map +1 -0
  13. package/dist/src/runtime/file-native-router.test.d.ts +2 -0
  14. package/dist/src/runtime/file-native-router.test.d.ts.map +1 -0
  15. package/dist/src/runtime/file-native-router.test.js +294 -0
  16. package/dist/src/runtime/file-native-router.test.js.map +1 -0
  17. package/dist/src/runtime/writeback-status.d.ts +26 -0
  18. package/dist/src/runtime/writeback-status.d.ts.map +1 -0
  19. package/dist/src/runtime/writeback-status.js +33 -0
  20. package/dist/src/runtime/writeback-status.js.map +1 -0
  21. package/dist/src/storage-bridge/discovery.d.ts +2 -14
  22. package/dist/src/storage-bridge/discovery.d.ts.map +1 -1
  23. package/dist/src/storage-bridge/discovery.js +3 -9
  24. package/dist/src/storage-bridge/discovery.js.map +1 -1
  25. package/dist/tests/atomic-index/atomic-index.test.d.ts +2 -0
  26. package/dist/tests/atomic-index/atomic-index.test.d.ts.map +1 -0
  27. package/dist/tests/atomic-index/atomic-index.test.js +182 -0
  28. package/dist/tests/atomic-index/atomic-index.test.js.map +1 -0
  29. package/dist/tests/storage-bridge/storage-bridge.test.js +1 -1
  30. package/dist/tests/storage-bridge/storage-bridge.test.js.map +1 -1
  31. package/package.json +2 -2
@@ -0,0 +1,294 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { classifyWrite, executeFileNativeWriteback, validatePayload, } from "./file-native-router.js";
4
+ import { clearWritebackStatus, listWritebackStatus, recordWritebackStatus, } from "./writeback-status.js";
5
+ const resources = [
6
+ {
7
+ name: "issues",
8
+ path: "/linear/issues",
9
+ pathPattern: /^\/linear\/issues(?:\/[^/]+(?:\.json)?)?$/,
10
+ idPattern: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
11
+ schema: "discovery/linear/issues/.schema.json",
12
+ createExample: "discovery/linear/issues/.create.example.json",
13
+ },
14
+ {
15
+ name: "comments",
16
+ path: "/linear/issues/{issueId}/comments",
17
+ pathPattern: /^\/linear\/issues\/[^/]+\/comments(?:\/[^/]+(?:\.json)?)?$/,
18
+ idPattern: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
19
+ schema: "discovery/linear/issues/{issueId}/comments/.schema.json",
20
+ createExample: "discovery/linear/issues/{issueId}/comments/.create.example.json",
21
+ },
22
+ ];
23
+ const issueId = "11111111-1111-1111-1111-111111111111";
24
+ test("classifyWrite maps canonical ids to patch and drafts to create", () => {
25
+ const patch = classifyWrite(`/linear/issues/${issueId}.json`, resources);
26
+ assert.equal(patch?.kind, "patch");
27
+ assert.equal(patch?.canonical, true);
28
+ assert.equal(patch?.id, issueId);
29
+ assert.equal(patch?.resource.name, "issues");
30
+ const create = classifyWrite("/linear/issues/draft-title.json", resources);
31
+ assert.equal(create?.kind, "create");
32
+ assert.equal(create?.canonical, false);
33
+ assert.equal(create?.id, "draft-title");
34
+ assert.equal(classifyWrite("/linear/issues", resources), null);
35
+ });
36
+ test("classifyWrite maps canonical delete events to delete", () => {
37
+ const deleted = classifyWrite(`/linear/issues/${issueId}.json`, resources, {
38
+ fsEvent: "delete",
39
+ });
40
+ assert.equal(deleted?.kind, "delete");
41
+ assert.equal(deleted?.canonical, true);
42
+ assert.equal(deleted?.id, issueId);
43
+ assert.equal(classifyWrite("/linear/issues/draft-title.json", resources, {
44
+ fsEvent: "delete",
45
+ }), null);
46
+ });
47
+ test("classifyWrite chooses the most specific matching resource", () => {
48
+ const route = classifyWrite(`/linear/issues/${issueId}/comments/draft-comment.json`, resources);
49
+ assert.equal(route?.kind, "create");
50
+ assert.equal(route?.resource.name, "comments");
51
+ });
52
+ test("validatePayload enforces create required fields", () => {
53
+ const schema = issueSchema();
54
+ const result = validatePayload({ priority: 2 }, schema, "create");
55
+ assert.equal(result.ok, false);
56
+ if (!result.ok) {
57
+ assert.deepEqual(result.errors.map((error) => [error.field, error.reason]), [["title", "required"]]);
58
+ }
59
+ assert.deepEqual(validatePayload({ priority: 2 }, schema, "patch"), {
60
+ ok: true,
61
+ });
62
+ });
63
+ test("validatePayload enforces additionalProperties false", () => {
64
+ const result = validatePayload({ title: "Issue", unexpected: true }, issueSchema(), "create");
65
+ assert.equal(result.ok, false);
66
+ if (!result.ok) {
67
+ assert.equal(result.errors[0]?.field, "unexpected");
68
+ assert.equal(result.errors[0]?.reason, "additionalProperties");
69
+ }
70
+ });
71
+ test("validatePayload rejects read-only fields", () => {
72
+ const result = validatePayload({ title: "Issue", id: issueId }, issueSchema(), "create");
73
+ assert.equal(result.ok, false);
74
+ if (!result.ok) {
75
+ assert.equal(result.errors[0]?.field, "id");
76
+ assert.equal(result.errors[0]?.reason, "readOnly");
77
+ }
78
+ });
79
+ test("validatePayload enforces field types and enums", () => {
80
+ const result = validatePayload({ title: 123, priority: 9 }, issueSchema(), "create");
81
+ assert.equal(result.ok, false);
82
+ if (!result.ok) {
83
+ assert.deepEqual(result.errors.map((error) => [error.field, error.reason]), [
84
+ ["title", "type"],
85
+ ["priority", "enum"],
86
+ ]);
87
+ }
88
+ });
89
+ test("writeback status sink records and filters entries", () => {
90
+ clearWritebackStatus();
91
+ recordWritebackStatus({
92
+ path: "/linear/issues/draft.json",
93
+ op: "create",
94
+ outcome: "validation_failed",
95
+ error: "Missing required field \"title\"",
96
+ field: "title",
97
+ timestamp: "2026-05-09T09:00:00.000Z",
98
+ });
99
+ recordWritebackStatus({
100
+ path: `/linear/issues/${issueId}.json`,
101
+ op: "patch",
102
+ outcome: "ok",
103
+ timestamp: "2026-05-09T09:01:00.000Z",
104
+ });
105
+ assert.equal(listWritebackStatus().length, 2);
106
+ assert.deepEqual(listWritebackStatus({ outcome: "validation_failed" }), [
107
+ {
108
+ path: "/linear/issues/draft.json",
109
+ op: "create",
110
+ outcome: "validation_failed",
111
+ error: "Missing required field \"title\"",
112
+ field: "title",
113
+ timestamp: "2026-05-09T09:00:00.000Z",
114
+ },
115
+ ]);
116
+ });
117
+ test("executeFileNativeWriteback validates, resolves create, records ok, and returns receipt", async () => {
118
+ clearWritebackStatus();
119
+ const result = await executeFileNativeWriteback({
120
+ path: "/linear/issues/draft-title.json",
121
+ content: JSON.stringify({ title: "Issue" }),
122
+ resources,
123
+ loadSchema: issueSchema,
124
+ now: () => new Date("2026-05-09T09:02:00.000Z"),
125
+ resolveWritebackRequest(path, content) {
126
+ assert.equal(path, "/linear/issues/draft-title.json");
127
+ assert.equal(JSON.parse(content).title, "Issue");
128
+ return {
129
+ action: "create_issue",
130
+ method: "POST",
131
+ endpoint: "/graphql",
132
+ };
133
+ },
134
+ applyWriteback(request, route) {
135
+ assert.equal(route.kind, "create");
136
+ assert.equal(request.action, "create_issue");
137
+ return { externalId: issueId };
138
+ },
139
+ });
140
+ assert.equal(result.ok, true);
141
+ if (result.ok) {
142
+ assert.equal(result.route.kind, "create");
143
+ assert.deepEqual(result.createReceipt, {
144
+ draftPath: "/linear/issues/draft-title.json",
145
+ canonicalPath: `/linear/issues/${issueId}.json`,
146
+ id: issueId,
147
+ resource: "issues",
148
+ createdAt: "2026-05-09T09:02:00.000Z",
149
+ });
150
+ assert.equal(result.status.outcome, "ok");
151
+ }
152
+ assert.deepEqual(listWritebackStatus({ path: "/linear/issues/draft-title.json" }), [
153
+ {
154
+ path: "/linear/issues/draft-title.json",
155
+ op: "create",
156
+ status: "accepted",
157
+ code: "OK",
158
+ outcome: "ok",
159
+ timestamp: "2026-05-09T09:02:00.000Z",
160
+ },
161
+ ]);
162
+ });
163
+ test("executeFileNativeWriteback records validation and read-only failures", async () => {
164
+ clearWritebackStatus();
165
+ const missingRequired = await executeFileNativeWriteback({
166
+ path: "/linear/issues/draft-title.json",
167
+ content: JSON.stringify({ priority: 2 }),
168
+ resources,
169
+ loadSchema: issueSchema,
170
+ now: () => new Date("2026-05-09T09:03:00.000Z"),
171
+ resolveWritebackRequest() {
172
+ throw new Error("resolver should not run after validation failure");
173
+ },
174
+ });
175
+ assert.equal(missingRequired.ok, false);
176
+ assert.equal(missingRequired.status?.status, "rejected");
177
+ assert.equal(missingRequired.status?.code, "VALIDATION_FAILED");
178
+ assert.equal(missingRequired.status?.outcome, "validation_failed");
179
+ assert.equal(missingRequired.status?.field, "title");
180
+ const readonly = await executeFileNativeWriteback({
181
+ path: `/linear/issues/${issueId}.json`,
182
+ content: JSON.stringify({ id: issueId }),
183
+ resources,
184
+ loadSchema: issueSchema,
185
+ now: () => new Date("2026-05-09T09:04:00.000Z"),
186
+ resolveWritebackRequest() {
187
+ throw new Error("resolver should not run after read-only failure");
188
+ },
189
+ });
190
+ assert.equal(readonly.ok, false);
191
+ assert.equal(readonly.status?.status, "rejected");
192
+ assert.equal(readonly.status?.code, "READ_ONLY_FIELD");
193
+ assert.equal(readonly.status?.outcome, "readonly_rejected");
194
+ assert.equal(readonly.status?.field, "id");
195
+ assert.deepEqual(listWritebackStatus().map((entry) => [
196
+ entry.op,
197
+ entry.status,
198
+ entry.code,
199
+ entry.outcome,
200
+ entry.field,
201
+ ]), [
202
+ ["create", "rejected", "VALIDATION_FAILED", "validation_failed", "title"],
203
+ ["patch", "rejected", "READ_ONLY_FIELD", "readonly_rejected", "id"],
204
+ ]);
205
+ });
206
+ test("executeFileNativeWriteback routes canonical deletes without schema validation", async () => {
207
+ clearWritebackStatus();
208
+ const result = await executeFileNativeWriteback({
209
+ path: `/linear/issues/${issueId}.json`,
210
+ resources,
211
+ fsEvent: "delete",
212
+ now: () => new Date("2026-05-09T09:05:00.000Z"),
213
+ resolveDeleteRequest(path) {
214
+ assert.equal(path, `/linear/issues/${issueId}.json`);
215
+ return {
216
+ action: "delete_issue",
217
+ method: "DELETE",
218
+ endpoint: `/issues/${issueId}`,
219
+ };
220
+ },
221
+ });
222
+ assert.equal(result.ok, true);
223
+ if (result.ok) {
224
+ assert.equal(result.route.kind, "delete");
225
+ assert.equal(result.request?.method, "DELETE");
226
+ }
227
+ assert.deepEqual(listWritebackStatus(), [
228
+ {
229
+ path: `/linear/issues/${issueId}.json`,
230
+ op: "delete",
231
+ status: "accepted",
232
+ code: "OK",
233
+ outcome: "ok",
234
+ timestamp: "2026-05-09T09:05:00.000Z",
235
+ },
236
+ ]);
237
+ });
238
+ test("executeFileNativeWriteback bypasses validation for schema-less create/patch resources", async () => {
239
+ clearWritebackStatus();
240
+ const emptySchema = {};
241
+ const create = await executeFileNativeWriteback({
242
+ path: "/linear/issues/draft.json",
243
+ content: JSON.stringify({ anything: "goes", id: "still-allowed-no-schema" }),
244
+ resources,
245
+ loadSchema: () => emptySchema,
246
+ now: () => new Date("2026-05-09T10:00:00.000Z"),
247
+ resolveWritebackRequest(path) {
248
+ assert.equal(path, "/linear/issues/draft.json");
249
+ return {
250
+ action: "create_issue",
251
+ method: "POST",
252
+ endpoint: "/issues",
253
+ body: { title: "from draft" },
254
+ };
255
+ },
256
+ });
257
+ assert.equal(create.ok, true);
258
+ if (create.ok) {
259
+ assert.equal(create.route.kind, "create");
260
+ }
261
+ const patch = await executeFileNativeWriteback({
262
+ path: `/linear/issues/${issueId}.json`,
263
+ content: JSON.stringify({ anything: "goes", id: "still-allowed-no-schema" }),
264
+ resources,
265
+ loadSchema: () => emptySchema,
266
+ now: () => new Date("2026-05-09T10:00:01.000Z"),
267
+ resolveWritebackRequest(path) {
268
+ assert.equal(path, `/linear/issues/${issueId}.json`);
269
+ return {
270
+ action: "update_issue",
271
+ method: "PUT",
272
+ endpoint: `/issues/${issueId}`,
273
+ body: { anything: "goes" },
274
+ };
275
+ },
276
+ });
277
+ assert.equal(patch.ok, true);
278
+ if (patch.ok) {
279
+ assert.equal(patch.route.kind, "patch");
280
+ }
281
+ });
282
+ function issueSchema() {
283
+ return {
284
+ type: "object",
285
+ required: ["title"],
286
+ additionalProperties: false,
287
+ properties: {
288
+ title: { type: "string" },
289
+ priority: { enum: [0, 1, 2, 3, 4] },
290
+ id: { type: "string", readOnly: true },
291
+ },
292
+ };
293
+ }
294
+ //# sourceMappingURL=file-native-router.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-native-router.test.js","sourceRoot":"","sources":["../../../src/runtime/file-native-router.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ export type WritebackOperation = "patch" | "create" | "delete";
2
+ export type WritebackOutcome = "ok" | "validation_failed" | "readonly_rejected" | "adapter_error";
3
+ export type WritebackStatusState = "accepted" | "rejected";
4
+ export type WritebackStatusCode = "OK" | "VALIDATION_FAILED" | "READ_ONLY_FIELD" | "ADAPTER_ERROR";
5
+ export interface WritebackStatusEntry {
6
+ path: string;
7
+ op: WritebackOperation;
8
+ status?: WritebackStatusState;
9
+ code?: WritebackStatusCode;
10
+ outcome: WritebackOutcome;
11
+ error?: string;
12
+ field?: string;
13
+ timestamp: string;
14
+ }
15
+ export interface WritebackStatusFilter {
16
+ path?: string;
17
+ op?: WritebackOperation;
18
+ status?: WritebackStatusState;
19
+ code?: WritebackStatusCode;
20
+ outcome?: WritebackOutcome;
21
+ field?: string;
22
+ }
23
+ export declare function recordWritebackStatus(entry: WritebackStatusEntry): void;
24
+ export declare function listWritebackStatus(filter?: WritebackStatusFilter): WritebackStatusEntry[];
25
+ export declare function clearWritebackStatus(): void;
26
+ //# sourceMappingURL=writeback-status.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"writeback-status.d.ts","sourceRoot":"","sources":["../../../src/runtime/writeback-status.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE/D,MAAM,MAAM,gBAAgB,GACxB,IAAI,GACJ,mBAAmB,GACnB,mBAAmB,GACnB,eAAe,CAAC;AAEpB,MAAM,MAAM,oBAAoB,GAAG,UAAU,GAAG,UAAU,CAAC;AAE3D,MAAM,MAAM,mBAAmB,GAC3B,IAAI,GACJ,mBAAmB,GACnB,iBAAiB,GACjB,eAAe,CAAC;AAEpB,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,kBAAkB,CAAC;IACvB,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,IAAI,CAAC,EAAE,mBAAmB,CAAC;IAC3B,OAAO,EAAE,gBAAgB,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,kBAAkB,CAAC;IACxB,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,IAAI,CAAC,EAAE,mBAAmB,CAAC;IAC3B,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAID,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,oBAAoB,GAAG,IAAI,CAEvE;AAED,wBAAgB,mBAAmB,CACjC,MAAM,GAAE,qBAA0B,GACjC,oBAAoB,EAAE,CAwBxB;AAED,wBAAgB,oBAAoB,IAAI,IAAI,CAE3C"}
@@ -0,0 +1,33 @@
1
+ const writebackStatusEntries = [];
2
+ export function recordWritebackStatus(entry) {
3
+ writebackStatusEntries.push({ ...entry });
4
+ }
5
+ export function listWritebackStatus(filter = {}) {
6
+ return writebackStatusEntries
7
+ .filter((entry) => {
8
+ if (filter.path !== undefined && entry.path !== filter.path) {
9
+ return false;
10
+ }
11
+ if (filter.op !== undefined && entry.op !== filter.op) {
12
+ return false;
13
+ }
14
+ if (filter.status !== undefined && entry.status !== filter.status) {
15
+ return false;
16
+ }
17
+ if (filter.code !== undefined && entry.code !== filter.code) {
18
+ return false;
19
+ }
20
+ if (filter.outcome !== undefined && entry.outcome !== filter.outcome) {
21
+ return false;
22
+ }
23
+ if (filter.field !== undefined && entry.field !== filter.field) {
24
+ return false;
25
+ }
26
+ return true;
27
+ })
28
+ .map((entry) => ({ ...entry }));
29
+ }
30
+ export function clearWritebackStatus() {
31
+ writebackStatusEntries.length = 0;
32
+ }
33
+ //# sourceMappingURL=writeback-status.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"writeback-status.js","sourceRoot":"","sources":["../../../src/runtime/writeback-status.ts"],"names":[],"mappings":"AAoCA,MAAM,sBAAsB,GAA2B,EAAE,CAAC;AAE1D,MAAM,UAAU,qBAAqB,CAAC,KAA2B;IAC/D,sBAAsB,CAAC,IAAI,CAAC,EAAE,GAAG,KAAK,EAAE,CAAC,CAAC;AAC5C,CAAC;AAED,MAAM,UAAU,mBAAmB,CACjC,SAAgC,EAAE;IAElC,OAAO,sBAAsB;SAC1B,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE;QAChB,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,CAAC,IAAI,EAAE,CAAC;YAC5D,OAAO,KAAK,CAAC;QACf,CAAC;QACD,IAAI,MAAM,CAAC,EAAE,KAAK,SAAS,IAAI,KAAK,CAAC,EAAE,KAAK,MAAM,CAAC,EAAE,EAAE,CAAC;YACtD,OAAO,KAAK,CAAC;QACf,CAAC;QACD,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS,IAAI,KAAK,CAAC,MAAM,KAAK,MAAM,CAAC,MAAM,EAAE,CAAC;YAClE,OAAO,KAAK,CAAC;QACf,CAAC;QACD,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,CAAC,IAAI,EAAE,CAAC;YAC5D,OAAO,KAAK,CAAC;QACf,CAAC;QACD,IAAI,MAAM,CAAC,OAAO,KAAK,SAAS,IAAI,KAAK,CAAC,OAAO,KAAK,MAAM,CAAC,OAAO,EAAE,CAAC;YACrE,OAAO,KAAK,CAAC;QACf,CAAC;QACD,IAAI,MAAM,CAAC,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,KAAK,KAAK,MAAM,CAAC,KAAK,EAAE,CAAC;YAC/D,OAAO,KAAK,CAAC;QACf,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC,CAAC;SACD,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,oBAAoB;IAClC,sBAAsB,CAAC,MAAM,GAAG,CAAC,CAAC;AACpC,CAAC"}
@@ -1,14 +1,6 @@
1
+ import { ReadOnlyFieldError, type AdapterResourceConfig, type AdapterResourceOperation } from "../runtime/file-native-router.js";
1
2
  import type { JsonValue } from "./event.js";
2
- export interface AdapterResourceConfig {
3
- readonly name: string;
4
- readonly path: string;
5
- readonly pathPattern: RegExp;
6
- readonly idPattern: RegExp;
7
- readonly schema: string;
8
- readonly createExample: string;
9
- readonly operations?: readonly AdapterResourceOperation[];
10
- }
11
- export type AdapterResourceOperation = "create" | "update" | "delete";
3
+ export { ReadOnlyFieldError, type AdapterResourceConfig, type AdapterResourceOperation, };
12
4
  export interface JsonSchemaObject {
13
5
  readonly $schema?: string;
14
6
  readonly type?: string | readonly string[];
@@ -21,10 +13,6 @@ export interface JsonSchemaObject {
21
13
  readonly additionalProperties?: boolean | JsonSchemaObject;
22
14
  readonly [key: string]: unknown;
23
15
  }
24
- export declare class ReadOnlyFieldError extends Error {
25
- readonly fields: readonly string[];
26
- constructor(fields: readonly string[]);
27
- }
28
16
  export declare function findResourceByPath(resources: readonly AdapterResourceConfig[], path: string): AdapterResourceConfig | undefined;
29
17
  export declare function assertReadOnlyFieldsRejected(schema: JsonSchemaObject, patch: Record<string, unknown>): void;
30
18
  export declare function collectReadOnlyFields(schema: JsonSchemaObject, prefix?: string): string[];
@@ -1 +1 @@
1
- {"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../../../src/storage-bridge/discovery.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,wBAAwB,EAAE,CAAC;CAC3D;AAED,MAAM,MAAM,wBAAwB,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEtE,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC;IAC3C,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IACvD,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;IAClC,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC7C,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC7C,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC7C,QAAQ,CAAC,oBAAoB,CAAC,EAAE,OAAO,GAAG,gBAAgB,CAAC;IAC3D,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC;AAED,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;gBAEvB,MAAM,EAAE,SAAS,MAAM,EAAE;CAKtC;AAED,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,SAAS,qBAAqB,EAAE,EAC3C,IAAI,EAAE,MAAM,GACX,qBAAqB,GAAG,SAAS,CAGnC;AAED,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,gBAAgB,EACxB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,IAAI,CAMN;AAED,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,gBAAgB,EACxB,MAAM,SAAK,GACV,MAAM,EAAE,CAeV;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAE/E"}
1
+ {"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../../../src/storage-bridge/discovery.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,KAAK,qBAAqB,EAC1B,KAAK,wBAAwB,EAC9B,MAAM,kCAAkC,CAAC;AAC1C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,OAAO,EACL,kBAAkB,EAClB,KAAK,qBAAqB,EAC1B,KAAK,wBAAwB,GAC9B,CAAC;AAEF,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC;IAC3C,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IACvD,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;IAClC,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC7C,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC7C,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC7C,QAAQ,CAAC,oBAAoB,CAAC,EAAE,OAAO,GAAG,gBAAgB,CAAC;IAC3D,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC;AAED,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,SAAS,qBAAqB,EAAE,EAC3C,IAAI,EAAE,MAAM,GACX,qBAAqB,GAAG,SAAS,CAGnC;AAED,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,gBAAgB,EACxB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,IAAI,CAMN;AAED,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,gBAAgB,EACxB,MAAM,SAAK,GACV,MAAM,EAAE,CAeV;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAE/E"}
@@ -1,11 +1,5 @@
1
- export class ReadOnlyFieldError extends Error {
2
- fields;
3
- constructor(fields) {
4
- super(`Writeback attempted to modify read-only fields: ${fields.join(", ")}`);
5
- this.name = "ReadOnlyFieldError";
6
- this.fields = fields;
7
- }
8
- }
1
+ import { ReadOnlyFieldError, } from "../runtime/file-native-router.js";
2
+ export { ReadOnlyFieldError, };
9
3
  export function findResourceByPath(resources, path) {
10
4
  const normalizedPath = path.endsWith(".json") ? path : path.replace(/\/$/, "");
11
5
  return resources.find((resource) => resource.pathPattern.test(normalizedPath));
@@ -14,7 +8,7 @@ export function assertReadOnlyFieldsRejected(schema, patch) {
14
8
  const readOnlyFields = collectReadOnlyFields(schema);
15
9
  const rejected = readOnlyFields.filter((field) => hasPath(patch, field));
16
10
  if (rejected.length > 0) {
17
- throw new ReadOnlyFieldError(rejected);
11
+ throw new ReadOnlyFieldError(rejected[0]);
18
12
  }
19
13
  }
20
14
  export function collectReadOnlyFields(schema, prefix = "") {
@@ -1 +1 @@
1
- {"version":3,"file":"discovery.js","sourceRoot":"","sources":["../../../src/storage-bridge/discovery.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"discovery.js","sourceRoot":"","sources":["../../../src/storage-bridge/discovery.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=atomic-index.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"atomic-index.test.d.ts","sourceRoot":"","sources":["../../../tests/atomic-index/atomic-index.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,182 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { AtomicIndexExhaustedError, isConflictError, upsertIndexAtomic, } from "../../src/atomic-index/index.js";
4
+ class CasVfs {
5
+ files = new Map();
6
+ revisionCounter = 0;
7
+ options;
8
+ conflictWrites = [];
9
+ successfulWrites = [];
10
+ constructor(options = {}) {
11
+ this.options = options;
12
+ }
13
+ armConflicts(path, count) {
14
+ this.options.forceConflicts = { path, remaining: count };
15
+ }
16
+ readFile(path) {
17
+ const existing = this.files.get(path);
18
+ return existing ? { content: existing.content, revision: existing.revision } : undefined;
19
+ }
20
+ writeFile(path, content, options) {
21
+ if (this.options.forceConflicts &&
22
+ this.options.forceConflicts.path === path &&
23
+ this.options.forceConflicts.remaining > 0) {
24
+ this.options.forceConflicts.remaining -= 1;
25
+ this.conflictWrites.push(path);
26
+ throw conflictError(this.files.get(path)?.revision ?? "0", options?.baseRevision ?? "0");
27
+ }
28
+ const baseRevision = options?.baseRevision ?? "0";
29
+ const existing = this.files.get(path);
30
+ const currentRevision = existing?.revision ?? "0";
31
+ if (currentRevision !== baseRevision) {
32
+ this.conflictWrites.push(path);
33
+ throw conflictError(currentRevision, baseRevision);
34
+ }
35
+ this.revisionCounter += 1;
36
+ const nextRevision = `r${this.revisionCounter}`;
37
+ this.files.set(path, { content, revision: nextRevision });
38
+ this.successfulWrites.push(path);
39
+ return { revision: nextRevision };
40
+ }
41
+ }
42
+ function conflictError(currentRevision, expectedRevision) {
43
+ const error = new Error(`RevisionConflictError: expected ${expectedRevision}, current ${currentRevision}`);
44
+ Object.assign(error, {
45
+ name: "RevisionConflictError",
46
+ status: 409,
47
+ code: "revision_conflict",
48
+ expectedRevision,
49
+ currentRevision,
50
+ });
51
+ return error;
52
+ }
53
+ const PATH = "/example/_index.json";
54
+ const PARSE_ROWS = (content) => {
55
+ if (!content) {
56
+ return [];
57
+ }
58
+ try {
59
+ const parsed = JSON.parse(content);
60
+ return Array.isArray(parsed) ? parsed : [];
61
+ }
62
+ catch {
63
+ return [];
64
+ }
65
+ };
66
+ const SERIALIZE_ROWS = (rows) => `${JSON.stringify(rows)}\n`;
67
+ function upsertRow(rows, next) {
68
+ const map = new Map();
69
+ for (const row of rows) {
70
+ map.set(row.id, row);
71
+ }
72
+ map.set(next.id, next);
73
+ return [...map.values()].sort((left, right) => left.id.localeCompare(right.id));
74
+ }
75
+ test("isConflictError detects RevisionConflictError by name", () => {
76
+ const err = Object.assign(new Error("boom"), { name: "RevisionConflictError" });
77
+ assert.equal(isConflictError(err), true);
78
+ });
79
+ test("isConflictError detects 409 by status", () => {
80
+ assert.equal(isConflictError({ status: 409 }), true);
81
+ assert.equal(isConflictError({ statusCode: 409 }), true);
82
+ });
83
+ test("isConflictError detects revision_conflict by code", () => {
84
+ assert.equal(isConflictError({ code: "revision_conflict" }), true);
85
+ });
86
+ test("isConflictError returns false for unrelated errors", () => {
87
+ assert.equal(isConflictError(new Error("disk full")), false);
88
+ assert.equal(isConflictError({ status: 500 }), false);
89
+ assert.equal(isConflictError(undefined), false);
90
+ assert.equal(isConflictError(null), false);
91
+ assert.equal(isConflictError("conflict"), false);
92
+ });
93
+ test("isConflictError does not match unrelated codes that merely contain the word 'conflict'", () => {
94
+ // Regression: previous regex `/(revision_conflict|conflict)/i` matched
95
+ // any code with "conflict" as a substring, causing false retries on
96
+ // unrelated errors like merge_conflict / name_conflict.
97
+ assert.equal(isConflictError({ code: "merge_conflict" }), false);
98
+ assert.equal(isConflictError({ code: "name_conflict" }), false);
99
+ assert.equal(isConflictError({ code: "permission_conflict" }), false);
100
+ assert.equal(isConflictError({ code: "CONFLICT" }), false);
101
+ });
102
+ test("upsertIndexAtomic writes once on the happy path with no conflict", async () => {
103
+ const vfs = new CasVfs();
104
+ await upsertIndexAtomic(vfs, PATH, PARSE_ROWS, (rows) => upsertRow(rows, { id: "7", title: "Add login" }), SERIALIZE_ROWS, { sleep: async () => undefined });
105
+ assert.equal(vfs.successfulWrites.length, 1);
106
+ assert.equal(vfs.conflictWrites.length, 0);
107
+ const stored = vfs.readFile(PATH);
108
+ assert.ok(stored, "index file should exist");
109
+ const rows = JSON.parse(stored.content);
110
+ assert.equal(rows.length, 1);
111
+ assert.equal(rows[0]?.id, "7");
112
+ });
113
+ test("upsertIndexAtomic retries successfully after a single conflict", async () => {
114
+ const vfs = new CasVfs();
115
+ // seed with a row from a "competing" writer
116
+ vfs.writeFile(PATH, JSON.stringify([{ id: "5", title: "Existing" }]) + "\n", {
117
+ baseRevision: "0",
118
+ });
119
+ vfs.armConflicts(PATH, 1);
120
+ let invocations = 0;
121
+ await upsertIndexAtomic(vfs, PATH, PARSE_ROWS, (rows) => {
122
+ invocations += 1;
123
+ return upsertRow(rows, { id: "7", title: "New" });
124
+ }, SERIALIZE_ROWS, { sleep: async () => undefined, baseDelayMs: 0 });
125
+ assert.equal(invocations, 2, "merge should run on each attempt");
126
+ assert.equal(vfs.conflictWrites.length, 1);
127
+ const stored = vfs.readFile(PATH);
128
+ const rows = JSON.parse(stored.content);
129
+ const ids = rows.map((row) => row.id).sort();
130
+ // Both the pre-seeded "competing" row and the new row must survive.
131
+ assert.deepEqual(ids, ["5", "7"]);
132
+ });
133
+ test("upsertIndexAtomic throws AtomicIndexExhaustedError when conflicts persist past the budget", async () => {
134
+ const vfs = new CasVfs({
135
+ forceConflicts: { path: PATH, remaining: 100 },
136
+ });
137
+ await assert.rejects(upsertIndexAtomic(vfs, PATH, PARSE_ROWS, (rows) => upsertRow(rows, { id: "7", title: "Stuck" }), SERIALIZE_ROWS, { maxAttempts: 3, sleep: async () => undefined, baseDelayMs: 0 }), (error) => {
138
+ assert.ok(error instanceof AtomicIndexExhaustedError);
139
+ assert.equal(error.attempts, 3);
140
+ assert.equal(error.path, PATH);
141
+ return true;
142
+ });
143
+ assert.equal(vfs.successfulWrites.length, 0);
144
+ });
145
+ test("upsertIndexAtomic rethrows non-conflict errors without retrying", async () => {
146
+ const vfs = {
147
+ readFile() {
148
+ return undefined;
149
+ },
150
+ writeFile() {
151
+ const error = new Error("disk full");
152
+ Object.assign(error, { status: 500 });
153
+ throw error;
154
+ },
155
+ };
156
+ let attempts = 0;
157
+ await assert.rejects(upsertIndexAtomic(vfs, PATH, PARSE_ROWS, (rows) => {
158
+ attempts += 1;
159
+ return rows;
160
+ }, SERIALIZE_ROWS, { sleep: async () => undefined, maxAttempts: 5 }), /disk full/);
161
+ assert.equal(attempts, 1, "non-conflict errors must not trigger retry");
162
+ });
163
+ test("upsertIndexAtomic reports existedAtWrite=true when the file already existed at the winning read", async () => {
164
+ // Regression: callers that build an IngestResult-shaped accounting wrapper
165
+ // around upsertIndexAtomic (e.g. github's runAtomicIndexWrite) previously
166
+ // read existedBefore from a dedicated pre-CAS read. If a racing writer
167
+ // created the file between that read and the eventual successful CAS
168
+ // write, the wrapper reported filesWritten: 1 / filesUpdated: 0 even though
169
+ // the winning attempt actually updated an existing file. The fix moves the
170
+ // existed check inside upsertIndexAtomic where the read that produced the
171
+ // winning baseRevision is the source of truth.
172
+ const vfs = new CasVfs();
173
+ vfs.writeFile(PATH, JSON.stringify([{ id: "1", title: "One" }]) + "\n");
174
+ const { existedAtWrite } = await upsertIndexAtomic(vfs, PATH, PARSE_ROWS, (rows) => upsertRow(rows, { id: "2", title: "Two" }), SERIALIZE_ROWS, { sleep: async () => undefined, baseDelayMs: 0 });
175
+ assert.equal(existedAtWrite, true, "existedAtWrite must reflect the winning read");
176
+ });
177
+ test("upsertIndexAtomic reports existedAtWrite=false on first creation", async () => {
178
+ const vfs = new CasVfs();
179
+ const { existedAtWrite } = await upsertIndexAtomic(vfs, PATH, PARSE_ROWS, (rows) => upsertRow(rows, { id: "1", title: "One" }), SERIALIZE_ROWS, { sleep: async () => undefined, baseDelayMs: 0 });
180
+ assert.equal(existedAtWrite, false);
181
+ });
182
+ //# sourceMappingURL=atomic-index.test.js.map