@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.d.ts CHANGED
@@ -1,26 +1,125 @@
1
+ /**
2
+ * DataFn Error Types and Envelopes
3
+ */
4
+ type DatafnErrorCode = "SCHEMA_INVALID" | "INVALID_CAPABILITY" | "INVALID_CAPABILITY_CONFIG" | "CAPABILITY_FIELD_COLLISION" | "CAPABILITY_DEPENDENCY" | "DFQL_INVALID" | "DFQL_UNKNOWN_RESOURCE" | "DFQL_UNKNOWN_FIELD" | "DFQL_UNKNOWN_RELATION" | "DFQL_UNSUPPORTED" | "DFQL_ABORTED" | "LIMIT_EXCEEDED" | "FORBIDDEN" | "NOT_FOUND" | "CONFLICT" | "INTERNAL"
5
+ /** LOW-026: Network/transport-layer errors (timeouts, connection refused, etc.) */
6
+ | "TRANSPORT_ERROR";
7
+ type DatafnError = {
8
+ code: DatafnErrorCode;
9
+ message: string;
10
+ details?: unknown;
11
+ };
12
+ type DatafnEnvelope<T> = {
13
+ ok: true;
14
+ result: T;
15
+ } | {
16
+ ok: false;
17
+ error: DatafnError;
18
+ };
19
+ /**
20
+ * Helper functions for creating envelopes
21
+ */
22
+ declare function ok<T>(result: T): DatafnEnvelope<T>;
23
+ declare function err<T = never>(code: DatafnErrorCode, message: string, details?: unknown): DatafnEnvelope<T>;
24
+
25
+ type SimpleCapability = "timestamps" | "audit" | "trash" | "archivable";
26
+ type AccessLevel = "viewer" | "editor" | "owner";
27
+ type ShareableVisibilityDefault = "ns" | "private" | "shared";
28
+ type ShareablePrincipalMode = "opaque-id";
29
+ type ShareableRelationInheritance = {
30
+ enabled: boolean;
31
+ relations?: string[];
32
+ requireRelateConsent?: boolean;
33
+ };
34
+ type ShareableCapability = {
35
+ shareable: {
36
+ levels: AccessLevel[];
37
+ default?: "private" | "shared";
38
+ visibilityDefault?: ShareableVisibilityDefault;
39
+ supportsScopeGrants?: boolean;
40
+ crossNsShareable?: boolean;
41
+ principalMode?: ShareablePrincipalMode;
42
+ relationInheritance?: ShareableRelationInheritance;
43
+ };
44
+ };
45
+ type CapabilityEntry = SimpleCapability | ShareableCapability;
46
+ type SchemaCapabilities = CapabilityEntry[];
47
+ type ResourceCapabilities = CapabilityEntry[] | {
48
+ exclude: SimpleCapability[];
49
+ };
50
+ type CapabilityKey = SimpleCapability | "shareable";
51
+ type CapabilityFieldDef = Pick<DatafnFieldSchema, "name" | "type" | "required" | "nullable" | "readonly" | "default">;
52
+ declare const CAPABILITY_FIELD_DEFS: Record<CapabilityKey, CapabilityFieldDef[]>;
53
+ type RelationCapabilityKey = "timestamps" | "audit";
54
+ /**
55
+ * Field definitions injected into join tables for each relation capability.
56
+ * Same field names as resource capabilities; kept separate for clarity.
57
+ */
58
+ declare const RELATION_CAPABILITY_FIELD_DEFS: Record<RelationCapabilityKey, CapabilityFieldDef[]>;
59
+ /**
60
+ * Returns the field names injected by the given resolved relation capabilities.
61
+ * Output is deduplicated and ordered deterministically (canonical capability order).
62
+ */
63
+ declare function getRelationCapabilityFieldNames(caps: string[]): string[];
64
+ declare function resolveCapabilities(globalCaps?: SchemaCapabilities, resourceCaps?: ResourceCapabilities): CapabilityEntry[];
65
+ declare function getCapabilityFields(resolvedCaps: CapabilityEntry[]): DatafnFieldSchema[];
66
+
1
67
  /**
2
68
  * DataFn Schema Types
3
69
  */
