@datafn/core 0.0.1 → 0.0.2

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/index.cjs CHANGED
@@ -1,5 +1,98 @@
1
1
  'use strict';
2
2
 
3
+ // src/types.ts
4
+ function defineSchema(schema) {
5
+ return schema;
6
+ }
7
+
8
+ // src/capabilities.ts
9
+ var CAPABILITY_FIELD_DEFS = {
10
+ timestamps: [
11
+ { name: "createdAt", type: "date", required: false, nullable: true, readonly: true, default: null },
12
+ { name: "updatedAt", type: "date", required: false, nullable: true, readonly: true, default: null }
13
+ ],
14
+ audit: [
15
+ { name: "createdBy", type: "string", required: false, nullable: true, readonly: true, default: null },
16
+ { name: "updatedBy", type: "string", required: false, nullable: true, readonly: true, default: null }
17
+ ],
18
+ trash: [
19
+ { name: "trashedAt", type: "date", required: false, nullable: true, readonly: true, default: null },
20
+ { name: "trashedBy", type: "string", required: false, nullable: true, readonly: true, default: null }
21
+ ],
22
+ archivable: [{ name: "isArchived", type: "boolean", required: false, nullable: false, readonly: false, default: false }],
23
+ shareable: [{ name: "visibility", type: "string", required: false, nullable: true, readonly: true, default: null }]
24
+ };
25
+ var RELATION_CAPABILITY_FIELD_DEFS = {
26
+ timestamps: [
27
+ { name: "createdAt", type: "date", required: false, nullable: true, readonly: true, default: null },
28
+ { name: "updatedAt", type: "date", required: false, nullable: true, readonly: true, default: null }
29
+ ],
30
+ audit: [
31
+ { name: "createdBy", type: "string", required: false, nullable: true, readonly: true, default: null },
32
+ { name: "updatedBy", type: "string", required: false, nullable: true, readonly: true, default: null }
33
+ ]
34
+ };
35
+ function getRelationCapabilityFieldNames(caps) {
36
+ const names = [];
37
+ const seen = /* @__PURE__ */ new Set();
38
+ for (const cap of caps) {
39
+ const defs = RELATION_CAPABILITY_FIELD_DEFS[cap] ?? [];
40
+ for (const def of defs) {
41
+ if (!seen.has(def.name)) {
42
+ seen.add(def.name);
43
+ names.push(def.name);
44
+ }
45
+ }
46
+ }
47
+ return names;
48
+ }
49
+ function isObject(value) {
50
+ return typeof value === "object" && value !== null && !Array.isArray(value);
51
+ }
52
+ function toCapabilityKey(entry) {
53
+ return typeof entry === "string" ? entry : "shareable";
54
+ }
55
+ function dedupeCapabilities(entries) {
56
+ const seen = /* @__PURE__ */ new Set();
57
+ const deduped = [];
58
+ for (const entry of entries) {
59
+ const key = toCapabilityKey(entry);
60
+ if (seen.has(key)) continue;
61
+ seen.add(key);
62
+ deduped.push(entry);
63
+ }
64
+ return deduped;
65
+ }
66
+ function resolveCapabilities(globalCaps, resourceCaps) {
67
+ const base = Array.isArray(globalCaps) ? globalCaps : [];
68
+ if (resourceCaps === void 0) {
69
+ return dedupeCapabilities(base);
70
+ }
71
+ if (Array.isArray(resourceCaps)) {
72
+ return dedupeCapabilities([...base, ...resourceCaps]);
73
+ }
74
+ if (isObject(resourceCaps) && Array.isArray(resourceCaps.exclude)) {
75
+ const excluded = new Set(resourceCaps.exclude);
76
+ return dedupeCapabilities(
77
+ base.filter((entry) => typeof entry === "string" ? !excluded.has(entry) : true)
78
+ );
79
+ }
80
+ return dedupeCapabilities(base);
81
+ }
82
+ function getCapabilityFields(resolvedCaps) {
83
+ const fields = [];
84
+ const seenFields = /* @__PURE__ */ new Set();
85
+ for (const capability of dedupeCapabilities(resolvedCaps)) {
86
+ const key = toCapabilityKey(capability);
87
+ for (const fieldDef of CAPABILITY_FIELD_DEFS[key]) {
88
+ if (seenFields.has(fieldDef.name)) continue;
89
+ seenFields.add(fieldDef.name);
90
+ fields.push({ ...fieldDef });
91
+ }
92
+ }
93
+ return fields;
94
+ }
95
+
3
96
  // src/errors.ts
