@telepath-computer/autofile 0.0.0-stage → 0.3.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/store.js ADDED
@@ -0,0 +1,1381 @@
1
+ /**
2
+ * Reads and writes Autofile's on-disk types and records, and exposes recognized
3
+ * records, declarations, and blobs as plain resource data. Each operation reads
4
+ * current files and keeps protocol-specific behavior outside the store.
5
+ */
6
+ import { randomBytes, randomUUID } from "node:crypto";
7
+ import { constants } from "node:fs";
8
+ import { lstat, mkdir, open, readdir, rename, rmdir, unlink, writeFile, } from "node:fs/promises";
9
+ import { dirname, join, resolve } from "node:path";
10
+ import { setTimeout as delay } from "node:timers/promises";
11
+ import { lookup as lookupMimeType } from "mime-types";
12
+ import { Query } from "mingo";
13
+ import lockfile from "proper-lockfile";
14
+ import { BLOB_NAME_PATTERN, BLOB_URI_PATTERN, MAX_BLOB_BYTES, blobNameFromUri, blobUri, deleteBlobUris, prepareBlob, writePreparedBlobs, } from "./blobs.js";
15
+ import { getStorage, getWriteLockPath } from "./config.js";
16
+ import { prepareSchema, SchemaRefusal } from "./schema.js";
17
+ const typeNamePattern = /^[a-z][a-z0-9-]*$/u;
18
+ const keyPattern = /^[a-z0-9][a-z0-9-]*$/u;
19
+ const recordUriPattern = /^autofile:\/\/\/([a-z][a-z0-9-]*)\/([a-z0-9][a-z0-9-]*)$/u;
20
+ const typeUriPattern = /^autofile:\/\/\/_types\/([a-z][a-z0-9-]*)$/u;
21
+ const timestampPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/u;
22
+ const MAX_FIND_RECORDS_PAGE_BYTES = 64 * 1024;
23
+ const tidAlphabet = "234567abcdefghijklmnopqrstuvwxyz";
24
+ const invalidTypeMessage = "type: must use lowercase letters, digits, and hyphens and start with a letter";
25
+ const invalidKeyMessage = "key: must use lowercase letters, digits, and hyphens and start with a letter or digit";
26
+ const invalidRecordUriMessage = "uri: must be a record URI, such as autofile:///task/book-the-dentist";
27
+ const clockIdentifier = BigInt(randomBytes(2).readUInt16BE() & 0x3ff);
28
+ let lastTidTimestamp = 0n;
29
+ /** Opens a store, creating its data folder when it does not exist. */
30
+ export async function openStore(options = {}) {
31
+ const dataFolder = resolve(options.dataFolder ?? await getStorage());
32
+ const writeLockPath = resolve(options.writeLockPath ?? getWriteLockPath());
33
+ await Promise.all([
34
+ mkdir(dataFolder, { recursive: true }),
35
+ mkdir(dirname(writeLockPath), { recursive: true }),
36
+ ]);
37
+ return createStore(dataFolder, writeLockPath);
38
+ }
39
+ function createStore(dataFolder, writeLockPath) {
40
+ const typeFolder = join(dataFolder, "_types");
41
+ const blobFolder = join(dataFolder, "_blobs");
42
+ async function withWriteLock(write) {
43
+ let release;
44
+ let retryDelay = 50;
45
+ while (true) {
46
+ try {
47
+ release = await lockfile.lock(dirname(writeLockPath), {
48
+ lockfilePath: writeLockPath,
49
+ stale: 10_000,
50
+ update: 5_000,
51
+ retries: 0,
52
+ });
53
+ break;
54
+ }
55
+ catch (error) {
56
+ if (!hasCode(error, "ELOCKED"))
57
+ throw error;
58
+ await delay(retryDelay);
59
+ retryDelay = Math.min(Math.ceil(retryDelay * 1.2), 1_000);
60
+ }
61
+ }
62
+ try {
63
+ return await write();
64
+ }
65
+ finally {
66
+ await release();
67
+ }
68
+ }
69
+ async function findRecords(input) {
70
+ if (!typeNamePattern.test(input.type))
71
+ return refuse(invalidTypeMessage);
72
+ if (input.q !== undefined && typeof input.q !== "string") {
73
+ return refuse("q: must be a string");
74
+ }
75
+ if (input.where !== undefined && !isJsonObject(input.where)) {
76
+ return refuse("where: must be a JSON object");
77
+ }
78
+ const limit = input.limit ?? 50;
79
+ if (!Number.isInteger(limit) || limit < 1 || limit > 200) {
80
+ return refuse("limit: must be an integer from 1 to 200");
81
+ }
82
+ if (input.fields !== undefined
83
+ && (!Array.isArray(input.fields) || input.fields.some((field) => (typeof field !== "string" || field.length === 0)))) {
84
+ return refuse("fields: must be an array of non-empty strings");
85
+ }
86
+ if (input.cursor !== undefined && typeof input.cursor !== "string") {
87
+ return refuse("cursor: must be a string");
88
+ }
89
+ const whereRefusal = input.where === undefined
90
+ ? undefined
91
+ : findWhereRefusal(input.where);
92
+ if (whereRefusal !== undefined)
93
+ return refuse(whereRefusal);
94
+ const afterUri = input.cursor === undefined
95
+ ? undefined
96
+ : decodeRecordCursor(input.cursor, input.type);
97
+ if (input.cursor !== undefined && afterUri === undefined) {
98
+ return refuse("cursor: is invalid");
99
+ }
100
+ const declaration = await readDeclaration(typeFolder, input.type);
101
+ if (declaration.status !== "found")
102
+ return declarationRefusal(input.type, declaration);
103
+ const schema = await preparedDeclarationSchema(declaration.value, typeUri(input.type));
104
+ if ("errors" in schema)
105
+ return schema;
106
+ const entries = (await recognizedRecordEntries(join(dataFolder, input.type)))
107
+ .map((entry) => ({
108
+ entry,
109
+ uri: recordUri(input.type, entry.name.slice(0, -".json".length)),
110
+ }))
111
+ .sort((left, right) => left.uri < right.uri ? -1 : left.uri > right.uri ? 1 : 0);
112
+ const start = afterUri === undefined
113
+ ? 0
114
+ : entries.findIndex(({ uri }) => uri > afterUri);
115
+ const firstPageIndex = start < 0 ? entries.length : start;
116
+ const words = input.q?.trim().toLowerCase().split(/\s+/u).filter(Boolean) ?? [];
117
+ const query = new Query(input.where ?? {}, { scriptEnabled: false });
118
+ const fields = input.fields === undefined ? undefined : new Set(input.fields);
119
+ const records = [];
120
+ const errors = [];
121
+ let nextIndex = firstPageIndex;
122
+ let lastReadUri;
123
+ while (nextIndex < entries.length && records.length + errors.length < limit) {
124
+ const current = entries[nextIndex];
125
+ if (current === undefined)
126
+ break;
127
+ nextIndex += 1;
128
+ const { entry, uri } = current;
129
+ let stored;
130
+ let record;
131
+ let resultError;
132
+ try {
133
+ stored = await readStoredRecord(join(dataFolder, input.type, entry.name));
134
+ }
135
+ catch (error) {
136
+ resultError = { uri, message: recordFileReadError(error) };
137
+ }
138
+ if (stored !== undefined) {
139
+ if (stored.status !== "found") {
140
+ resultError = {
141
+ uri,
142
+ message: stored.status === "invalid" ? stored.message : "record does not exist",
143
+ };
144
+ }
145
+ else if (stringValuesContainWords(stored.value.value, words)
146
+ && query.test(stored.value.value)) {
147
+ const validationErrors = await schema.validate(stored.value.value);
148
+ if (validationErrors.length > 0) {
149
+ resultError = { uri, message: validationErrors.join("; ") };
150
+ }
151
+ else {
152
+ record = {
153
+ uri,
154
+ createdAt: stored.value.createdAt,
155
+ updatedAt: stored.value.updatedAt,
156
+ value: fields === undefined
157
+ ? stored.value.value
158
+ : Object.fromEntries(Object.entries(stored.value.value).filter(([field]) => fields.has(field))),
159
+ };
160
+ }
161
+ }
162
+ }
163
+ if (record === undefined && resultError === undefined) {
164
+ lastReadUri = uri;
165
+ continue;
166
+ }
167
+ const nextRecords = record === undefined ? records : [...records, record];
168
+ const nextErrors = resultError === undefined ? errors : [...errors, resultError];
169
+ const nextPageBytes = Buffer.byteLength(JSON.stringify({
170
+ records: nextRecords,
171
+ errors: nextErrors,
172
+ }), "utf8");
173
+ if (records.length + errors.length > 0 && nextPageBytes > MAX_FIND_RECORDS_PAGE_BYTES) {
174
+ // Reading the candidate is necessary to know its serialized size, but it belongs to the
175
+ // next page. Leave its index unconsumed so the cursor resumes before it instead of dropping
176
+ // it; only a page's first result may exceed the cap.
177
+ nextIndex -= 1;
178
+ break;
179
+ }
180
+ if (record !== undefined)
181
+ records.push(record);
182
+ if (resultError !== undefined)
183
+ errors.push(resultError);
184
+ lastReadUri = uri;
185
+ if (nextPageBytes >= MAX_FIND_RECORDS_PAGE_BYTES)
186
+ break;
187
+ }
188
+ return {
189
+ records,
190
+ errors,
191
+ ...(lastReadUri !== undefined && nextIndex < entries.length
192
+ ? { cursor: encodeRecordCursor(lastReadUri) }
193
+ : {}),
194
+ };
195
+ }
196
+ async function getRecord(input) {
197
+ const parsed = parseRecordUri(input.uri);
198
+ if (parsed === undefined)
199
+ return refuse(invalidRecordUriMessage);
200
+ const declaration = await readDeclaration(typeFolder, parsed.type);
201
+ if (declaration.status !== "found")
202
+ return declarationRefusal(parsed.type, declaration);
203
+ const stored = await readStoredRecord(recordPath(dataFolder, parsed.type, parsed.key));
204
+ if (stored.status !== "found")
205
+ return recordReadRefusal(input.uri, stored);
206
+ const schema = await preparedDeclarationSchema(declaration.value, typeUri(parsed.type));
207
+ if ("errors" in schema)
208
+ return schema;
209
+ const validationErrors = await schema.validate(stored.value.value);
210
+ const result = {
211
+ uri: input.uri,
212
+ createdAt: stored.value.createdAt,
213
+ updatedAt: stored.value.updatedAt,
214
+ value: stored.value.value,
215
+ };
216
+ if (validationErrors.length > 0)
217
+ result.error = validationErrors.join("; ");
218
+ return result;
219
+ }
220
+ async function createRecord(input, options = {}) {
221
+ return withWriteLock(async () => {
222
+ if (!typeNamePattern.test(input.type))
223
+ return refuse(invalidTypeMessage);
224
+ if (input.key !== undefined && !keyPattern.test(input.key)) {
225
+ return refuse(invalidKeyMessage);
226
+ }
227
+ if (!isJsonObject(input.record))
228
+ return refuse("record: must be a JSON object");
229
+ const declaration = await readDeclaration(typeFolder, input.type);
230
+ if (declaration.status !== "found")
231
+ return declarationRefusal(input.type, declaration);
232
+ const schema = await preparedDeclarationSchema(declaration.value, typeUri(input.type));
233
+ if ("errors" in schema)
234
+ return schema;
235
+ const validationErrors = await schema.validate(input.record);
236
+ if (validationErrors.length > 0) {
237
+ const uri = input.key === undefined ? undefined : recordUri(input.type, input.key);
238
+ return { errors: validationErrors.map((message) => ({ ...(uri === undefined ? {} : { uri }), message })) };
239
+ }
240
+ const sourceUri = input.key === undefined ? undefined : recordUri(input.type, input.key);
241
+ const linkErrors = await missingLinkErrors(dataFolder, typeFolder, schema, input.record, sourceUri);
242
+ if (linkErrors.length > 0)
243
+ return { errors: linkErrors };
244
+ await mkdir(join(dataFolder, input.type), { recursive: true });
245
+ let key = input.key;
246
+ if (key === undefined) {
247
+ do
248
+ key = nextTid();
249
+ while (await pathExists(recordPath(dataFolder, input.type, key)));
250
+ }
251
+ else if (await pathExists(recordPath(dataFolder, input.type, key))) {
252
+ return refuse("a record with that key already exists", recordUri(input.type, key));
253
+ }
254
+ const uri = recordUri(input.type, key);
255
+ const materialized = await prepareRecordBlobs(blobFolder, schema, input.record, options.allowLocalPaths ?? true, uri);
256
+ if ("errors" in materialized)
257
+ return materialized;
258
+ const materializedLinkErrors = await missingLinkErrors(dataFolder, typeFolder, schema, materialized.value, uri);
259
+ if (materializedLinkErrors.length > 0)
260
+ return { errors: materializedLinkErrors };
261
+ const now = new Date().toISOString();
262
+ await writePreparedBlobs(blobFolder, materialized.blobs);
263
+ await writeJsonAtomically(recordPath(dataFolder, input.type, key), {
264
+ createdAt: now,
265
+ updatedAt: now,
266
+ value: materialized.value,
267
+ });
268
+ return { uri };
269
+ });
270
+ }
271
+ async function updateRecord(input, options = {}) {
272
+ return withWriteLock(async () => {
273
+ const parsed = parseRecordUri(input.uri);
274
+ if (parsed === undefined)
275
+ return refuse(invalidRecordUriMessage);
276
+ if (!isJsonObject(input.patch))
277
+ return refuse("patch: must be a JSON object", input.uri);
278
+ const declaration = await readDeclaration(typeFolder, parsed.type);
279
+ if (declaration.status !== "found")
280
+ return declarationRefusal(parsed.type, declaration);
281
+ const path = recordPath(dataFolder, parsed.type, parsed.key);
282
+ const stored = await readStoredRecord(path);
283
+ if (stored.status !== "found")
284
+ return recordReadRefusal(input.uri, stored);
285
+ const value = mergePatch(stored.value.value, input.patch);
286
+ const schema = await preparedDeclarationSchema(declaration.value, typeUri(parsed.type));
287
+ if ("errors" in schema)
288
+ return schema;
289
+ const validationErrors = await schema.validate(value);
290
+ if (validationErrors.length > 0) {
291
+ return { errors: validationErrors.map((message) => ({ uri: input.uri, message })) };
292
+ }
293
+ const linkErrors = await missingLinkErrors(dataFolder, typeFolder, schema, value, input.uri);
294
+ if (linkErrors.length > 0)
295
+ return { errors: linkErrors };
296
+ const materialized = await prepareRecordBlobs(blobFolder, schema, value, options.allowLocalPaths ?? true, input.uri);
297
+ if ("errors" in materialized)
298
+ return materialized;
299
+ const materializedLinkErrors = await missingLinkErrors(dataFolder, typeFolder, schema, materialized.value, input.uri);
300
+ if (materializedLinkErrors.length > 0)
301
+ return { errors: materializedLinkErrors };
302
+ const previousBlobs = await blobsHeldByRecord(stored.value.value, schema);
303
+ await writePreparedBlobs(blobFolder, materialized.blobs);
304
+ await writeJsonAtomically(path, {
305
+ createdAt: stored.value.createdAt,
306
+ updatedAt: new Date().toISOString(),
307
+ value: materialized.value,
308
+ });
309
+ await deleteUnheldBlobs(dataFolder, typeFolder, previousBlobs);
310
+ return { uri: input.uri };
311
+ });
312
+ }
313
+ async function deleteRecord(input) {
314
+ return withWriteLock(async () => {
315
+ const parsed = parseRecordUri(input.uri);
316
+ if (parsed === undefined)
317
+ return refuse(invalidRecordUriMessage);
318
+ const declaration = await readDeclaration(typeFolder, parsed.type);
319
+ if (declaration.status !== "found")
320
+ return declarationRefusal(parsed.type, declaration);
321
+ const path = recordPath(dataFolder, parsed.type, parsed.key);
322
+ if (!await isRegularFile(path))
323
+ return refuse("record does not exist", input.uri);
324
+ const changes = [];
325
+ const errors = [];
326
+ let deletedRecord;
327
+ try {
328
+ const target = await readStoredRecord(path);
329
+ if (target.status === "missing") {
330
+ errors.push({ uri: input.uri, message: "record does not exist" });
331
+ }
332
+ else if (target.status === "invalid") {
333
+ errors.push({ uri: input.uri, message: target.message });
334
+ }
335
+ else {
336
+ deletedRecord = target.value;
337
+ }
338
+ }
339
+ catch (error) {
340
+ errors.push({ uri: input.uri, message: recordFileReadError(error) });
341
+ }
342
+ const typeEntries = (await readDirectory(typeFolder))
343
+ .filter((entry) => (entry.isFile()
344
+ && entry.name.endsWith(".json")
345
+ && typeNamePattern.test(entry.name.slice(0, -".json".length))))
346
+ .sort((left, right) => left.name.localeCompare(right.name));
347
+ for (const typeEntry of typeEntries) {
348
+ const type = typeEntry.name.slice(0, -".json".length);
349
+ const otherDeclaration = await readValidDeclaration(typeFolder, type);
350
+ if (otherDeclaration === undefined)
351
+ continue;
352
+ const schema = await preparedDeclarationSchema(otherDeclaration, typeUri(type));
353
+ if ("errors" in schema)
354
+ continue;
355
+ for (const entry of await recognizedRecordEntries(join(dataFolder, type))) {
356
+ const key = entry.name.slice(0, -".json".length);
357
+ const uri = recordUri(type, key);
358
+ if (uri === input.uri)
359
+ continue;
360
+ let stored;
361
+ try {
362
+ stored = await readStoredRecord(join(dataFolder, type, entry.name));
363
+ }
364
+ catch (error) {
365
+ errors.push({ uri, message: recordFileReadError(error) });
366
+ continue;
367
+ }
368
+ if (stored.status !== "found") {
369
+ errors.push({
370
+ uri,
371
+ message: stored.status === "invalid" ? stored.message : "record does not exist",
372
+ });
373
+ continue;
374
+ }
375
+ const validationErrors = await schema.validate(stored.value.value);
376
+ if (validationErrors.length > 0) {
377
+ if (containsStringValue(stored.value.value, input.uri)) {
378
+ errors.push({
379
+ uri,
380
+ message: `record does not match its type and holds ${input.uri} as a string value`,
381
+ });
382
+ }
383
+ continue;
384
+ }
385
+ const pointers = schema.formatFields(stored.value.value, "uri")
386
+ .filter((field) => field.value === input.uri)
387
+ .map(({ pointer }) => pointer);
388
+ if (pointers.length === 0)
389
+ continue;
390
+ const value = removeFields(stored.value.value, pointers);
391
+ const remainingErrors = await schema.validate(value);
392
+ if (remainingErrors.length > 0) {
393
+ errors.push({ uri, message: remainingErrors.join("; ") });
394
+ continue;
395
+ }
396
+ changes.push({
397
+ path: join(dataFolder, type, entry.name),
398
+ record: {
399
+ createdAt: stored.value.createdAt,
400
+ updatedAt: timestampAfter(stored.value.updatedAt),
401
+ value,
402
+ },
403
+ });
404
+ }
405
+ }
406
+ if (errors.length > 0) {
407
+ errors.sort((left, right) => {
408
+ const uriOrder = (left.uri ?? "").localeCompare(right.uri ?? "");
409
+ return uriOrder === 0 ? left.message.localeCompare(right.message) : uriOrder;
410
+ });
411
+ return { errors };
412
+ }
413
+ const targetSchema = await preparedDeclarationSchema(declaration.value, typeUri(parsed.type));
414
+ const previousBlobs = deletedRecord === undefined
415
+ ? new Set()
416
+ : "errors" in targetSchema
417
+ ? blobUrisIn(deletedRecord.value)
418
+ : await blobsHeldByRecord(deletedRecord.value, targetSchema);
419
+ for (const change of changes)
420
+ await writeJsonAtomically(change.path, change.record);
421
+ await unlink(path);
422
+ await deleteUnheldBlobs(dataFolder, typeFolder, previousBlobs);
423
+ return { uri: input.uri };
424
+ });
425
+ }
426
+ async function listTypes(_input) {
427
+ const entries = await readDirectory(typeFolder);
428
+ const types = [];
429
+ for (const entry of entries) {
430
+ if (!entry.isFile() || !entry.name.endsWith(".json"))
431
+ continue;
432
+ const type = entry.name.slice(0, -".json".length);
433
+ if (!typeNamePattern.test(type))
434
+ continue;
435
+ const declaration = await readValidDeclaration(typeFolder, type);
436
+ if (declaration === undefined)
437
+ continue;
438
+ types.push({ uri: typeUri(type), type, description: declaration.description });
439
+ }
440
+ types.sort((left, right) => left.type.localeCompare(right.type));
441
+ return { types };
442
+ }
443
+ async function getType(input) {
444
+ if (!typeNamePattern.test(input.type))
445
+ return refuse(invalidTypeMessage);
446
+ const declaration = await readDeclaration(typeFolder, input.type);
447
+ if (declaration.status !== "found")
448
+ return declarationRefusal(input.type, declaration);
449
+ const schema = await preparedDeclarationSchema(declaration.value, typeUri(input.type));
450
+ if ("errors" in schema)
451
+ return schema;
452
+ return {
453
+ uri: typeUri(input.type),
454
+ description: declaration.value.description,
455
+ ...(declaration.value.titleField === undefined
456
+ ? {}
457
+ : { titleField: declaration.value.titleField }),
458
+ schema: declaration.value.schema,
459
+ };
460
+ }
461
+ async function createType(input) {
462
+ return withWriteLock(async () => {
463
+ if (!typeNamePattern.test(input.type))
464
+ return refuse(invalidTypeMessage);
465
+ if (typeof input.description !== "string")
466
+ return refuse("description: must be a string");
467
+ if (input.titleField !== undefined && typeof input.titleField !== "string") {
468
+ return refuse("titleField: must be a string");
469
+ }
470
+ if (!isJsonObject(input.schema))
471
+ return refuse("schema: must be a JSON object");
472
+ const declarationPath = join(typeFolder, `${input.type}.json`);
473
+ if (await pathExists(declarationPath)) {
474
+ return refuse("type already exists", typeUri(input.type));
475
+ }
476
+ const schema = await prepareDeclarationSchema(input.schema, typeUri(input.type));
477
+ if ("errors" in schema)
478
+ return schema;
479
+ await Promise.all([
480
+ mkdir(typeFolder, { recursive: true }),
481
+ mkdir(join(dataFolder, input.type), { recursive: true }),
482
+ ]);
483
+ await writeJsonAtomically(declarationPath, {
484
+ description: input.description,
485
+ ...(input.titleField === undefined ? {} : { titleField: input.titleField }),
486
+ schema: input.schema,
487
+ });
488
+ return { uri: typeUri(input.type) };
489
+ });
490
+ }
491
+ async function updateType(input) {
492
+ return withWriteLock(async () => {
493
+ if (!typeNamePattern.test(input.type))
494
+ return refuse(invalidTypeMessage);
495
+ const current = await readDeclaration(typeFolder, input.type);
496
+ if (current.status !== "found")
497
+ return declarationRefusal(input.type, current);
498
+ if (input.description !== undefined && typeof input.description !== "string") {
499
+ return refuse("description: must be a string");
500
+ }
501
+ if (input.titleField !== undefined
502
+ && input.titleField !== null
503
+ && typeof input.titleField !== "string") {
504
+ return refuse("titleField: must be a string or null");
505
+ }
506
+ if (input.schema !== undefined && !isJsonObject(input.schema)) {
507
+ return refuse("schema: must be a JSON object");
508
+ }
509
+ const titleField = input.titleField === null
510
+ ? undefined
511
+ : input.titleField ?? current.value.titleField;
512
+ const declaration = {
513
+ description: input.description ?? current.value.description,
514
+ ...(titleField === undefined ? {} : { titleField }),
515
+ schema: input.schema ?? current.value.schema,
516
+ };
517
+ const schema = await prepareDeclarationSchema(declaration.schema, typeUri(input.type));
518
+ if ("errors" in schema)
519
+ return schema;
520
+ await writeJsonAtomically(join(typeFolder, `${input.type}.json`), declaration);
521
+ const errors = await invalidRecords(dataFolder, input.type, schema);
522
+ return { uri: typeUri(input.type), errors };
523
+ });
524
+ }
525
+ async function deleteType(input) {
526
+ return withWriteLock(async () => {
527
+ if (!typeNamePattern.test(input.type))
528
+ return refuse(invalidTypeMessage);
529
+ const declaration = await readDeclaration(typeFolder, input.type);
530
+ if (declaration.status !== "found")
531
+ return declarationRefusal(input.type, declaration);
532
+ try {
533
+ await rmdir(join(dataFolder, input.type));
534
+ }
535
+ catch (error) {
536
+ if (hasCode(error, "ENOTEMPTY") || hasCode(error, "EEXIST")) {
537
+ return refuse("type folder is not empty", typeUri(input.type));
538
+ }
539
+ if (!hasCode(error, "ENOENT"))
540
+ throw error;
541
+ }
542
+ await unlink(join(typeFolder, `${input.type}.json`));
543
+ return { uri: typeUri(input.type) };
544
+ });
545
+ }
546
+ async function listResources() {
547
+ const declarationEntries = await readDirectory(typeFolder);
548
+ const resources = [];
549
+ for (const entry of declarationEntries) {
550
+ if (!entry.isFile() || !entry.name.endsWith(".json"))
551
+ continue;
552
+ resources.push(...await listTypeResources(entry.name.slice(0, -".json".length)));
553
+ }
554
+ resources.push(...await listResourceFolder("_blobs"));
555
+ resources.sort((left, right) => left.uri < right.uri ? -1 : left.uri > right.uri ? 1 : 0);
556
+ return resources;
557
+ }
558
+ async function getResourceMetadata(uri) {
559
+ const typeMatch = typeUriPattern.exec(uri);
560
+ const type = typeMatch?.[1];
561
+ if (type !== undefined) {
562
+ const declaration = await readValidDeclaration(typeFolder, type);
563
+ return declaration === undefined ? undefined : declarationResource(type, declaration);
564
+ }
565
+ const blobName = BLOB_URI_PATTERN.exec(uri)?.[1];
566
+ if (blobName !== undefined) {
567
+ const size = await regularFileSize(join(blobFolder, blobName), MAX_BLOB_BYTES);
568
+ return size === undefined ? undefined : {
569
+ uri,
570
+ name: `_blobs/${blobName}`,
571
+ mimeType: mimeTypeForBlob(blobName),
572
+ size,
573
+ };
574
+ }
575
+ const record = parseRecordUri(uri);
576
+ if (record === undefined)
577
+ return undefined;
578
+ const [declaration, stored] = await Promise.all([
579
+ readValidDeclaration(typeFolder, record.type),
580
+ readStoredRecord(recordPath(dataFolder, record.type, record.key)),
581
+ ]);
582
+ if (declaration === undefined || stored.status !== "found")
583
+ return undefined;
584
+ return recordResource(record.type, record.key, declaration, stored.value);
585
+ }
586
+ async function listTypeResources(type) {
587
+ if (!typeNamePattern.test(type))
588
+ return [];
589
+ const declaration = await readValidDeclaration(typeFolder, type);
590
+ if (declaration === undefined)
591
+ return [];
592
+ const resources = [declarationResource(type, declaration)];
593
+ const recordEntries = await recognizedRecordEntries(join(dataFolder, type));
594
+ for (const entry of recordEntries) {
595
+ const key = entry.name.slice(0, -".json".length);
596
+ const stored = await readStoredRecord(join(dataFolder, type, entry.name));
597
+ if (stored.status !== "found")
598
+ continue;
599
+ resources.push(recordResource(type, key, declaration, stored.value));
600
+ }
601
+ return resources;
602
+ }
603
+ async function listResourceFolder(folder) {
604
+ if (folder === "_types") {
605
+ const declarationEntries = await readDirectory(typeFolder);
606
+ const resources = [];
607
+ for (const entry of declarationEntries) {
608
+ if (!entry.isFile() || !entry.name.endsWith(".json"))
609
+ continue;
610
+ const type = entry.name.slice(0, -".json".length);
611
+ const resource = await getResourceMetadata(typeUri(type));
612
+ if (resource !== undefined)
613
+ resources.push(resource);
614
+ }
615
+ return resources;
616
+ }
617
+ if (folder === "_blobs") {
618
+ const resources = [];
619
+ for (const entry of await readDirectory(blobFolder)) {
620
+ if (!entry.isFile() || !BLOB_NAME_PATTERN.test(entry.name))
621
+ continue;
622
+ const resource = await getResourceMetadata(blobUri(entry.name));
623
+ if (resource !== undefined)
624
+ resources.push(resource);
625
+ }
626
+ return resources;
627
+ }
628
+ if (!typeNamePattern.test(folder))
629
+ return [];
630
+ const recordPrefix = `autofile:///${folder}/`;
631
+ return (await listTypeResources(folder)).filter(({ uri }) => uri.startsWith(recordPrefix));
632
+ }
633
+ async function readResource(uri) {
634
+ const typeMatch = typeUriPattern.exec(uri);
635
+ const type = typeMatch?.[1];
636
+ if (type !== undefined) {
637
+ const declaration = await readValidDeclarationText(typeFolder, type);
638
+ if (declaration === undefined)
639
+ return undefined;
640
+ return { uri, mimeType: "application/json", text: declaration.text };
641
+ }
642
+ const blobMatch = BLOB_URI_PATTERN.exec(uri);
643
+ const blobName = blobMatch?.[1];
644
+ if (blobName !== undefined) {
645
+ if (!await isDirectory(blobFolder))
646
+ return undefined;
647
+ const bytes = await readRegularFile(join(blobFolder, blobName), MAX_BLOB_BYTES);
648
+ if (bytes === undefined)
649
+ return undefined;
650
+ return { uri, mimeType: mimeTypeForBlob(blobName), blob: bytes.toString("base64") };
651
+ }
652
+ const record = parseRecordUri(uri);
653
+ if (record === undefined)
654
+ return undefined;
655
+ if (await readValidDeclaration(typeFolder, record.type) === undefined)
656
+ return undefined;
657
+ const stored = await readStoredRecordText(recordPath(dataFolder, record.type, record.key));
658
+ if (stored.status !== "found")
659
+ return undefined;
660
+ return { uri, mimeType: "application/json", text: stored.text };
661
+ }
662
+ return {
663
+ findRecords,
664
+ getRecord,
665
+ createRecord,
666
+ updateRecord,
667
+ deleteRecord,
668
+ listTypes,
669
+ getType,
670
+ createType,
671
+ updateType,
672
+ deleteType,
673
+ listResources,
674
+ getResourceMetadata,
675
+ listResourceFolder,
676
+ listTypeResources,
677
+ readResource,
678
+ };
679
+ }
680
+ function declarationResource(type, declaration) {
681
+ return {
682
+ uri: typeUri(type),
683
+ name: `_types/${type}`,
684
+ mimeType: "application/json",
685
+ description: declaration.description,
686
+ };
687
+ }
688
+ function recordResource(type, key, declaration, stored) {
689
+ const title = declaration.titleField === undefined
690
+ ? undefined
691
+ : stored.value[declaration.titleField];
692
+ return {
693
+ uri: recordUri(type, key),
694
+ name: `${type}/${key}`,
695
+ ...(typeof title === "string" ? { title } : {}),
696
+ mimeType: "application/json",
697
+ lastModified: stored.updatedAt,
698
+ };
699
+ }
700
+ async function prepareDeclarationSchema(schema, uri) {
701
+ try {
702
+ return await prepareSchema(schema);
703
+ }
704
+ catch (error) {
705
+ if (error instanceof SchemaRefusal) {
706
+ return { errors: error.messages.map((message) => ({ uri, message })) };
707
+ }
708
+ throw error;
709
+ }
710
+ }
711
+ async function preparedDeclarationSchema(declaration, uri) {
712
+ return prepareDeclarationSchema(declaration.schema, uri);
713
+ }
714
+ async function missingLinkErrors(dataFolder, typeFolder, schema, value, sourceUri) {
715
+ const errors = [];
716
+ const existence = new Map();
717
+ const checkedFields = new Set();
718
+ for (const field of schema.formatFields(value, "uri")) {
719
+ if (typeof field.value !== "string" || !/^autofile:/iu.test(field.value))
720
+ continue;
721
+ const fieldKey = `${field.pointer}\0${field.value}`;
722
+ if (checkedFields.has(fieldKey))
723
+ continue;
724
+ checkedFields.add(fieldKey);
725
+ let exists = existence.get(field.value);
726
+ if (exists === undefined) {
727
+ exists = await recordExists(dataFolder, typeFolder, field.value);
728
+ existence.set(field.value, exists);
729
+ }
730
+ if (exists)
731
+ continue;
732
+ errors.push({
733
+ ...(sourceUri === undefined ? {} : { uri: sourceUri }),
734
+ message: `${field.location}: links to a record that does not exist: ${field.value}`,
735
+ });
736
+ }
737
+ return errors;
738
+ }
739
+ async function prepareRecordBlobs(blobFolder, schema, value, allowLocalPaths, uri) {
740
+ const materialized = structuredClone(value);
741
+ const preparedBlobs = new Map();
742
+ const errors = [];
743
+ const visitedPointers = new Set();
744
+ for (const field of schema.formatFields(value, "blob")) {
745
+ if (visitedPointers.has(field.pointer))
746
+ continue;
747
+ visitedPointers.add(field.pointer);
748
+ if (typeof field.value !== "string")
749
+ continue;
750
+ const prepared = await prepareBlob(field.value, blobFolder, allowLocalPaths);
751
+ if ("message" in prepared) {
752
+ errors.push(formatFieldError(uri, field.location, prepared.message));
753
+ continue;
754
+ }
755
+ setValueAtPointer(materialized, field.pointer, prepared.uri);
756
+ preparedBlobs.set(prepared.uri, prepared);
757
+ }
758
+ if (errors.length > 0)
759
+ return { errors };
760
+ const validationErrors = await schema.validate(materialized);
761
+ if (validationErrors.length > 0) {
762
+ return { errors: validationErrors.map((message) => ({ uri, message })) };
763
+ }
764
+ const finalBlobs = new Map();
765
+ const finalPointers = new Set();
766
+ for (const field of schema.formatFields(materialized, "blob")) {
767
+ if (finalPointers.has(field.pointer))
768
+ continue;
769
+ finalPointers.add(field.pointer);
770
+ const fieldValue = typeof field.value === "string" ? field.value : undefined;
771
+ if (fieldValue === undefined || blobNameFromUri(fieldValue) === undefined) {
772
+ const message = "must hold a blob URI after storing";
773
+ errors.push(formatFieldError(uri, field.location, message));
774
+ continue;
775
+ }
776
+ let prepared = preparedBlobs.get(fieldValue);
777
+ if (prepared === undefined) {
778
+ const existing = await prepareBlob(fieldValue, blobFolder, false);
779
+ if ("message" in existing) {
780
+ errors.push(formatFieldError(uri, field.location, existing.message));
781
+ continue;
782
+ }
783
+ prepared = existing;
784
+ }
785
+ finalBlobs.set(fieldValue, prepared);
786
+ }
787
+ if (errors.length > 0)
788
+ return { errors };
789
+ return { value: materialized, blobs: [...finalBlobs.values()] };
790
+ }
791
+ function formatFieldError(uri, location, message) {
792
+ return { uri, message: location === "" ? message : `${location}: ${message}` };
793
+ }
794
+ async function blobsHeldByRecord(value, schema) {
795
+ const validationErrors = await schema.validate(value);
796
+ if (validationErrors.length > 0)
797
+ return blobUrisIn(value);
798
+ return new Set(schema.formatFields(value, "blob").flatMap(({ value: fieldValue }) => (typeof fieldValue === "string" && blobNameFromUri(fieldValue) !== undefined
799
+ ? [fieldValue]
800
+ : [])));
801
+ }
802
+ async function deleteUnheldBlobs(dataFolder, typeFolder, candidates) {
803
+ if (candidates.size === 0)
804
+ return;
805
+ const held = await blobsHeldInStore(dataFolder, typeFolder);
806
+ if (held === undefined)
807
+ return;
808
+ await deleteBlobUris(join(dataFolder, "_blobs"), [...candidates].filter((uri) => !held.has(uri)));
809
+ }
810
+ async function blobsHeldInStore(dataFolder, typeFolder) {
811
+ const held = new Set();
812
+ let typeEntries;
813
+ try {
814
+ typeEntries = (await readDirectory(typeFolder)).filter((entry) => (entry.isFile() && entry.name.endsWith(".json")));
815
+ }
816
+ catch (error) {
817
+ if (hasErrorCode(error))
818
+ return undefined;
819
+ throw error;
820
+ }
821
+ for (const typeEntry of typeEntries) {
822
+ const type = typeEntry.name.slice(0, -".json".length);
823
+ if (!typeNamePattern.test(type))
824
+ continue;
825
+ let declaration;
826
+ try {
827
+ declaration = await readDeclaration(typeFolder, type);
828
+ }
829
+ catch (error) {
830
+ if (!hasErrorCode(error))
831
+ throw error;
832
+ declaration = { status: "invalid", message: "type declaration cannot be read" };
833
+ }
834
+ let schema;
835
+ if (declaration.status === "found") {
836
+ const prepared = await preparedDeclarationSchema(declaration.value, typeUri(type));
837
+ if (!("errors" in prepared))
838
+ schema = prepared;
839
+ }
840
+ let recordEntries;
841
+ try {
842
+ recordEntries = await recognizedRecordEntries(join(dataFolder, type));
843
+ }
844
+ catch (error) {
845
+ if (hasErrorCode(error))
846
+ return undefined;
847
+ throw error;
848
+ }
849
+ for (const entry of recordEntries) {
850
+ let record;
851
+ try {
852
+ record = await readJsonText(join(dataFolder, type, entry.name), "record file");
853
+ }
854
+ catch (error) {
855
+ if (hasErrorCode(error))
856
+ return undefined;
857
+ throw error;
858
+ }
859
+ if (record.status === "invalid")
860
+ return undefined;
861
+ if (record.status === "missing")
862
+ continue;
863
+ if (schema !== undefined && isStoredRecord(record.value)) {
864
+ for (const uri of await blobsHeldByRecord(record.value.value, schema))
865
+ held.add(uri);
866
+ }
867
+ else {
868
+ for (const uri of blobUrisIn(record.value))
869
+ held.add(uri);
870
+ }
871
+ }
872
+ }
873
+ return held;
874
+ }
875
+ function blobUrisIn(value) {
876
+ const uris = new Set();
877
+ collectBlobUris(value, uris);
878
+ return uris;
879
+ }
880
+ function collectBlobUris(value, uris) {
881
+ if (typeof value === "string") {
882
+ if (blobNameFromUri(value) !== undefined)
883
+ uris.add(value);
884
+ }
885
+ else if (Array.isArray(value)) {
886
+ for (const item of value)
887
+ collectBlobUris(item, uris);
888
+ }
889
+ else if (typeof value === "object" && value !== null) {
890
+ for (const item of Object.values(value))
891
+ collectBlobUris(item, uris);
892
+ }
893
+ }
894
+ function setValueAtPointer(value, pointer, replacement) {
895
+ const path = parseJsonPointer(pointer);
896
+ const field = path.at(-1);
897
+ if (field === undefined)
898
+ return;
899
+ let parent = value;
900
+ for (const part of path.slice(0, -1)) {
901
+ let next;
902
+ if (Array.isArray(parent))
903
+ next = parent[Number(part)];
904
+ else if (isJsonObject(parent))
905
+ next = parent[part];
906
+ if (next === undefined)
907
+ return;
908
+ parent = next;
909
+ }
910
+ if (Array.isArray(parent))
911
+ parent[Number(field)] = replacement;
912
+ else if (isJsonObject(parent)) {
913
+ Object.defineProperty(parent, field, {
914
+ value: replacement,
915
+ enumerable: true,
916
+ configurable: true,
917
+ writable: true,
918
+ });
919
+ }
920
+ }
921
+ async function recordExists(dataFolder, typeFolder, uri) {
922
+ const parsed = parseRecordUri(uri);
923
+ if (parsed === undefined)
924
+ return false;
925
+ if (await readValidDeclaration(typeFolder, parsed.type) === undefined)
926
+ return false;
927
+ const stored = await readStoredRecord(recordPath(dataFolder, parsed.type, parsed.key));
928
+ return stored.status === "found";
929
+ }
930
+ async function readValidDeclaration(typeFolder, type) {
931
+ return (await readValidDeclarationText(typeFolder, type))?.value;
932
+ }
933
+ async function readValidDeclarationText(typeFolder, type) {
934
+ const result = await readDeclarationText(typeFolder, type);
935
+ if (result.status !== "found")
936
+ return undefined;
937
+ try {
938
+ await prepareSchema(result.value.schema);
939
+ }
940
+ catch (error) {
941
+ if (error instanceof SchemaRefusal)
942
+ return undefined;
943
+ throw error;
944
+ }
945
+ return { value: result.value, text: result.text };
946
+ }
947
+ async function invalidRecords(dataFolder, type, schema) {
948
+ const errors = [];
949
+ const entries = await recognizedRecordEntries(join(dataFolder, type));
950
+ for (const entry of entries) {
951
+ const key = entry.name.slice(0, -".json".length);
952
+ const uri = recordUri(type, key);
953
+ const record = await readStoredRecord(join(dataFolder, type, entry.name));
954
+ if (record.status !== "found") {
955
+ errors.push({ uri, message: record.status === "invalid" ? record.message : "record does not exist" });
956
+ continue;
957
+ }
958
+ const validationErrors = await schema.validate(record.value.value);
959
+ if (validationErrors.length > 0)
960
+ errors.push({ uri, message: validationErrors.join("; ") });
961
+ }
962
+ return errors;
963
+ }
964
+ async function readDeclaration(folder, type) {
965
+ const result = await readDeclarationText(folder, type);
966
+ if (result.status !== "found")
967
+ return result;
968
+ return { status: "found", value: result.value };
969
+ }
970
+ async function readDeclarationText(folder, type) {
971
+ if (!await isDirectory(folder))
972
+ return { status: "missing" };
973
+ const result = await readJsonText(join(folder, `${type}.json`), "type declaration");
974
+ if (result.status !== "found")
975
+ return result;
976
+ if (!isTypeDeclaration(result.value)) {
977
+ return { status: "invalid", message: "type declaration is not valid" };
978
+ }
979
+ return { status: "found", value: result.value, text: result.text };
980
+ }
981
+ async function readStoredRecord(path) {
982
+ const result = await readStoredRecordText(path);
983
+ if (result.status !== "found")
984
+ return result;
985
+ return { status: "found", value: result.value };
986
+ }
987
+ async function readStoredRecordText(path) {
988
+ if (!await isDirectory(dirname(path)))
989
+ return { status: "missing" };
990
+ const result = await readJsonText(path, "record file");
991
+ if (result.status !== "found")
992
+ return result;
993
+ if (!isStoredRecord(result.value)) {
994
+ return { status: "invalid", message: "record file does not contain a valid record" };
995
+ }
996
+ return { status: "found", value: result.value, text: result.text };
997
+ }
998
+ async function readJsonText(path, label) {
999
+ const bytes = await readRegularFile(path);
1000
+ if (bytes === undefined)
1001
+ return { status: "missing" };
1002
+ let text;
1003
+ try {
1004
+ text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
1005
+ }
1006
+ catch {
1007
+ return { status: "invalid", message: `${label} is not valid UTF-8` };
1008
+ }
1009
+ try {
1010
+ return { status: "found", value: JSON.parse(text), text };
1011
+ }
1012
+ catch {
1013
+ return { status: "invalid", message: `${label} cannot be read as JSON` };
1014
+ }
1015
+ }
1016
+ async function writeJsonAtomically(path, value) {
1017
+ const temporaryPath = join(dirname(path), `.${path.slice(path.lastIndexOf("/") + 1)}.${process.pid}.${randomUUID()}`);
1018
+ const text = `${JSON.stringify(value, null, 2)}\n`;
1019
+ try {
1020
+ await writeFile(temporaryPath, text, { encoding: "utf8", flag: "wx" });
1021
+ await rename(temporaryPath, path);
1022
+ }
1023
+ catch (error) {
1024
+ try {
1025
+ await unlink(temporaryPath);
1026
+ }
1027
+ catch (cleanupError) {
1028
+ if (!hasCode(cleanupError, "ENOENT"))
1029
+ throw cleanupError;
1030
+ }
1031
+ throw error;
1032
+ }
1033
+ }
1034
+ function mergePatch(target, patch) {
1035
+ const result = { ...target };
1036
+ for (const [key, patchValue] of Object.entries(patch)) {
1037
+ if (patchValue === null) {
1038
+ delete result[key];
1039
+ }
1040
+ else if (isJsonObject(patchValue)) {
1041
+ Object.defineProperty(result, key, {
1042
+ value: mergePatch(isJsonObject(result[key]) ? result[key] : {}, patchValue),
1043
+ enumerable: true,
1044
+ configurable: true,
1045
+ writable: true,
1046
+ });
1047
+ }
1048
+ else {
1049
+ Object.defineProperty(result, key, {
1050
+ value: patchValue,
1051
+ enumerable: true,
1052
+ configurable: true,
1053
+ writable: true,
1054
+ });
1055
+ }
1056
+ }
1057
+ return result;
1058
+ }
1059
+ function nextTid() {
1060
+ const now = BigInt(Date.now()) * 1000n;
1061
+ lastTidTimestamp = now > lastTidTimestamp ? now : lastTidTimestamp + 1n;
1062
+ let value = (lastTidTimestamp << 10n) | clockIdentifier;
1063
+ let tid = "";
1064
+ for (let index = 0; index < 13; index += 1) {
1065
+ tid = `${tidAlphabet[Number(value & 31n)]}${tid}`;
1066
+ value >>= 5n;
1067
+ }
1068
+ return tid;
1069
+ }
1070
+ /** Returns whether a string is the canonical URI of an Autofile resource. */
1071
+ export function isResourceUri(uri) {
1072
+ return parseRecordUri(uri) !== undefined
1073
+ || typeUriPattern.test(uri)
1074
+ || BLOB_URI_PATTERN.test(uri);
1075
+ }
1076
+ function parseRecordUri(uri) {
1077
+ const match = recordUriPattern.exec(uri);
1078
+ if (match === null || match[1] === undefined || match[2] === undefined)
1079
+ return undefined;
1080
+ return { type: match[1], key: match[2] };
1081
+ }
1082
+ function recordUri(type, key) {
1083
+ return `autofile:///${type}/${key}`;
1084
+ }
1085
+ function typeUri(type) {
1086
+ return `autofile:///_types/${type}`;
1087
+ }
1088
+ function mimeTypeForBlob(name) {
1089
+ return lookupMimeType(name) || "application/octet-stream";
1090
+ }
1091
+ function recordPath(dataFolder, type, key) {
1092
+ return join(dataFolder, type, `${key}.json`);
1093
+ }
1094
+ function encodeRecordCursor(uri) {
1095
+ return Buffer.from(uri, "utf8").toString("base64url");
1096
+ }
1097
+ function decodeRecordCursor(cursor, type) {
1098
+ if (!/^[A-Za-z0-9_-]+$/u.test(cursor))
1099
+ return undefined;
1100
+ const bytes = Buffer.from(cursor, "base64url");
1101
+ if (bytes.toString("base64url") !== cursor)
1102
+ return undefined;
1103
+ let uri;
1104
+ try {
1105
+ uri = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
1106
+ }
1107
+ catch {
1108
+ return undefined;
1109
+ }
1110
+ const record = parseRecordUri(uri);
1111
+ return record?.type === type ? uri : undefined;
1112
+ }
1113
+ function findWhereRefusal(value) {
1114
+ if (Array.isArray(value)) {
1115
+ for (const item of value) {
1116
+ const refusal = findWhereRefusal(item);
1117
+ if (refusal !== undefined)
1118
+ return refusal;
1119
+ }
1120
+ return undefined;
1121
+ }
1122
+ if (!isJsonObject(value))
1123
+ return undefined;
1124
+ for (const [key, item] of Object.entries(value)) {
1125
+ if (key.startsWith("$"))
1126
+ return `where: the operator ${key} is not supported`;
1127
+ const unsafeKey = key.split(".").find((part) => (part === "__proto__" || part === "constructor" || part === "prototype"));
1128
+ if (unsafeKey !== undefined)
1129
+ return `where: the key ${unsafeKey} is not supported`;
1130
+ const refusal = findWhereRefusal(item);
1131
+ if (refusal !== undefined)
1132
+ return refusal;
1133
+ }
1134
+ return undefined;
1135
+ }
1136
+ function recordFileReadError(error) {
1137
+ const code = error instanceof Error && "code" in error && typeof error.code === "string"
1138
+ ? `: ${error.code}`
1139
+ : "";
1140
+ return `record file cannot be read${code}`;
1141
+ }
1142
+ function stringValuesContainWords(value, words) {
1143
+ if (words.length === 0)
1144
+ return true;
1145
+ const strings = [];
1146
+ collectStringValues(value, strings);
1147
+ const normalized = strings.map((item) => item.toLowerCase());
1148
+ return words.every((word) => normalized.some((item) => item.includes(word)));
1149
+ }
1150
+ function collectStringValues(value, strings) {
1151
+ if (typeof value === "string") {
1152
+ strings.push(value);
1153
+ }
1154
+ else if (Array.isArray(value)) {
1155
+ for (const item of value)
1156
+ collectStringValues(item, strings);
1157
+ }
1158
+ else if (isJsonObject(value)) {
1159
+ for (const item of Object.values(value))
1160
+ collectStringValues(item, strings);
1161
+ }
1162
+ }
1163
+ function containsStringValue(value, expected) {
1164
+ if (typeof value === "string")
1165
+ return value === expected;
1166
+ if (Array.isArray(value))
1167
+ return value.some((item) => containsStringValue(item, expected));
1168
+ return isJsonObject(value)
1169
+ && Object.values(value).some((item) => containsStringValue(item, expected));
1170
+ }
1171
+ function removeFields(value, pointers) {
1172
+ const result = structuredClone(value);
1173
+ const paths = [...new Set(pointers)].map(parseJsonPointer);
1174
+ paths.sort((left, right) => {
1175
+ const leftParent = left.slice(0, -1).join("\0");
1176
+ const rightParent = right.slice(0, -1).join("\0");
1177
+ if (leftParent === rightParent) {
1178
+ const leftIndex = Number(left.at(-1));
1179
+ const rightIndex = Number(right.at(-1));
1180
+ if (Number.isInteger(leftIndex) && Number.isInteger(rightIndex)) {
1181
+ return rightIndex - leftIndex;
1182
+ }
1183
+ }
1184
+ return right.length - left.length;
1185
+ });
1186
+ for (const path of paths) {
1187
+ const field = path.at(-1);
1188
+ if (field === undefined)
1189
+ continue;
1190
+ let parent = result;
1191
+ let parentFound = true;
1192
+ for (const part of path.slice(0, -1)) {
1193
+ let next;
1194
+ if (Array.isArray(parent))
1195
+ next = parent[Number(part)];
1196
+ else if (isJsonObject(parent))
1197
+ next = parent[part];
1198
+ if (next === undefined) {
1199
+ parentFound = false;
1200
+ break;
1201
+ }
1202
+ parent = next;
1203
+ }
1204
+ if (!parentFound)
1205
+ continue;
1206
+ if (Array.isArray(parent))
1207
+ parent.splice(Number(field), 1);
1208
+ else if (isJsonObject(parent))
1209
+ delete parent[field];
1210
+ }
1211
+ return result;
1212
+ }
1213
+ function parseJsonPointer(pointer) {
1214
+ if (pointer === "")
1215
+ return [];
1216
+ return pointer.slice(1).split("/").map((part) => (part.replaceAll("~1", "/").replaceAll("~0", "~")));
1217
+ }
1218
+ function timestampAfter(previous) {
1219
+ return new Date(Math.max(Date.now(), new Date(previous).valueOf() + 1)).toISOString();
1220
+ }
1221
+ function refuse(message, uri) {
1222
+ return { errors: [{ ...(uri === undefined ? {} : { uri }), message }] };
1223
+ }
1224
+ function declarationRefusal(type, result) {
1225
+ const uri = typeUri(type);
1226
+ if (result.status === "missing")
1227
+ return refuse("type does not exist", uri);
1228
+ if (result.status === "invalid")
1229
+ return refuse(result.message, uri);
1230
+ throw new Error("A found declaration is not a refusal");
1231
+ }
1232
+ function recordReadRefusal(uri, result) {
1233
+ if (result.status === "missing")
1234
+ return refuse("record does not exist", uri);
1235
+ if (result.status === "invalid")
1236
+ return refuse(result.message, uri);
1237
+ throw new Error("A found record is not a refusal");
1238
+ }
1239
+ async function recognizedRecordEntries(folder) {
1240
+ return (await readDirectory(folder))
1241
+ .filter((entry) => {
1242
+ if (!entry.isFile() || !entry.name.endsWith(".json"))
1243
+ return false;
1244
+ return keyPattern.test(entry.name.slice(0, -".json".length));
1245
+ })
1246
+ .sort((left, right) => left.name.localeCompare(right.name));
1247
+ }
1248
+ async function readDirectory(path) {
1249
+ try {
1250
+ if (!(await lstat(path)).isDirectory())
1251
+ return [];
1252
+ return await readdir(path, { withFileTypes: true });
1253
+ }
1254
+ catch (error) {
1255
+ if (hasCode(error, "ENOENT") || hasCode(error, "ENOTDIR"))
1256
+ return [];
1257
+ throw error;
1258
+ }
1259
+ }
1260
+ async function isDirectory(path) {
1261
+ try {
1262
+ return (await lstat(path)).isDirectory();
1263
+ }
1264
+ catch (error) {
1265
+ if (hasCode(error, "ENOENT") || hasCode(error, "ENOTDIR"))
1266
+ return false;
1267
+ throw error;
1268
+ }
1269
+ }
1270
+ async function isRegularFile(path) {
1271
+ try {
1272
+ return (await lstat(path)).isFile();
1273
+ }
1274
+ catch (error) {
1275
+ if (hasCode(error, "ENOENT"))
1276
+ return false;
1277
+ throw error;
1278
+ }
1279
+ }
1280
+ async function openRegularFile(path) {
1281
+ let handle;
1282
+ try {
1283
+ handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
1284
+ }
1285
+ catch (error) {
1286
+ if (hasCode(error, "ENOENT")
1287
+ || hasCode(error, "ENOTDIR")
1288
+ || hasCode(error, "ELOOP")
1289
+ || hasCode(error, "ENXIO")
1290
+ || hasCode(error, "ENODEV")) {
1291
+ return undefined;
1292
+ }
1293
+ throw error;
1294
+ }
1295
+ try {
1296
+ const details = await handle.stat();
1297
+ if (!details.isFile()) {
1298
+ await handle.close();
1299
+ return undefined;
1300
+ }
1301
+ return { handle, size: details.size };
1302
+ }
1303
+ catch (error) {
1304
+ await handle.close();
1305
+ throw error;
1306
+ }
1307
+ }
1308
+ async function regularFileSize(path, maximumBytes) {
1309
+ const file = await openRegularFile(path);
1310
+ if (file === undefined)
1311
+ return undefined;
1312
+ try {
1313
+ return file.size <= maximumBytes ? file.size : undefined;
1314
+ }
1315
+ finally {
1316
+ await file.handle.close();
1317
+ }
1318
+ }
1319
+ async function readRegularFile(path, maximumBytes = Number.POSITIVE_INFINITY) {
1320
+ const file = await openRegularFile(path);
1321
+ if (file === undefined)
1322
+ return undefined;
1323
+ try {
1324
+ if (file.size > maximumBytes)
1325
+ return undefined;
1326
+ const bytes = await file.handle.readFile();
1327
+ return bytes.byteLength <= maximumBytes ? bytes : undefined;
1328
+ }
1329
+ finally {
1330
+ await file.handle.close();
1331
+ }
1332
+ }
1333
+ async function pathExists(path) {
1334
+ try {
1335
+ await lstat(path);
1336
+ return true;
1337
+ }
1338
+ catch (error) {
1339
+ if (hasCode(error, "ENOENT"))
1340
+ return false;
1341
+ throw error;
1342
+ }
1343
+ }
1344
+ function isTypeDeclaration(value) {
1345
+ if (!isJsonObject(value))
1346
+ return false;
1347
+ if (Object.keys(value).some((key) => !["description", "titleField", "schema"].includes(key))) {
1348
+ return false;
1349
+ }
1350
+ if (typeof value.description !== "string" || !isJsonObject(value.schema))
1351
+ return false;
1352
+ return value.titleField === undefined || typeof value.titleField === "string";
1353
+ }
1354
+ function isStoredRecord(value) {
1355
+ if (!isJsonObject(value))
1356
+ return false;
1357
+ if (Object.keys(value).length !== 3)
1358
+ return false;
1359
+ if (!["createdAt", "updatedAt", "value"].every((key) => Object.hasOwn(value, key)))
1360
+ return false;
1361
+ return isTimestamp(value.createdAt) && isTimestamp(value.updatedAt) && isJsonObject(value.value);
1362
+ }
1363
+ function isTimestamp(value) {
1364
+ if (typeof value !== "string" || !timestampPattern.test(value))
1365
+ return false;
1366
+ const parsed = new Date(value);
1367
+ if (Number.isNaN(parsed.valueOf()))
1368
+ return false;
1369
+ const canonical = parsed.toISOString();
1370
+ return value === canonical || value === canonical.replace(".000Z", "Z");
1371
+ }
1372
+ function isJsonObject(value) {
1373
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1374
+ }
1375
+ function hasCode(error, code) {
1376
+ return hasErrorCode(error) && error.code === code;
1377
+ }
1378
+ function hasErrorCode(error) {
1379
+ return error instanceof Error && "code" in error && typeof error.code === "string";
1380
+ }
1381
+ //# sourceMappingURL=store.js.map