70
+
4
71
  type DatafnSchema = {
72
+ capabilities?: SchemaCapabilities;
5
73
  resources: DatafnResourceSchema[];
6
74
  relations?: DatafnRelationSchema[];
75
+ /** Defaults to true. Set to false to disable row-level namespace isolation. */
76
+ namespaced?: boolean;
77
+ };
78
+ /**
79
+ * Permissions policy shape for server-side authorization enforcement.
80
+ * All field arrays are optional; if omitted, no fields are accessible.
81
+ */
82
+ type DatafnPermissionsPolicy = {
83
+ /**
84
+ * Read policy: controls which fields can be selected and filtered.
85
+ */
86
+ read?: {
87
+ fields: string[];
88
+ };
89
+ /**
90
+ * Write policy: controls which fields can be written via mutations.
91
+ */
92
+ write?: {
93
+ fields: string[];
94
+ };
95
+ /**
96
+ * Optional owner field for owner-scoped resources.
97
+ * When set, additional owner-based authorization checks can be performed.
98
+ */
99
+ ownerField?: string;
7
100
  };
8
101
  type DatafnResourceSchema = {
9
102
  name: string;
10
103
  version: number;
11
104
  idPrefix?: string;
12
105
  isRemoteOnly?: boolean;
106
+ /**
107
+ * Resource capabilities.
108
+ * For shareable resources, SPV2 extends shareable config with
109
+ * visibility/membership/scope-related options while preserving legacy fields.
110
+ */
111
+ capabilities?: ResourceCapabilities;
13
112
  fields: DatafnFieldSchema[];
14
113
  indices?: {
15
114
  base?: string[];
16
115
  search?: string[];
17
116
  vector?: string[];
18
117
  } | string[];
19
- permissions?: unknown;
118
+ permissions?: DatafnPermissionsPolicy;
20
119
  };