4
97
  function ok(result) {
5
98
  return { ok: true, result };
@@ -16,6 +109,51 @@ function err(code, message, details) {
16
109
  }
17
110
 
18
111
  // src/schema.ts
112
+ var SIMPLE_CAPABILITIES = /* @__PURE__ */ new Set([
113
+ "timestamps",
114
+ "audit",
115
+ "trash",
116
+ "archivable"
117
+ ]);
118
+ var SHAREABLE_LEVELS = /* @__PURE__ */ new Set(["viewer", "editor", "owner"]);
119
+ var SHAREABLE_DEFAULTS = /* @__PURE__ */ new Set(["private", "shared"]);
120
+ var SHAREABLE_VISIBILITY_DEFAULTS = /* @__PURE__ */ new Set(["ns", "private", "shared"]);
121
+ var SHAREABLE_PRINCIPAL_MODES = /* @__PURE__ */ new Set(["opaque-id"]);
122
+ var RELATION_SIMPLE_CAPABILITIES = /* @__PURE__ */ new Set([
123
+ "timestamps",
124
+ "audit"
125
+ ]);
126
+ var RELATION_CAPABILITY_CANONICAL_ORDER = ["timestamps", "audit"];
127
+ function resolveRelationCapabilities(capabilities) {
128
+ if (capabilities === void 0) return ok([]);
129
+ if (!Array.isArray(capabilities)) {
130
+ return err("SCHEMA_INVALID", "Relation capabilities must be an array", {
131
+ path: "capabilities"
132
+ });
133
+ }
134
+ for (const entry of capabilities) {
135
+ if (typeof entry !== "string") {
136
+ return err("SCHEMA_INVALID", "Relation capability values must be strings", {
137
+ path: "capabilities"
138
+ });
139
+ }
140
+ if (!RELATION_SIMPLE_CAPABILITIES.has(entry)) {
141
+ return err("INVALID_CAPABILITY", `Unknown relation capability "${entry}"`, {
142
+ path: "capabilities"
143
+ });
144
+ }
145
+ }
146
+ const declared = new Set(capabilities);
147
+ return ok(RELATION_CAPABILITY_CANONICAL_ORDER.filter((cap) => declared.has(cap)));
148
+ }
149
+ function resolveNamespaced(s) {
150
+ if (typeof s.namespaced === "boolean") return s.namespaced;
151
+ return true;
152
+ }
153
+ function isNamespaced(schema) {
154
+ if (typeof schema.namespaced === "boolean") return schema.namespaced;
155
+ return true;
156
+ }
19
157
  function validateSchema(schema) {
20
158
  if (typeof schema !== "object" || schema === null || Array.isArray(schema)) {
21
159
  return err("SCHEMA_INVALID", "Invalid schema: expected object", {
@@ -23,11 +161,20 @@ function validateSchema(schema) {
23
161
  });
24
162
  }
25
163
  const s = schema;
164
+ const globalCapsResult = parseSchemaCapabilities(s.capabilities);
165
+ if (!globalCapsResult.ok) return globalCapsResult;
166
+ const globalCapabilities = globalCapsResult.result;
26
167
  if (!s.resources || !Array.isArray(s.resources)) {
27
168
  return err("SCHEMA_INVALID", "Invalid schema: missing resources", {
28
169
  path: "resources"
29
170
  });
30
171
  }
172
+ const MAX_RESOURCES = 100;
173
+ if (s.resources.length > MAX_RESOURCES) {
174
+ return err("SCHEMA_INVALID", `Invalid schema: too many resources (max ${MAX_RESOURCES})`, {
175
+ path: "resources"
176
+ });
177
+ }
31
178
  const resourceNames = /* @__PURE__ */ new Set();
32
179
  const normalizedResources = [];
33
180
  for (const resource of s.resources) {
@@ -66,6 +213,14 @@ function validateSchema(schema) {
66
213
  { path: "resources" }
67
214
  );
68
215
  }
216
+ const MAX_FIELDS_PER_RESOURCE = 200;
217
+ if (r.fields.length > MAX_FIELDS_PER_RESOURCE) {
218
+ return err(
219
+ "SCHEMA_INVALID",
220
+ `Invalid schema: resource "${r.name}" has too many fields (max ${MAX_FIELDS_PER_RESOURCE})`,
221
+ { path: `resources.${r.name}.fields` }
222
+ );
223
+ }
69
224
  const fieldNames = /* @__PURE__ */ new Set();
70
225
  for (const field of r.fields) {
71
226
  if (typeof field !== "object" || field === null || Array.isArray(field)) {
@@ -90,6 +245,31 @@ function validateSchema(schema) {
90
245
  }
91
246
  fieldNames.add(f.name);
92
247
  }
248
+ const resourceCapsResult = parseResourceCapabilities(r.capabilities);
249
+ if (!resourceCapsResult.ok) return resourceCapsResult;
250
+ const resolvedCapabilities = resolveCapabilities(
251
+ globalCapabilities,
252
+ resourceCapsResult.result
253
+ );
254
+ if (hasShareable(resolvedCapabilities) && !hasCapability(resolvedCapabilities, "audit")) {
255
+ return err(
256
+ "CAPABILITY_DEPENDENCY",
257
+ `"shareable" capability requires "audit" capability`,
258
+ { path: `resources.${r.name}.capabilities` }
259
+ );
260
+ }
261
+ const collision = findCapabilityFieldCollision(fieldNames, resolvedCapabilities);
262
+ if (collision) {
263
+ return err(
264
+ "CAPABILITY_FIELD_COLLISION",
265
+ `Field "${collision.field}" on resource "${r.name}" collides with capability-injected field from "${collision.capability}"`,
266
+ { path: `resources.${r.name}.fields.${collision.field}` }
267
+ );
268
+ }
269
+ const injectedFields = getCapabilityFields(resolvedCapabilities);
270
+ for (const injectedField of injectedFields) {
271
+ fieldNames.add(injectedField.name);
272
+ }
93
273
  let normalizedIndices;
94
274
  if (Array.isArray(r.indices)) {
95
275
  normalizedIndices = {
@@ -107,17 +287,318 @@ function validateSchema(schema) {
107
287
  } else {
108
288
  normalizedIndices = { base: [], search: [], vector: [] };
109
289
  }
290
+ for (const [category, idxFields] of Object.entries(normalizedIndices)) {
291
+ for (const idxField of idxFields) {
292
+ if (!fieldNames.has(idxField)) {
293
+ return err(
294
+ "SCHEMA_INVALID",
295
+ `Invalid schema: index field "${idxField}" in indices.${category} does not match any field in resource "${r.name}". Available fields: ${[...fieldNames].join(", ")}`,
296
+ { path: `resources.${r.name}.indices.${category}` }
297
+ );
298
+ }
299
+ }
300
+ }
110
301
  normalizedResources.push({
111
302
  ...r,
303
+ capabilities: resourceCapsResult.result,
304
+ fields: [...r.fields, ...injectedFields],
112
305
  indices: normalizedIndices
113
306
  });
114
307
  }
115
308
  const relations = Array.isArray(s.relations) ? s.relations : [];
309
+ for (const rel of relations) {
310
+ if (typeof rel !== "object" || rel === null || Array.isArray(rel)) {
311
+ return err("SCHEMA_INVALID", "Invalid schema: relation must be object", {
312
+ path: "relations"
313
+ });
314
+ }
315
+ const r = rel;
316
+ const validateRelationRef = (ref, side) => {
317
+ const refs = Array.isArray(ref) ? ref : typeof ref === "string" ? [ref] : [];
318
+ for (const name of refs) {
319
+ if (typeof name === "string" && !resourceNames.has(name)) {
320
+ return err(
321
+ "SCHEMA_INVALID",
322
+ `Invalid schema: relation.${side} references unknown resource "${name}". Available: ${[...resourceNames].join(", ")}`,
323
+ { path: `relations.${side}` }
324
+ );
325
+ }
326
+ }
327
+ return null;
328
+ };
329
+ if (r.from !== void 0) {
330
+ const e = validateRelationRef(r.from, "from");
331
+ if (e) return e;
332
+ }
333
+ if (r.to !== void 0) {
334
+ const e = validateRelationRef(r.to, "to");
335
+ if (e) return e;
336
+ }
337
+ if (r.joinTable !== void 0 && typeof r.joinTable !== "string") {
338
+ return err("SCHEMA_INVALID", "Invalid schema: relation.joinTable must be string", {
339
+ path: "relations"
340
+ });
341
+ }
342
+ if (r.joinColumns !== void 0) {
343
+ if (typeof r.joinColumns !== "object" || r.joinColumns === null || Array.isArray(r.joinColumns)) {
344
+ return err("SCHEMA_INVALID", "Invalid schema: relation.joinColumns must be object with from/to strings", {
345
+ path: "relations"
346
+ });
347
+ }
348
+ const jc = r.joinColumns;
349
+ if (typeof jc.from !== "string" || typeof jc.to !== "string") {
350
+ return err("SCHEMA_INVALID", "Invalid schema: relation.joinColumns.from and .to must be strings", {
351
+ path: "relations"
352
+ });
353
+ }
354
+ }
355
+ if (r.capabilities !== void 0) {
356
+ if (r.type !== "many-many") {
357
+ return err(
358
+ "SCHEMA_INVALID",
359
+ `Relation capabilities are only supported on many-many relations, got "${r.type}"`,
360
+ { path: "relations" }
361
+ );
362
+ }
363
+ const capResult = resolveRelationCapabilities(r.capabilities);
364
+ if (!capResult.ok) return capResult;
365
+ if (Array.isArray(r.metadata) && capResult.result.length > 0) {
366
+ const injectedNames = new Set(getRelationCapabilityFieldNames(capResult.result));
367
+ const relationName = typeof r.relation === "string" ? r.relation : "unknown";
368
+ for (const metaField of r.metadata) {
369
+ if (typeof metaField?.name === "string" && injectedNames.has(metaField.name)) {
370
+ return err(
371
+ "CAPABILITY_FIELD_COLLISION",
372
+ `Relation metadata field "${metaField.name}" on relation "${relationName}" collides with relation capability-injected field`,
373
+ { path: `relations.${relationName}.metadata.${metaField.name}` }
374
+ );
375
+ }
376
+ }
377
+ }
378
+ }
379
+ }
380
+ const namespaced = resolveNamespaced(s);
116
381
  return ok({
382
+ capabilities: globalCapabilities,
117
383
  resources: normalizedResources,
118
- relations
384
+ relations,
385
+ namespaced
119
386
  });
120
387
  }
388
+ function parseSchemaCapabilities(value) {
389
+ if (value === void 0) return ok(void 0);
390
+ if (!Array.isArray(value)) {
391
+ return err("INVALID_CAPABILITY_CONFIG", "Schema capabilities must be an array", {
392
+ path: "capabilities"
393
+ });
394
+ }
395
+ return parseCapabilityArray(value, "capabilities");
396
+ }
397
+ function parseResourceCapabilities(value) {
398
+ if (value === void 0) return ok(void 0);
399
+ if (Array.isArray(value)) {
400
+ return parseCapabilityArray(value, "resources.capabilities");
401
+ }
402
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
403
+ return err(
404
+ "INVALID_CAPABILITY_CONFIG",
405
+ "Resource capabilities must be an array or { exclude: [...] }",
406
+ { path: "resources.capabilities" }
407
+ );
408
+ }
409
+ const obj = value;
410
+ if (!Array.isArray(obj.exclude)) {
411
+ return err(
412
+ "INVALID_CAPABILITY_CONFIG",
413
+ "Resource capability exclusion must be { exclude: SimpleCapability[] }",
414
+ { path: "resources.capabilities.exclude" }
415
+ );
416
+ }
417
+ const excluded = [];
418
+ for (const cap of obj.exclude) {
419
+ if (typeof cap !== "string") {
420
+ return err("INVALID_CAPABILITY", "Capability name must be a string", {
421
+ path: "resources.capabilities.exclude"
422
+ });
423
+ }
424
+ if (!SIMPLE_CAPABILITIES.has(cap)) {
425
+ return err("INVALID_CAPABILITY", `Unknown capability "${cap}"`, {
426
+ path: "resources.capabilities.exclude"
427
+ });
428
+ }
429
+ excluded.push(cap);
430
+ }
431
+ return ok({ exclude: excluded });
432
+ }
433
+ function parseCapabilityArray(input, path) {
434
+ const parsed = [];
435
+ for (const entry of input) {
436
+ if (typeof entry === "string") {
437
+ if (entry === "shareable") {
438
+ return err(
439
+ "INVALID_CAPABILITY_CONFIG",
440
+ `"shareable" capability requires object config`,
441
+ { path }
442
+ );
443
+ }
444
+ if (!SIMPLE_CAPABILITIES.has(entry)) {
445
+ return err("INVALID_CAPABILITY", `Unknown capability "${entry}"`, { path });
446
+ }
447
+ parsed.push(entry);
448
+ continue;
449
+ }
450
+ const shareableResult = parseShareableCapability(entry, path);
451
+ if (shareableResult.ok) {
452
+ parsed.push(shareableResult.result);
453
+ continue;
454
+ }
455
+ return shareableResult;
456
+ }
457
+ return ok(resolveCapabilities(parsed, void 0));
458
+ }
459
+ function parseShareableCapability(entry, path) {
460
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
461
+ return err("INVALID_CAPABILITY_CONFIG", "Invalid capability configuration", { path });
462
+ }
463
+ const obj = entry;
464
+ if (!obj.shareable || typeof obj.shareable !== "object" || Array.isArray(obj.shareable)) {
465
+ return err("INVALID_CAPABILITY_CONFIG", "Invalid capability configuration", { path });
466
+ }
467
+ const shareable = obj.shareable;
468
+ if (!Array.isArray(shareable.levels)) {
469
+ return err(
470
+ "INVALID_CAPABILITY_CONFIG",
471
+ `"shareable.levels" must be an array`,
472
+ { path: `${path}.shareable.levels` }
473
+ );
474
+ }
475
+ if (shareable.levels.length === 0 || !shareable.levels.every((level) => typeof level === "string" && SHAREABLE_LEVELS.has(level))) {
476
+ return err(
477
+ "INVALID_CAPABILITY_CONFIG",
478
+ `"shareable.levels" must include one or more of [viewer, editor, owner]`,
479
+ { path: `${path}.shareable.levels` }
480
+ );
481
+ }
482
+ if (shareable.default !== void 0 && (typeof shareable.default !== "string" || !SHAREABLE_DEFAULTS.has(shareable.default))) {
483
+ return err(
484
+ "INVALID_CAPABILITY_CONFIG",
485
+ `"shareable.default" must be one of [private, shared]`,
486
+ { path: `${path}.shareable.default` }
487
+ );
488
+ }
489
+ if (shareable.visibilityDefault !== void 0 && (typeof shareable.visibilityDefault !== "string" || !SHAREABLE_VISIBILITY_DEFAULTS.has(shareable.visibilityDefault))) {
490
+ return err(
491
+ "INVALID_CAPABILITY_CONFIG",
492
+ `"shareable.visibilityDefault" must be one of [ns, private, shared]`,
493
+ { path: `${path}.shareable.visibilityDefault` }
494
+ );
495
+ }
496
+ if (shareable.default === void 0 && shareable.visibilityDefault === void 0) {
497
+ return err(
498
+ "INVALID_CAPABILITY_CONFIG",
499
+ `"shareable.default" or "shareable.visibilityDefault" must be provided`,
500
+ { path: `${path}.shareable` }
501
+ );
502
+ }
503
+ if (shareable.supportsScopeGrants !== void 0 && typeof shareable.supportsScopeGrants !== "boolean") {
504
+ return err(
505
+ "INVALID_CAPABILITY_CONFIG",
506
+ `"shareable.supportsScopeGrants" must be boolean`,
507
+ { path: `${path}.shareable.supportsScopeGrants` }
508
+ );
509
+ }
510
+ if (shareable.crossNsShareable !== void 0 && typeof shareable.crossNsShareable !== "boolean") {
511
+ return err(
512
+ "INVALID_CAPABILITY_CONFIG",
513
+ `"shareable.crossNsShareable" must be boolean`,
514
+ { path: `${path}.shareable.crossNsShareable` }
515
+ );
516
+ }
517
+ if (shareable.principalMode !== void 0 && (typeof shareable.principalMode !== "string" || !SHAREABLE_PRINCIPAL_MODES.has(shareable.principalMode))) {
518
+ return err(
519
+ "INVALID_CAPABILITY_CONFIG",
520
+ `"shareable.principalMode" must be one of [opaque-id]`,
521
+ { path: `${path}.shareable.principalMode` }
522
+ );
523
+ }
524
+ let relationInheritance;
525
+ if (shareable.relationInheritance !== void 0) {
526
+ const rel = shareable.relationInheritance;
527
+ if (typeof rel !== "object" || rel === null || Array.isArray(rel)) {
528
+ return err(
529
+ "INVALID_CAPABILITY_CONFIG",
530
+ `"shareable.relationInheritance" must be an object`,
531
+ { path: `${path}.shareable.relationInheritance` }
532
+ );
533
+ }
534
+ const relObj = rel;
535
+ if (typeof relObj.enabled !== "boolean") {
536
+ return err(
537
+ "INVALID_CAPABILITY_CONFIG",
538
+ `"shareable.relationInheritance.enabled" must be boolean`,
539
+ { path: `${path}.shareable.relationInheritance.enabled` }
540
+ );
541
+ }
542
+ if (relObj.relations !== void 0 && (!Array.isArray(relObj.relations) || !relObj.relations.every((value) => typeof value === "string"))) {
543
+ return err(
544
+ "INVALID_CAPABILITY_CONFIG",
545
+ `"shareable.relationInheritance.relations" must be string[]`,
546
+ { path: `${path}.shareable.relationInheritance.relations` }
547
+ );
548
+ }
549
+ if (relObj.requireRelateConsent !== void 0 && typeof relObj.requireRelateConsent !== "boolean") {
550
+ return err(
551
+ "INVALID_CAPABILITY_CONFIG",
552
+ `"shareable.relationInheritance.requireRelateConsent" must be boolean`,
553
+ { path: `${path}.shareable.relationInheritance.requireRelateConsent` }
554
+ );
555
+ }
556
+ relationInheritance = {
557
+ enabled: relObj.enabled,
558
+ relations: relObj.relations,
559
+ requireRelateConsent: relObj.requireRelateConsent ?? true
560
+ };
561
+ }
562
+ const normalized = {
563
+ shareable: {
564
+ levels: shareable.levels,
565
+ default: shareable.default ?? (shareable.visibilityDefault === "private" ? "private" : "shared"),
566
+ visibilityDefault: shareable.visibilityDefault ?? shareable.default,
567
+ supportsScopeGrants: shareable.supportsScopeGrants ?? true,
568
+ crossNsShareable: shareable.crossNsShareable ?? true,
569
+ principalMode: shareable.principalMode ?? "opaque-id",
570
+ relationInheritance
571
+ }
572
+ };
573
+ return ok(normalized);
574
+ }
575
+ function hasCapability(caps, capability) {
576
+ return caps.some((cap) => cap === capability);
577
+ }
578
+ function hasShareable(caps) {
579
+ return caps.some((cap) => typeof cap === "object" && cap !== null && "shareable" in cap);
580
+ }
581
+ function findCapabilityFieldCollision(userFieldNames, resolvedCapabilities) {
582
+ for (const cap of resolvedCapabilities) {
583
+ const key = typeof cap === "string" ? cap : "shareable";
584
+ const injectedDefs = CAPABILITY_FIELD_DEFS[key];
585
+ for (const field of injectedDefs) {
586
+ if (userFieldNames.has(field.name)) {
587
+ return { field: field.name, capability: key };
588
+ }
589
+ }
590
+ }
591
+ return null;
592
+ }
593
+
594
+ // src/ns.ts
595
+ function ns(...segments) {
596
+ const filtered = segments.filter((s) => s != null && s !== "");
597
+ if (filtered.length === 0) {
598
+ throw new Error("ns() requires at least one non-empty segment");
599
+ }
600
+ return filtered.join(":");
601
+ }
121
602
 
122
603
  // src/normalize.ts
123
604
  function normalizeDfql(value) {
@@ -152,11 +633,707 @@ function unwrapEnvelope(env) {
152
633
  throw env.error;
153
634
  }
154
635
 
636
+ // src/kv.ts
637
+ var KV_RESOURCE_NAME = "kv";
638
+ function kvId(key) {
639
+ if (typeof key !== "string") {
640
+ throw new Error("Invalid KV key: must be string");
641
+ }
642
+ return `${KV_RESOURCE_NAME}:${key}`;
643
+ }
644
+ function ensureBuiltinKv(schema) {
645
+ const existingKv = schema.resources.find((r) => r.name === KV_RESOURCE_NAME);
646
+ if (existingKv) {
647
+ if (existingKv.version !== 1) {
648
+ throw new Error(
649
+ `KV resource version mismatch: expected 1, got ${existingKv.version}`
650
+ );
651
+ }
652
+ return schema;
653
+ }
654
+ const kvResource = {
655
+ name: KV_RESOURCE_NAME,
656
+ version: 1,
657
+ idPrefix: KV_RESOURCE_NAME,
658
+ fields: [
659
+ {
660
+ name: "id",
661
+ type: "string",
662
+ required: true
663
+ },
664
+ {
665
+ name: "value",
666
+ type: "json",
667
+ required: false
668
+ }
669
+ ],
670
+ indices: ["id"],
671
+ permissions: {
672
+ read: { fields: ["id", "value"] },
673
+ write: { fields: ["id", "value"] }
674
+ }
675
+ };
676
+ return {
677
+ ...schema,
678
+ resources: [...schema.resources, kvResource]
679
+ };
680
+ }
681
+
682
+ // src/joins.ts
683
+ function getJoinStoreKey(from, relation, to) {
684
+ return `join_${from}_${relation}_${to}`;
685
+ }
686
+ function getJoinTableName(from, relation, joinTable) {
687
+ return joinTable || `__datafn_join_${from}_${relation}`;
688
+ }
689
+ function enumerateJoinStoreKeys(relations, resourceFilter) {
690
+ const keys = [];
691
+ for (const rel of relations) {
692
+ if (rel.type !== "many-many") continue;
693
+ const froms = Array.isArray(rel.from) ? rel.from : [rel.from];
694
+ const tos = Array.isArray(rel.to) ? rel.to : [rel.to];
695
+ const relName = rel.relation ?? "rel";
696
+ for (const f of froms) {
697
+ if (resourceFilter && !resourceFilter.has(f)) continue;
698
+ for (const t of tos) {
699
+ keys.push(getJoinStoreKey(f, relName, t));
700
+ }
701
+ }
702
+ }
703
+ return keys;
704
+ }
705
+
706
+ // src/schema-index.ts
707
+ function buildSchemaIndex(schema) {
708
+ const resourcesByName = /* @__PURE__ */ new Map();
709
+ const fieldsByResource = /* @__PURE__ */ new Map();
710
+ const relationsByResource = /* @__PURE__ */ new Map();
711
+ const relationsFromResource = /* @__PURE__ */ new Map();
712
+ for (const resource of schema.resources) {
713
+ resourcesByName.set(resource.name, resource);
714
+ const fieldMap = /* @__PURE__ */ new Map();
715
+ for (const field of resource.fields) {
716
+ fieldMap.set(field.name, field);
717
+ }
718
+ fieldsByResource.set(resource.name, fieldMap);
719
+ relationsByResource.set(resource.name, []);
720
+ relationsFromResource.set(resource.name, []);
721
+ }
722
+ if (schema.relations) {
723
+ for (const rel of schema.relations) {
724
+ const fromResources = Array.isArray(rel.from) ? rel.from : [rel.from];
725
+ const toResources = Array.isArray(rel.to) ? rel.to : [rel.to];
726
+ for (const fromRes of fromResources) {
727
+ const fromRelations = relationsFromResource.get(fromRes) ?? [];
728
+ fromRelations.push(rel);
729
+ relationsFromResource.set(fromRes, fromRelations);
730
+ const allRelations = relationsByResource.get(fromRes) ?? [];
731
+ allRelations.push(rel);
732
+ relationsByResource.set(fromRes, allRelations);
733
+ }
734
+ for (const toRes of toResources) {
735
+ if (rel.inverse) {
736
+ const allRelations = relationsByResource.get(toRes) ?? [];
737
+ if (!allRelations.includes(rel)) {
738
+ allRelations.push(rel);
739
+ relationsByResource.set(toRes, allRelations);
740
+ }
741
+ }
742
+ }
743
+ }
744
+ }
745
+ return { resourcesByName, fieldsByResource, relationsByResource, relationsFromResource };
746
+ }
747
+ function getResource(index, name) {
748
+ return index.resourcesByName.get(name);
749
+ }
750
+ function getField(index, resource, field) {
751
+ return index.fieldsByResource.get(resource)?.get(field);
752
+ }
753
+ function getRelationsFrom(index, resource) {
754
+ return index.relationsFromResource.get(resource) ?? [];
755
+ }
756
+ function getRelation(index, fromResource, relationName) {
757
+ const relations = index.relationsFromResource.get(fromResource) ?? [];
758
+ return relations.find((r) => r.relation === relationName);
759
+ }
760
+ function getRelationTarget(relation) {
761
+ return Array.isArray(relation.to) ? relation.to[0] : relation.to;
762
+ }
763
+ function findRelationBidirectional(schema, resource, relationName) {
764
+ return schema.relations?.find(
765
+ (r) => r.from === resource && r.relation === relationName || r.to === resource && r.inverse === relationName
766
+ );
767
+ }
768
+
769
+ // src/filters.ts
770
+ var OP_REMAP = {
771
+ eq: "$eq",
772
+ ne: "$ne",
773
+ gt: "$gt",
774
+ gte: "$gte",
775
+ lt: "$lt",
776
+ lte: "$lte",
777
+ in: "$in",
778
+ not_in: "$nin",
779
+ like: "$like",
780
+ ilike: "$ilike",
781
+ not_like: "$not_like",
782
+ not_ilike: "$not_ilike",
783
+ is_null: "$is_null",
784
+ is_not_null: "$is_not_null",
785
+ is_empty: "$is_empty",
786
+ is_not_empty: "$is_not_empty",
787
+ before: "$before",
788
+ after: "$after",
789
+ between: "$between",
790
+ not_between: "$not_between"
791
+ };
792
+ function normalizeFilterOps(filters) {
793
+ const out = {};
794
+ for (const [key, value] of Object.entries(filters)) {
795
+ if ((key === "$and" || key === "$or") && Array.isArray(value)) {
796
+ out[key] = value.map(normalizeFilterOps);
797
+ } else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
798
+ const ops = value;
799
+ const normalized = {};
800
+ for (const [op, opVal] of Object.entries(ops)) {
801
+ normalized[OP_REMAP[op] ?? op] = opVal;
802
+ }
803
+ out[key] = normalized;
804
+ } else {
805
+ out[key] = value;
806
+ }
807
+ }
808
+ return out;
809
+ }
810
+ function dfqlError(code, message) {
811
+ const err2 = { code, message };
812
+ throw err2;
813
+ }
814
+ function likeToRegExp(pattern, caseInsensitive) {
815
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/%/g, ".*").replace(/_/g, ".");
816
+ return new RegExp(`^${escaped}$`, caseInsensitive ? "i" : "");
817
+ }
818
+ function resolvePath(record, parts) {
819
+ let current = record;
820
+ for (const part of parts) {
821
+ if (current === null || current === void 0 || typeof current !== "object") return void 0;
822
+ current = current[part];
823
+ }
824
+ return current;
825
+ }
826
+ function evaluateOperators(fieldValue, ops) {
827
+ for (const [op, opVal] of Object.entries(ops)) {
828
+ switch (op) {
829
+ case "$eq":
830
+ if (fieldValue !== opVal) return false;
831
+ break;
832
+ case "$ne":
833
+ if (fieldValue === opVal) return false;
834
+ break;
835
+ case "$gt":
836
+ if (!(fieldValue > opVal)) return false;
837
+ break;
838
+ case "$gte":
839
+ if (!(fieldValue >= opVal)) return false;
840
+ break;
841
+ case "$lt":
842
+ if (!(fieldValue < opVal)) return false;
843
+ break;
844
+ case "$lte":
845
+ if (!(fieldValue <= opVal)) return false;
846
+ break;
847
+ case "$in":
848
+ if (!Array.isArray(opVal)) dfqlError("DFQL_INVALID", `$in requires array`);
849
+ if (!opVal.includes(fieldValue)) return false;
850
+ break;
851
+ case "$nin":
852
+ if (!Array.isArray(opVal)) dfqlError("DFQL_INVALID", `$nin requires array`);
853
+ if (opVal.includes(fieldValue)) return false;
854
+ break;
855
+ case "$contains":
856
+ if (typeof fieldValue === "string") {
857
+ if (!fieldValue.includes(opVal)) return false;
858
+ } else if (Array.isArray(fieldValue)) {
859
+ if (!fieldValue.includes(opVal)) return false;
860
+ } else {
861
+ return false;
862
+ }
863
+ break;
864
+ case "$startsWith":
865
+ if (typeof fieldValue !== "string") return false;
866
+ if (!fieldValue.startsWith(opVal)) return false;
867
+ break;
868
+ case "$endsWith":
869
+ if (typeof fieldValue !== "string") return false;
870
+ if (!fieldValue.endsWith(opVal)) return false;
871
+ break;
872
+ case "$like":
873
+ if (typeof fieldValue !== "string" || typeof opVal !== "string") return false;
874
+ if (!likeToRegExp(opVal, false).test(fieldValue)) return false;
875
+ break;
876
+ case "$ilike":
877
+ if (typeof fieldValue !== "string" || typeof opVal !== "string") return false;
878
+ if (!likeToRegExp(opVal, true).test(fieldValue)) return false;
879
+ break;
880
+ case "$not_like":
881
+ if (typeof fieldValue !== "string" || typeof opVal !== "string") return false;
882
+ if (likeToRegExp(opVal, false).test(fieldValue)) return false;
883
+ break;
884
+ case "$not_ilike":
885
+ if (typeof fieldValue !== "string" || typeof opVal !== "string") return false;
886
+ if (likeToRegExp(opVal, true).test(fieldValue)) return false;
887
+ break;
888
+ case "$is_null":
889
+ if (opVal === true && fieldValue !== null && fieldValue !== void 0) return false;
890
+ if (opVal === false && (fieldValue === null || fieldValue === void 0)) return false;
891
+ break;
892
+ case "$is_not_null":
893
+ if (opVal === true && (fieldValue === null || fieldValue === void 0)) return false;
894
+ if (opVal === false && fieldValue !== null && fieldValue !== void 0) return false;
895
+ break;
896
+ case "$is_empty": {
897
+ const isEmpty = fieldValue === null || fieldValue === void 0 || fieldValue === "" || Array.isArray(fieldValue) && fieldValue.length === 0;
898
+ if (opVal === true && !isEmpty) return false;
899
+ if (opVal === false && isEmpty) return false;
900
+ break;
901
+ }
902
+ case "$is_not_empty": {
903
+ const isEmpty = fieldValue === null || fieldValue === void 0 || fieldValue === "" || Array.isArray(fieldValue) && fieldValue.length === 0;
904
+ if (opVal === true && isEmpty) return false;
905
+ if (opVal === false && !isEmpty) return false;
906
+ break;
907
+ }
908
+ case "$before":
909
+ if (!(fieldValue < opVal)) return false;
910
+ break;
911
+ case "$after":
912
+ if (!(fieldValue > opVal)) return false;
913
+ break;
914
+ case "$between": {
915
+ if (!Array.isArray(opVal) || opVal.length !== 2)
916
+ dfqlError("DFQL_INVALID", `$between requires [low, high] array`);
917
+ const [low, high] = opVal;
918
+ const fv = fieldValue;
919
+ if (!(fv >= low && fv <= high)) return false;
920
+ break;
921
+ }
922
+ case "$not_between": {
923
+ if (!Array.isArray(opVal) || opVal.length !== 2)
924
+ dfqlError("DFQL_INVALID", `$not_between requires [low, high] array`);
925
+ const [low, high] = opVal;
926
+ const fv = fieldValue;
927
+ if (!(fv < low || fv > high)) return false;
928
+ break;
929
+ }
930
+ default:
931
+ dfqlError("DFQL_UNSUPPORTED", `Unsupported DFQL operator: ${op}`);
932
+ }
933
+ }
934
+ return true;
935
+ }
936
+ var MAX_FILTER_DEPTH = 10;
937
+ function evaluateFilter(record, filters, opts, _depth = 0) {
938
+ if (_depth > MAX_FILTER_DEPTH) {
939
+ dfqlError("DFQL_INVALID", `Filter nesting depth exceeds maximum of ${MAX_FILTER_DEPTH}`);
940
+ }
941
+ for (const [key, value] of Object.entries(filters)) {
942
+ if (key === "$and") {
943
+ if (!Array.isArray(value)) dfqlError("DFQL_INVALID", "$and must be array");
944
+ for (const sub of value) {
945
+ if (!evaluateFilter(record, sub, opts, _depth + 1)) return false;
946
+ }
947
+ continue;
948
+ }
949
+ if (key === "$or") {
950
+ if (!Array.isArray(value)) dfqlError("DFQL_INVALID", "$or must be array");
951
+ let matched = false;
952
+ for (const sub of value) {
953
+ if (evaluateFilter(record, sub, opts, _depth + 1)) {
954
+ matched = true;
955
+ break;
956
+ }
957
+ }
958
+ if (!matched) return false;
959
+ continue;
960
+ }
961
+ if (key.includes(".")) {
962
+ const parts = key.split(".");
963
+ const firstSeg = parts[0];
964
+ const firstVal = record[firstSeg];
965
+ if (firstVal !== void 0 && firstVal !== null) {
966
+ const resolved = resolvePath(record, parts);
967
+ if (!evaluateFieldValue(resolved, value)) return false;
968
+ } else if (opts?.resolveRelation) {
969
+ const resource = opts.resource ?? "";
970
+ const recordId = record.id;
971
+ const related = opts.resolveRelation(resource, recordId, firstSeg);
972
+ if (related.length === 0) return false;
973
+ const subFilter = { [parts.slice(1).join(".")]: value };
974
+ const anyMatch = related.some(
975
+ (r) => evaluateFilter(r, subFilter, opts)
976
+ );
977
+ if (!anyMatch) return false;
978
+ } else {
979
+ return false;
980
+ }
981
+ continue;
982
+ }
983
+ const fieldValue = record[key];
984
+ if (!evaluateFieldValue(fieldValue, value)) return false;
985
+ }
986
+ return true;
987
+ }
988
+ function evaluateFieldValue(fieldValue, value) {
989
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
990
+ return evaluateOperators(fieldValue, value);
991
+ }
992
+ return fieldValue === value;
993
+ }
994
+
995
+ // src/relations.ts
996
+ function normalizeRelationPayload(payload) {
997
+ if (typeof payload === "string") {
998
+ return [{ toId: payload, metadata: {} }];
999
+ }
1000
+ if (Array.isArray(payload)) {
1001
+ return payload.map((item) => {
1002
+ if (typeof item === "string") {
1003
+ return { toId: item, metadata: {} };
1004
+ }
1005
+ if (typeof item === "object" && item !== null) {
1006
+ const { $ref, ...meta } = item;
1007
+ if (!$ref || typeof $ref !== "string") {
1008
+ throw new Error("Invalid relation payload item: missing or invalid $ref");
1009
+ }
1010
+ return { toId: $ref, metadata: meta };
1011
+ }
1012
+ throw new Error("Invalid relation payload item type");
1013
+ });
1014
+ }
1015
+ if (typeof payload === "object" && payload !== null) {
1016
+ const { $ref, ...meta } = payload;
1017
+ if (!$ref || typeof $ref !== "string") {
1018
+ throw new Error("Invalid relation payload: missing or invalid $ref");
1019
+ }
1020
+ return [{ toId: $ref, metadata: meta }];
1021
+ }
1022
+ throw new Error("Invalid relation payload: must be string, object with $ref, or array");
1023
+ }
1024
+
1025
+ // src/aggregate.ts
1026
+ function calculateAggregation(op, field, records) {
1027
+ if (op === "count") {
1028
+ return records.length;
1029
+ }
1030
+ const values = records.map((r) => r[field]).filter((v) => v !== null && v !== void 0);
1031
+ if (values.length === 0) return null;
1032
+ if (op === "sum") {
1033
+ return values.reduce((a, b) => (Number(a) || 0) + (Number(b) || 0), 0);
1034
+ }
1035
+ if (op === "min") {
1036
+ let min = values[0];
1037
+ for (const v of values) {
1038
+ if (v < min) min = v;
1039
+ }
1040
+ return min;
1041
+ }
1042
+ if (op === "max") {
1043
+ let max = values[0];
1044
+ for (const v of values) {
1045
+ if (v > max) max = v;
1046
+ }
1047
+ return max;
1048
+ }
1049
+ if (op === "avg") {
1050
+ const sum = values.reduce((a, b) => (Number(a) || 0) + (Number(b) || 0), 0);
1051
+ return sum / values.length;
1052
+ }
1053
+ return null;
1054
+ }
1055
+
1056
+ // src/sort.ts
1057
+ function parseSortTerms(sort) {
1058
+ if (!sort || sort.length === 0) return [];
1059
+ return sort.map((term) => {
1060
+ if (term.startsWith("-")) {
1061
+ return { field: term.slice(1), direction: "desc" };
1062
+ }
1063
+ const colonIdx = term.indexOf(":");
1064
+ if (colonIdx !== -1) {
1065
+ const field = term.slice(0, colonIdx);
1066
+ const direction = term.slice(colonIdx + 1);
1067
+ return { field, direction };
1068
+ }
1069
+ return { field: term, direction: "asc" };
1070
+ });
1071
+ }
1072
+ function sortRecords(records, terms) {
1073
+ return [...records].sort((a, b) => {
1074
+ for (const term of terms) {
1075
+ const aVal = a[term.field];
1076
+ const bVal = b[term.field];
1077
+ if (aVal === bVal) continue;
1078
+ let cmp;
1079
+ if (aVal === null || aVal === void 0) {
1080
+ cmp = 1;
1081
+ } else if (bVal === null || bVal === void 0) {
1082
+ cmp = -1;
1083
+ } else if (typeof aVal === "string" && typeof bVal === "string") {
1084
+ cmp = aVal.localeCompare(bVal);
1085
+ } else if (typeof aVal === "number" && typeof bVal === "number") {
1086
+ cmp = aVal - bVal;
1087
+ } else {
1088
+ cmp = String(aVal).localeCompare(String(bVal));
1089
+ }
1090
+ if (cmp !== 0) return term.direction === "asc" ? cmp : -cmp;
1091
+ }
1092
+ return String(a.id ?? "").localeCompare(String(b.id ?? ""));
1093
+ });
1094
+ }
1095
+
1096
+ // src/select.ts
1097
+ function parseSelectToken(token) {
1098
+ const parts = token.split(".");
1099
+ const baseName = parts[0];
1100
+ const directive = parts.length > 1 ? parts.slice(1).join(".") : void 0;
1101
+ return { path: parts, baseName, directive };
1102
+ }
1103
+
1104
+ // src/date.ts
1105
+ function dfqlInvalid(msg) {
1106
+ throw { code: "DFQL_INVALID", message: msg };
1107
+ }
1108
+ function toEpochMs(value) {
1109
+ if (typeof value === "number") return value;
1110
+ if (value instanceof Date) {
1111
+ const ms = value.getTime();
1112
+ if (isNaN(ms)) dfqlInvalid(`Invalid Date object`);
1113
+ return ms;
1114
+ }
1115
+ if (typeof value === "string") {
1116
+ const ms = new Date(value).getTime();
1117
+ if (isNaN(ms)) dfqlInvalid(`Invalid date string: ${value}`);
1118
+ return ms;
1119
+ }
1120
+ dfqlInvalid(`Cannot convert to epoch ms: ${String(value)}`);
1121
+ }
1122
+ function fromEpochMs(value) {
1123
+ if (value instanceof Date) {
1124
+ if (isNaN(value.getTime())) dfqlInvalid(`Invalid Date object`);
1125
+ return value;
1126
+ }
1127
+ if (typeof value === "number") return new Date(value);
1128
+ if (typeof value === "string") {
1129
+ const d = new Date(value);
1130
+ if (isNaN(d.getTime())) dfqlInvalid(`Invalid date string: ${value}`);
1131
+ return d;
1132
+ }
1133
+ dfqlInvalid(`Cannot convert to Date: ${String(value)}`);
1134
+ }
1135
+ function coerceDateFieldsToEpoch(record, fields) {
1136
+ for (const field of fields) {
1137
+ if (field.type !== "date") continue;
1138
+ const val = record[field.name];
1139
+ if (val === null || val === void 0) continue;
1140
+ record[field.name] = toEpochMs(val);
1141
+ }
1142
+ return record;
1143
+ }
1144
+ function parseDateFieldsToDate(record, fields) {
1145
+ for (const field of fields) {
1146
+ if (field.type !== "date") continue;
1147
+ const val = record[field.name];
1148
+ if (val === null || val === void 0) continue;
1149
+ record[field.name] = fromEpochMs(val);
1150
+ }
1151
+ return record;
1152
+ }
1153
+
1154
+ // src/validate.ts
1155
+ var DISALLOWED_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1156
+ var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2})?)?$/;
1157
+ function checkPrototypePollution(obj) {
1158
+ const visited = /* @__PURE__ */ new WeakSet();
1159
+ const check = (value) => {
1160
+ if (typeof value !== "object" || value === null) {
1161
+ return { ok: true };
1162
+ }
1163
+ const objectValue = value;
1164
+ if (visited.has(objectValue)) {
1165
+ return { ok: true };
1166
+ }
1167
+ visited.add(objectValue);
1168
+ if (Array.isArray(value)) {
1169
+ for (const element of value) {
1170
+ const nestedResult = check(element);
1171
+ if (!nestedResult.ok) return nestedResult;
1172
+ }
1173
+ return { ok: true };
1174
+ }
1175
+ for (const key of Object.keys(value)) {
1176
+ if (DISALLOWED_KEYS.has(key)) {
1177
+ return { ok: false, key };
1178
+ }
1179
+ const nestedResult = check(value[key]);
1180
+ if (!nestedResult.ok) {
1181
+ return nestedResult;
1182
+ }
1183
+ }
1184
+ return { ok: true };
1185
+ };
1186
+ return check(obj);
1187
+ }
1188
+ function validateFieldValue(schemaType, value, nullable) {
1189
+ if (value === null) {
1190
+ if (nullable) return { ok: true };
1191
+ return { ok: false, error: `expected ${schemaType}, got null` };
1192
+ }
1193
+ switch (schemaType) {
1194
+ case "string":
1195
+ if (typeof value !== "string") {
1196
+ return { ok: false, error: `expected string, got ${typeof value}` };
1197
+ }
1198
+ return { ok: true };
1199
+ case "number":
1200
+ if (typeof value !== "number" || Number.isNaN(value)) {
1201
+ const actual = typeof value === "number" ? "NaN" : typeof value;
1202
+ return { ok: false, error: `expected number, got ${actual}` };
1203
+ }
1204
+ return { ok: true };
1205
+ case "boolean":
1206
+ if (typeof value !== "boolean") {
1207
+ return { ok: false, error: `expected boolean, got ${typeof value}` };
1208
+ }
1209
+ return { ok: true };
1210
+ case "date":
1211
+ if (typeof value === "string") {
1212
+ if (!ISO_DATE_RE.test(value) || Number.isNaN(Date.parse(value))) {
1213
+ return { ok: false, error: `expected date (ISO 8601 string or epoch number), got invalid date string` };
1214
+ }
1215
+ return { ok: true };
1216
+ }
1217
+ if (typeof value === "number") {
1218
+ return { ok: true };
1219
+ }
1220
+ return { ok: false, error: `expected date (ISO 8601 string or epoch number), got ${typeof value}` };
1221
+ case "object":
1222
+ if (typeof value !== "object" || Array.isArray(value)) {
1223
+ const actual = Array.isArray(value) ? "array" : typeof value;
1224
+ return { ok: false, error: `expected object, got ${actual}` };
1225
+ }
1226
+ return { ok: true };
1227
+ case "array":
1228
+ if (!Array.isArray(value)) {
1229
+ return { ok: false, error: `expected array, got ${typeof value}` };
1230
+ }
1231
+ return { ok: true };
1232
+ case "file":
1233
+ if (typeof value !== "string") {
1234
+ return { ok: false, error: `expected file (string), got ${typeof value}` };
1235
+ }
1236
+ return { ok: true };
1237
+ case "json":
1238
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
1239
+ return { ok: true };
1240
+ }
1241
+ if (typeof value === "object") {
1242
+ return { ok: true };
1243
+ }
1244
+ return { ok: false, error: `expected json, got ${typeof value}` };
1245
+ default:
1246
+ return { ok: false, error: `unknown schema type: ${schemaType}` };
1247
+ }
1248
+ }
1249
+
1250
+ // src/hooks.ts
1251
+ function wrapError(err2, pluginName, hookName) {
1252
+ if (err2 && typeof err2 === "object") {
1253
+ const e = err2;
1254
+ return {
1255
+ code: e.code || "INTERNAL",
1256
+ message: e.message || "Plugin error",
1257
+ details: { path: `plugins.${pluginName}.${hookName}`, ...e.details ?? {} }
1258
+ };
1259
+ }
1260
+ return {
1261
+ code: "INTERNAL",
1262
+ message: String(err2),
1263
+ details: { path: `plugins.${pluginName}.${hookName}` }
1264
+ };
1265
+ }
1266
+ async function runBeforeHook(plugins, env, hookName, ctx, payload, preArgs = []) {
1267
+ let current = payload;
1268
+ const filtered = plugins.filter((p) => p.runsOn.includes(env));
1269
+ for (const plugin of filtered) {
1270
+ const hook = plugin[hookName];
1271
+ if (!hook) continue;
1272
+ try {
1273
+ const result = await hook(ctx, ...preArgs, current);
1274
+ if (result !== void 0) current = result;
1275
+ } catch (err2) {
1276
+ return { ok: false, error: wrapError(err2, plugin.name, String(hookName)) };
1277
+ }
1278
+ }
1279
+ return { ok: true, value: current };
1280
+ }
1281
+ async function runAfterHook(plugins, env, hookName, ctx, payload, result, preArgs = []) {
1282
+ const filtered = plugins.filter((p) => p.runsOn.includes(env));
1283
+ for (const plugin of filtered) {
1284
+ const hook = plugin[hookName];
1285
+ if (!hook) continue;
1286
+ try {
1287
+ await hook(ctx, ...preArgs, payload, result);
1288
+ } catch (err2) {
1289
+ console.error(`Plugin ${plugin.name}.${String(hookName)} error:`, err2);
1290
+ }
1291
+ }
1292
+ }
1293
+
1294
+ exports.CAPABILITY_FIELD_DEFS = CAPABILITY_FIELD_DEFS;
1295
+ exports.KV_RESOURCE_NAME = KV_RESOURCE_NAME;
1296
+ exports.OP_REMAP = OP_REMAP;
1297
+ exports.RELATION_CAPABILITY_FIELD_DEFS = RELATION_CAPABILITY_FIELD_DEFS;
1298
+ exports.buildSchemaIndex = buildSchemaIndex;
1299
+ exports.calculateAggregation = calculateAggregation;
1300
+ exports.checkPrototypePollution = checkPrototypePollution;
1301
+ exports.coerceDateFieldsToEpoch = coerceDateFieldsToEpoch;
1302
+ exports.defineSchema = defineSchema;
155
1303
  exports.dfqlKey = dfqlKey;