21
120
  type DatafnFieldSchema = {
22
121
  name: string;
23
- type: "string" | "number" | "boolean" | "object" | "array" | "date" | "file";
122
+ type: "string" | "number" | "boolean" | "object" | "array" | "date" | "file" | "json";
24
123
  required: boolean;
25
124
  nullable?: boolean;
26
125
  readonly?: boolean;
@@ -35,6 +134,7 @@ type DatafnFieldSchema = {
35
134
  encrypt?: boolean;
36
135
  volatile?: boolean;
37
136
  };
137
+ type RelationSimpleCapability = "timestamps" | "audit";
38
138
  type DatafnRelationSchema = {
39
139
  from: string | string[];
40
140
  to: string | string[];
@@ -44,16 +144,70 @@ type DatafnRelationSchema = {
44
144
  cache?: boolean;
45
145
  metadata?: Array<{
46
146
  name: string;
47
- type: "string" | "number" | "boolean" | "date" | "object";
147
+ type: "string" | "number" | "boolean" | "date" | "object" | "json";
48
148
  }>;
49
149
  fkField?: string;
50
150
  pathField?: string;
151
+ joinTable?: string;
152
+ joinColumns?: {
153
+ from: string;
154
+ to: string;
155
+ };
156
+ capabilities?: RelationSimpleCapability[];
51
157
  };
158
+ /**
159
+ * Type-safe schema definition helper.
160
+ * Uses const generic to preserve literal field name types and constrain
161
+ * index entries to only declared field names within each resource.
162
+ */
163
+ type DatafnFieldInput = {
164
+ readonly name: string;
165
+ readonly type: DatafnFieldSchema["type"];
166
+ readonly required: boolean;
167
+ readonly [k: string]: unknown;
168
+ };
169
+ type DatafnIndicesInput<FieldName extends string> = {
170
+ readonly base?: readonly FieldName[];
171
+ readonly search?: readonly FieldName[];
172
+ readonly vector?: readonly FieldName[];
173
+ } | readonly FieldName[];
174
+ type SchemaCapabilitiesInput = readonly CapabilityEntry[];
175
+ type ResourceCapabilitiesInput = SchemaCapabilitiesInput | {
176
+ readonly exclude: readonly SimpleCapability[];
177
+ };
178
+ type DatafnResourceBase = {
179
+ readonly name: string;
180
+ readonly version: number;
181
+ readonly idPrefix?: string;
182
+ readonly isRemoteOnly?: boolean;
183
+ readonly capabilities?: ResourceCapabilitiesInput;
184
+ readonly fields: readonly DatafnFieldInput[];
185
+ readonly indices?: DatafnIndicesInput<string>;
186
+ readonly permissions?: DatafnPermissionsPolicy;
187
+ readonly [k: string]: unknown;
188
+ };
189
+ type ValidateResources<R extends readonly DatafnResourceBase[]> = {
190
+ readonly [K in keyof R]: R[K] extends {
191
+ readonly fields: readonly {
192
+ readonly name: infer N extends string;
193
+ }[];
194
+ } ? Omit<R[K], "indices"> & {
195
+ readonly indices?: DatafnIndicesInput<N>;
196
+ } : R[K];
197
+ };
198
+ declare function defineSchema<const T extends {
199
+ readonly capabilities?: SchemaCapabilitiesInput;
200
+ readonly resources: readonly DatafnResourceBase[];
201
+ readonly relations?: readonly DatafnRelationSchema[] | DatafnRelationSchema[];
202
+ readonly namespaced?: boolean;
203
+ }>(schema: T & {
204
+ readonly resources: ValidateResources<T["resources"]>;
205
+ }): T & DatafnSchema;
52
206
  /**
53
207
  * Event Types
54
208
  */
55
209
  interface DatafnEvent {
56
- type: "mutation_applied" | "mutation_rejected" | "sync_applied" | "sync_failed";
210
+ type: "mutation_applied" | "mutation_rejected" | "sync_applied" | "sync_failed" | "sync_retry" | "connectivity_changed" | "ws_connected" | "ws_disconnected";
57
211
  resource?: string;
58
212
  ids?: string[];
59
213
  mutationId?: string;
@@ -62,6 +216,8 @@ interface DatafnEvent {
62
216
  context?: unknown;
63
217
  action?: string;
64
218
  fields?: string[];
219
+ system?: boolean;
220
+ fromRemoteTab?: boolean;
65
221
  }
66
222
  type DatafnEventFilter = Partial<{
67
223
  type: DatafnEvent["type"] | Array<DatafnEvent["type"]>;
@@ -71,6 +227,7 @@ type DatafnEventFilter = Partial<{
71
227
  action: string | string[];
72
228
  fields: string | string[];
73
229
  contextKeys: string[];
230
+ context: Record<string, unknown>;
74
231
  }>;
75
232
  /**
76
233
  * Signal Type
@@ -78,7 +235,51 @@ type DatafnEventFilter = Partial<{
78
235
  interface DatafnSignal<T> {
79
236
  get(): T;
80
237
  subscribe(handler: (value: T) => void): () => void;
238
+ readonly loading: boolean;
239
+ readonly error: DatafnError | null;
240
+ readonly refreshing: boolean;
241
+ readonly nextCursor: string | null | undefined;
242
+ dispose(): void;
243
+ }
244
+ /**
245
+ * Logger interface for structured, pluggable logging.
246
+ */
247
+ interface DatafnLogger {
248
+ info(message: string, context?: Record<string, unknown>): void;
249
+ warn(message: string, context?: Record<string, unknown>): void;
250
+ error(message: string, context?: Record<string, unknown>): void;
251
+ debug(message: string, context?: Record<string, unknown>): void;
81
252
  }
253
+ /**
254
+ * Limits configuration for server-side enforcement.
255
+ */
256
+ type DatafnLimitsConfig = {
257
+ maxLimit?: number;
258
+ maxTransactSteps?: number;
259
+ maxPayloadBytes?: number;
260
+ maxPullLimit?: number;
261
+ maxBatchSize?: number;
262
+ maxCloneRecords?: number;
263
+ idempotencyTtlMs?: number;
264
+ idempotencyMaxEntries?: number;
265
+ maxSelectTokens?: number;
266
+ maxFilterKeysPerLevel?: number;
267
+ maxSortFields?: number;
268
+ maxAggregations?: number;
269
+ maxSearchQueryLength?: number;
270
+ maxLikePatternLength?: number;
271
+ maxIdLength?: number;
272
+ maxFilterDepth?: number;
273
+ };
274
+ /**
275
+ * WebSocket configuration for connection limits and heartbeat.
276
+ */
277
+ type DatafnWsConfig = {
278
+ maxConnections?: number;
279
+ maxConnectionsPerNamespace?: number;
280
+ heartbeatIntervalMs?: number;
281
+ heartbeatTimeoutMs?: number;
282
+ };
82
283
  /**
83
284
  * Plugin Types
84
285
  */
@@ -94,38 +295,29 @@ interface DatafnPlugin {
94
295
  afterQuery?: (ctx: DatafnHookContext, q: unknown, result: unknown) => Promise<unknown> | unknown;
95
296
  beforeMutation?: (ctx: DatafnHookContext, m: unknown | unknown[]) => Promise<unknown> | unknown;
96
297
  afterMutation?: (ctx: DatafnHookContext, m: unknown | unknown[], result: unknown) => Promise<void> | void;
97
- beforeSync?: (ctx: DatafnHookContext, phase: "seed" | "clone" | "pull" | "push", payload: unknown) => Promise<unknown> | unknown;
98
- afterSync?: (ctx: DatafnHookContext, phase: "seed" | "clone" | "pull" | "push", payload: unknown, result: unknown) => Promise<void> | void;
298
+ beforeSync?: (ctx: DatafnHookContext, phase: "seed" | "clone" | "pull" | "push" | "cloneUp" | "reconcile", payload: unknown) => Promise<unknown> | unknown;
299
+ afterSync?: (ctx: DatafnHookContext, phase: "seed" | "clone" | "pull" | "push" | "cloneUp" | "reconcile", payload: unknown, result: unknown) => Promise<void> | void;
99
300
  }
100
301
 
101
- /**
102
- * DataFn Error Types and Envelopes
103
- */
104
- type DatafnErrorCode = "SCHEMA_INVALID" | "DFQL_INVALID" | "DFQL_UNKNOWN_RESOURCE" | "DFQL_UNKNOWN_FIELD" | "DFQL_UNKNOWN_RELATION" | "DFQL_UNSUPPORTED" | "LIMIT_EXCEEDED" | "FORBIDDEN" | "NOT_FOUND" | "CONFLICT" | "INTERNAL";
105
- type DatafnError = {
106
- code: DatafnErrorCode;
107
- message: string;
108
- details?: unknown;
109
- };
110
- type DatafnEnvelope<T> = {
111
- ok: true;
112
- result: T;
113
- } | {
114
- ok: false;
115
- error: DatafnError;
116
- };
117
- /**
118
- * Helper functions for creating envelopes
119
- */
120
- declare function ok<T>(result: T): DatafnEnvelope<T>;
121
- declare function err<T = never>(code: DatafnErrorCode, message: string, details?: unknown): DatafnEnvelope<T>;
122
-
123
302
  /**
124
303
  * Schema Validation
125
304
  *
126
305
  * Validates and normalizes DataFn schemas according to SCHEMA-001.
127
306
  */
128
307
 
308
+ /**
309
+ * Resolves and validates relation capability declarations.
310
+ * Returns canonical ordered, deduplicated array, or error.
311
+ * - Returns ok([]) when capabilities is undefined.
312
+ * - Rejects non-array, non-string entries, and unknown capability values.
313
+ * - Returns deterministic canonical order: ["timestamps", "audit"] subset.
314
+ */
315
+ declare function resolveRelationCapabilities(capabilities: unknown): DatafnEnvelope<RelationSimpleCapability[]>;
316
+ /**
317
+ * Returns whether namespace isolation is enabled for a given schema.
318
+ * Default is `true`.
319
+ */
320
+ declare function isNamespaced(schema: DatafnSchema): boolean;
129
321
  /**
130
322
  * Validates a schema and returns a normalized version.
131
323
  *
@@ -140,6 +332,20 @@ declare function err<T = never>(code: DatafnErrorCode, message: string, details?
140
332
  */
141
333
  declare function validateSchema(schema: unknown): DatafnEnvelope<DatafnSchema>;
142
334
 
335
+ /**
336
+ * Composable namespace helper.
337
+ *
338
+ * Joins non-empty string segments with `:` to form a namespace string.
339
+ *
340
+ * @example
341
+ * ns("org-acme", "user-123") // => "org-acme:user-123"
342
+ * ns("user-1") // => "user-1"
343
+ * ns("org", "project", "user") // => "org:project:user"
344
+ * ns("org", "", "user") // => "org:user" (empty filtered)
345
+ * ns() // throws Error
346
+ */
347
+ declare function ns(...segments: string[]): string;
348
+
143
349
  /**
144
350
  * DFQL Normalization
145
351
  *
@@ -151,6 +357,11 @@ declare function validateSchema(schema: unknown): DatafnEnvelope<DatafnSchema>;
151
357
  * - Sorts object keys alphabetically
152
358
  * - Removes undefined values
153
359
  * - Preserves arrays, primitives, and null as-is
360
+ *
361
+ * LOW-023: undefined vs null semantics:
362
+ * - `null` is preserved as `null` (explicit absence of a value — serialisable to JSON)
363
+ * - `undefined` is stripped from the output (non-serialisable; treated as "not set")
364
+ * This mirrors JSON.stringify behavior and ensures stable cache keys across environments.
154
365
  */
155
366
  declare function normalizeDfql(value: unknown): unknown;
156
367
  /**
@@ -175,4 +386,331 @@ declare function dfqlKey(value: unknown): string;
175
386
  */
176
387
  declare function unwrapEnvelope<T>(env: DatafnEnvelope<T>): T;
177
388
 
178
- export { type DatafnEnvelope, type DatafnError, type DatafnErrorCode, type DatafnEvent, type DatafnEventFilter, type DatafnFieldSchema, type DatafnHookContext, type DatafnPlugin, type DatafnRelationSchema, type DatafnResourceSchema, type DatafnSchema, type DatafnSignal, dfqlKey, err, normalizeDfql, ok, unwrapEnvelope, validateSchema };
389
+ type DfqlSort = string[];
390
+ type DfqlCursor = {
391
+ after?: Record<string, unknown>;
392
+ before?: Record<string, unknown>;
393
+ };
394
+ type DfqlQuery = {
395
+ resource: string;
396
+ version: number;
397
+ select?: string[];
398
+ omit?: string[];
399
+ filters?: Record<string, unknown>;
400
+ search?: Record<string, unknown>;
401
+ sort?: DfqlSort;
402
+ limit?: number;
403
+ offset?: number;
404
+ cursor?: DfqlCursor;
405
+ count?: boolean;
406
+ groupBy?: string[];
407
+ aggregations?: Record<string, unknown>;
408
+ having?: Record<string, unknown>;
409
+ signal?: AbortSignal;
410
+ };
411
+ type DfqlQueryFragment = Omit<DfqlQuery, "resource" | "version">;
412
+ type DfqlMutation = {
413
+ resource: string;
414
+ version: number;
415
+ operation: string;
416
+ id?: string | string[];
417
+ record?: Record<string, unknown>;
418
+ records?: Array<Record<string, unknown>>;
419
+ clientId?: string;
420
+ mutationId?: string;
421
+ timestamp?: number | string;
422
+ context?: unknown;
423
+ relations?: Record<string, unknown>;
424
+ if?: Record<string, unknown>;
425
+ cascade?: unknown;
426
+ silent?: boolean;
427
+ system?: boolean;
428
+ debounceKey?: string;
429
+ debounceMs?: number;
430
+ };
431
+ type DfqlMutationFragment = Omit<DfqlMutation, "resource" | "version">;
432
+ type DfqlTransact = {
433
+ transactionId?: string;
434
+ atomic?: boolean;
435
+ steps: Array<{
436
+ query?: DfqlQuery;
437
+ mutation?: DfqlMutation;
438
+ }>;
439
+ };
440
+
441
+ /**
442
+ * Built-in KV resource utilities
443
+ */
444
+
445
+ /**
446
+ * Canonical KV resource name
447
+ */
448
+ declare const KV_RESOURCE_NAME = "kv";
449
+ /**
450
+ * Generate the canonical KV id from a key.
451
+ * Mapping: kvId(key) = "kv:" + key
452
+ */
453
+ declare function kvId(key: string): string;
454
+ /**
455
+ * Ensure the schema includes the built-in KV resource.
456
+ * If schema already has resource "kv", validates it is compatible.
457
+ * Otherwise appends a resource definition for KV.
458
+ */
459
+ declare function ensureBuiltinKv(schema: DatafnSchema): DatafnSchema;
460
+
461
+ /**
462
+ * Shared join store key utilities.
463
+ *
464
+ * These functions provide the single canonical derivation of join store names
465
+ * used by both client and server across all sync operations.
466
+ */
467
+
468
+ /**
469
+ * Derive the logical join store key used by clients and change tracking.
470
+ * Format: join_{fromResource}_{relationName}_{toResource}
471
+ */
472
+ declare function getJoinStoreKey(from: string, relation: string, to: string): string;
473
+ /**
474
+ * Derive the actual DB table name for a join table.
475
+ * Format: __datafn_join_{fromResource}_{relationName}
476
+ * Can be overridden by relation.joinTable in schema.
477
+ */
478
+ declare function getJoinTableName(from: string, relation: string, joinTable?: string): string;
479
+ /**
480
+ * Enumerate all logical join store keys for a schema's many-many relations.
481
+ * Handles multi-resource from/to arrays.
482
+ * Useful for building cursor maps, reconcile payloads, and similar operations.
483
+ *
484
+ * @param relations - Array of relation schema definitions
485
+ * @param resourceFilter - Optional set of resource names; if provided, only
486
+ * joins whose `from` resource is in this set are included.
487
+ */
488
+ declare function enumerateJoinStoreKeys(relations: DatafnRelationSchema[], resourceFilter?: Set<string>): string[];
489
+
490
+ interface SchemaIndex {
491
+ resourcesByName: Map<string, DatafnResourceSchema>;
492
+ fieldsByResource: Map<string, Map<string, DatafnFieldSchema>>;
493
+ /** All relations involving the resource (as from OR inverse to). */
494
+ relationsByResource: Map<string, DatafnRelationSchema[]>;
495
+ /** Relations where the resource is the from side. */
496
+ relationsFromResource: Map<string, DatafnRelationSchema[]>;
497
+ }
498
+ declare function buildSchemaIndex(schema: DatafnSchema): SchemaIndex;
499
+ declare function getResource(index: SchemaIndex, name: string): DatafnResourceSchema | undefined;
500
+ declare function getField(index: SchemaIndex, resource: string, field: string): DatafnFieldSchema | undefined;
501
+ declare function getRelationsFrom(index: SchemaIndex, resource: string): DatafnRelationSchema[];
502
+ declare function getRelation(index: SchemaIndex, fromResource: string, relationName: string): DatafnRelationSchema | undefined;
503
+ declare function getRelationTarget(relation: DatafnRelationSchema): string;
504
+ /**
505
+ * Find a relation by name in either direction (forward or inverse).
506
+ * Checks both `relation === relationName` (from side) and `inverse === relationName` (to side).
507
+ */
508
+ declare function findRelationBidirectional(schema: DatafnSchema, resource: string, relationName: string): DatafnRelationSchema | undefined;
509
+
510
+ /** Map non-$-prefixed operator names to core's $-prefixed equivalents. */
511
+ declare const OP_REMAP: Record<string, string>;
512
+ /** Recursively remap non-$-prefixed operator names to core's $-prefixed form. */
513
+ declare function normalizeFilterOps(filters: Record<string, unknown>): Record<string, unknown>;
514
+ interface FilterEvalOptions {
515
+ resolveRelation?: (resource: string, recordId: string, relationName: string) => Record<string, unknown>[];
516
+ resource?: string;
517
+ }
518
+ /**
519
+ * Evaluate a DFQL filter against a single record.
520
+ * Supports: $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $contains, $startsWith, $endsWith,
521
+ * $like, $ilike, $not_like, $not_ilike, $is_null, $is_not_null, $is_empty, $is_not_empty,
522
+ * $before, $after, $between, $not_between, $and, $or.
523
+ * Returns true if the record matches all filter conditions.
524
+ */
525
+ declare function evaluateFilter(record: Record<string, unknown>, filters: Record<string, unknown>, opts?: FilterEvalOptions, _depth?: number): boolean;
526
+
527
+ /**
528
+ * Shared relation payload normalization utilities.
529
+ * Used by both server mutation executor and offline client.
530
+ */
531
+ /**
532
+ * Normalized relation payload item.
533
+ */
534
+ interface NormalizedRelation {
535
+ toId: string;
536
+ metadata: Record<string, unknown>;
537
+ }
538
+ /**
539
+ * Normalize relation payload to consistent format.
540
+ * - String -> [{ toId, metadata: {} }]
541
+ * - String[] -> [{ toId, metadata: {} }, ...]
542
+ * - Object with $ref -> [{ toId: $ref, metadata: {...rest} }]
543
+ * - Array of above -> flatten
544
+ */
545
+ declare function normalizeRelationPayload(payload: unknown): NormalizedRelation[];
546
+
547
+ /**
548
+ * Shared aggregation computation.
549
+ * Used by both server and client offline aggregate query execution.
550
+ */
551
+ /**
552
+ * Calculate an aggregation over a set of records for a given op and field.
553
+ * Null-safe: null/undefined values are excluded from sum/min/max/avg.
554
+ * Returns null for empty sets (SQL semantics).
555
+ */
556
+ declare function calculateAggregation(op: string, field: string, records: Record<string, unknown>[]): unknown;
557
+
558
+ interface SortTerm {
559
+ field: string;
560
+ direction: "asc" | "desc";
561
+ }
562
+ /**
563
+ * Parse DFQL sort strings into structured sort terms.
564
+ * Handles "field", "field:asc", "field:desc", "-field".
565
+ * Returns [] for undefined or empty input.
566
+ */
567
+ declare function parseSortTerms(sort: string[] | undefined): SortTerm[];
568
+ /**
569
+ * Sort records by the given terms with a stable id tie-breaker.
570
+ * Null values sort after non-null in ascending order (before in descending).
571
+ */
572
+ declare function sortRecords(records: Record<string, unknown>[], terms: SortTerm[]): Record<string, unknown>[];
573
+
574
+ interface SelectToken {
575
+ path: string[];
576
+ baseName: string;
577
+ directive: string | undefined;
578
+ }
579
+ /**
580
+ * Parse a DFQL select token string into its structured components.
581
+ * Examples: "id" → {path:["id"], baseName:"id", directive:undefined}
582
+ * "tags.*" → {path:["tags","*"], baseName:"tags", directive:"*"}
583
+ * "tasks.tags.*" → {path:["tasks","tags","*"], baseName:"tasks", directive:"tags.*"}
584
+ */
585
+ declare function parseSelectToken(token: string): SelectToken;
586
+
587
+ /**
588
+ * Shared date conversion utilities (DTE-001, DTE-002)
589
+ */
590
+
591
+ /**
592
+ * Convert a Date, ISO string, or epoch number to epoch milliseconds.
593
+ * Idempotent: if already a number, returns it directly.
594
+ */
595
+ declare function toEpochMs(value: unknown): number;
596
+ /**
597
+ * Convert an epoch number, ISO string, or Date to a Date object.
598
+ * Idempotent: if already a Date, returns it directly.
599
+ */
600
+ declare function fromEpochMs(value: unknown): Date;
601
+ type FieldLike = Pick<DatafnFieldSchema, "name" | "type">;
602
+ /**
603
+ * Mutate record in place: convert date-typed fields to epoch milliseconds.
604
+ *
605
+ * LOW-024: This function intentionally mutates the record in-place and returns it
606
+ * (callers rely on the mutation side-effect, as verified by existing tests).
607
+ * If a non-mutating variant is needed, create a separate helper that copies first.
608
+ */
609
+ declare function coerceDateFieldsToEpoch(record: Record<string, unknown>, fields: FieldLike[]): Record<string, unknown>;
610
+ /**
611
+ * Mutate record in place: convert date-typed fields to Date objects.
612
+ *
613
+ * LOW-024: This function intentionally mutates the record in-place and returns it
614
+ * (callers rely on the mutation side-effect, as verified by existing tests).
615
+ * If a non-mutating variant is needed, create a separate helper that copies first.
616
+ */
617
+ declare function parseDateFieldsToDate(record: Record<string, unknown>, fields: FieldLike[]): Record<string, unknown>;
618
+
619
+ /**
620
+ * Shared validation primitives for DataFn.
621
+ * - Prototype pollution key checking (recursive)
622
+ * - Field value type validation against schema types
623
+ */
624
+ /**
625
+ * Recursively checks a value for prototype pollution keys.
626
+ * Returns `{ ok: true }` if clean, or `{ ok: false, key }` with the first offending key found.
627
+ */
628
+ declare function checkPrototypePollution(obj: unknown): {
629
+ ok: boolean;
630
+ key?: string;
631
+ };
632
+ /**
633
+ * Validates a field value against its declared schema type.
634
+ * Returns `{ ok: true }` if valid, or `{ ok: false, error }` with a descriptive message.
635
+ */
636
+ declare function validateFieldValue(schemaType: string, value: unknown, nullable: boolean): {
637
+ ok: boolean;
638
+ error?: string;
639
+ };
640
+
641
+ /**
642
+ * Generic plugin hook runner (HKS-001)
643
+ * Fail-closed for before hooks, fail-open for after hooks.
644
+ */
645
+
646
+ interface HookError {
647
+ code: string;
648
+ message: string;
649
+ details?: Record<string, unknown>;
650
+ }
651
+ type BeforeHookResult<T> = {
652
+ ok: true;
653
+ value: T;
654
+ } | {
655
+ ok: false;
656
+ error: HookError;
657
+ };
658
+ /**
659
+ * Run a before-style hook across plugins filtered by env.
660
+ * Fail-closed: first error stops the chain and returns { ok: false, error }.
661
+ * Each hook's return value (if defined) becomes the new payload.
662
+ *
663
+ * @param preArgs Optional args inserted before payload in hook call: hook(ctx, ...preArgs, payload)
664
+ */
665
+ declare function runBeforeHook<T = unknown>(plugins: DatafnPlugin[], env: "client" | "server", hookName: keyof DatafnPlugin, ctx: DatafnHookContext, payload: T, preArgs?: unknown[]): Promise<BeforeHookResult<T>>;
666
+ /**
667
+ * Run an after-style hook across plugins filtered by env.
668
+ * Fail-open: errors are logged but do not stop the chain.
669
+ *
670
+ * @param preArgs Optional args inserted before payload in hook call: hook(ctx, ...preArgs, payload, result)
671
+ */
672
+ declare function runAfterHook(plugins: DatafnPlugin[], env: "client" | "server", hookName: keyof DatafnPlugin, ctx: DatafnHookContext, payload: unknown, result: unknown, preArgs?: unknown[]): Promise<void>;
673
+
674
+ interface SearchProvider {
675
+ readonly name: string;
676
+ search(params: {
677
+ resource: string;
678
+ query: string;
679
+ type?: "fullText" | "semantic";
680
+ fields?: string[];
681
+ limit?: number;
682
+ prefix?: boolean;
683
+ fuzzy?: boolean | number;
684
+ fieldBoosts?: Record<string, number>;
685
+ signal?: AbortSignal;
686
+ }): Promise<string[]>;
687
+ searchAll?(params: {
688
+ query: string;
689
+ resources?: string[];
690
+ fields?: string[];
691
+ limit?: number;
692
+ limitPerResource?: number;
693
+ prefix?: boolean;
694
+ fuzzy?: boolean | number;
695
+ fieldBoosts?: Record<string, number>;
696
+ signal?: AbortSignal;
697
+ }): Promise<Array<{
698
+ resource: string;
699
+ id: string;
700
+ score: number;
701
+ }>>;
702
+ updateIndices(params: {
703
+ resource: string;
704
+ records: Record<string, unknown>[];
705
+ operation: "upsert" | "delete";
706
+ }): Promise<void>;
707
+ initialize?(config: {
708
+ resources: Array<{
709
+ name: string;
710
+ searchFields: string[];
711
+ }>;
712
+ }): Promise<void>;
713
+ dispose?(): Promise<void>;
714
+ }
715
+
716
+ export { type AccessLevel, type BeforeHookResult, CAPABILITY_FIELD_DEFS, type CapabilityEntry, type DatafnEnvelope, type DatafnError, type DatafnErrorCode, type DatafnEvent, type DatafnEventFilter, type DatafnFieldSchema, type DatafnHookContext, type DatafnLimitsConfig, type DatafnLogger, type DatafnPlugin, type DatafnRelationSchema, type DatafnResourceSchema, type DatafnSchema, type DatafnSignal, type DatafnWsConfig, type DfqlCursor, type DfqlMutation, type DfqlMutationFragment, type DfqlQuery, type DfqlQueryFragment, type DfqlSort, type DfqlTransact, type FilterEvalOptions, type HookError, KV_RESOURCE_NAME, type NormalizedRelation, OP_REMAP, RELATION_CAPABILITY_FIELD_DEFS, type RelationSimpleCapability, type ResourceCapabilities, type SchemaCapabilities, type SchemaIndex, type SearchProvider, type SelectToken, type ShareableCapability, type SimpleCapability, type SortTerm, buildSchemaIndex, calculateAggregation, checkPrototypePollution, coerceDateFieldsToEpoch, defineSchema, dfqlKey, ensureBuiltinKv, enumerateJoinStoreKeys, err, evaluateFilter, findRelationBidirectional, fromEpochMs, getCapabilityFields, getField, getJoinStoreKey, getJoinTableName, getRelation, getRelationCapabilityFieldNames, getRelationTarget, getRelationsFrom, getResource, isNamespaced, kvId, normalizeDfql, normalizeFilterOps, normalizeRelationPayload, ns, ok, parseDateFieldsToDate, parseSelectToken, parseSortTerms, resolveCapabilities, resolveRelationCapabilities, runAfterHook, runBeforeHook, sortRecords, toEpochMs, unwrapEnvelope, validateFieldValue, validateSchema };