1304
+ exports.ensureBuiltinKv = ensureBuiltinKv;
1305
+ exports.enumerateJoinStoreKeys = enumerateJoinStoreKeys;
156
1306
  exports.err = err;
1307
+ exports.evaluateFilter = evaluateFilter;
1308
+ exports.findRelationBidirectional = findRelationBidirectional;
1309
+ exports.fromEpochMs = fromEpochMs;
1310
+ exports.getCapabilityFields = getCapabilityFields;
1311
+ exports.getField = getField;
1312
+ exports.getJoinStoreKey = getJoinStoreKey;
1313
+ exports.getJoinTableName = getJoinTableName;
1314
+ exports.getRelation = getRelation;
1315
+ exports.getRelationCapabilityFieldNames = getRelationCapabilityFieldNames;
1316
+ exports.getRelationTarget = getRelationTarget;
1317
+ exports.getRelationsFrom = getRelationsFrom;
1318
+ exports.getResource = getResource;
1319
+ exports.isNamespaced = isNamespaced;
1320
+ exports.kvId = kvId;
157
1321
  exports.normalizeDfql = normalizeDfql;
1322
+ exports.normalizeFilterOps = normalizeFilterOps;
1323
+ exports.normalizeRelationPayload = normalizeRelationPayload;
1324
+ exports.ns = ns;
158
1325
  exports.ok = ok;
1326
+ exports.parseDateFieldsToDate = parseDateFieldsToDate;
1327
+ exports.parseSelectToken = parseSelectToken;
1328
+ exports.parseSortTerms = parseSortTerms;
1329
+ exports.resolveCapabilities = resolveCapabilities;
1330
+ exports.resolveRelationCapabilities = resolveRelationCapabilities;
1331
+ exports.runAfterHook = runAfterHook;
1332
+ exports.runBeforeHook = runBeforeHook;
1333
+ exports.sortRecords = sortRecords;
1334
+ exports.toEpochMs = toEpochMs;
159
1335
  exports.unwrapEnvelope = unwrapEnvelope;
1336
+ exports.validateFieldValue = validateFieldValue;
160
1337
  exports.validateSchema = validateSchema;
161
1338
  //# sourceMappingURL=index.cjs.map
162
1339
  //# sourceMappingURL=index.cjs.map