@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/README.md +465 -89
- package/dist/index.cjs +1178 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +567 -29
- package/dist/index.d.ts +567 -29
- package/dist/index.js +1141 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @datafn/core
|
|
2
2
|
|
|
3
|
-
Core types, schema validation,
|
|
3
|
+
Core types, schema validation, DFQL normalization, and shared utilities for the DataFn ecosystem. Every other DataFn package depends on `@datafn/core`.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -10,16 +10,21 @@ npm install @datafn/core
|
|
|
10
10
|
|
|
11
11
|
## Features
|
|
12
12
|
|
|
13
|
-
- **Type Definitions
|
|
14
|
-
- **Schema Validation
|
|
15
|
-
- **DFQL
|
|
16
|
-
- **
|
|
13
|
+
- **Type Definitions** — Complete TypeScript types for schemas, resources, fields, relations, events, signals, and plugins
|
|
14
|
+
- **Schema Validation** — Runtime validation and normalization of DataFn schemas
|
|
15
|
+
- **DFQL Types** — Query, mutation, and transaction type definitions
|
|
16
|
+
- **DFQL Normalization** — Deterministic normalization for stable cache keys
|
|
17
|
+
- **Envelope Pattern** — Structured `ok | error` result types with helper functions
|
|
18
|
+
- **KV Utilities** — Built-in key-value resource helpers (`ensureBuiltinKv`, `kvId`)
|
|
19
|
+
- **Error Codes** — Enumerated error codes for consistent error handling
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
---
|
|
19
22
|
|
|
20
|
-
|
|
23
|
+
## Schema Definition
|
|
21
24
|
|
|
22
|
-
|
|
25
|
+
A DataFn schema describes your entire data model: resources (tables), their fields, and the relationships between them.
|
|
26
|
+
|
|
27
|
+
### DatafnSchema
|
|
23
28
|
|
|
24
29
|
```typescript
|
|
25
30
|
type DatafnSchema = {
|
|
@@ -28,77 +33,349 @@ type DatafnSchema = {
|
|
|
28
33
|
};
|
|
29
34
|
```
|
|
30
35
|
|
|
31
|
-
|
|
36
|
+
### DatafnResourceSchema
|
|
32
37
|
|
|
33
|
-
|
|
38
|
+
Each resource maps to a table or collection in your database.
|
|
34
39
|
|
|
35
40
|
```typescript
|
|
36
41
|
type DatafnResourceSchema = {
|
|
42
|
+
/** Unique resource name (e.g. "todos", "users") */
|
|
37
43
|
name: string;
|
|
44
|
+
/** Schema version — increment when making breaking changes */
|
|
38
45
|
version: number;
|
|
39
|
-
|
|
46
|
+
/** Optional prefix for generated IDs (e.g. "todo" → "todo:uuid") */
|
|
40
47
|
idPrefix?: string;
|
|
48
|
+
/**
|
|
49
|
+
* When true, the resource is never stored locally.
|
|
50
|
+
* Queries always go to the remote server.
|
|
51
|
+
*/
|
|
41
52
|
isRemoteOnly?: boolean;
|
|
53
|
+
/** Field definitions */
|
|
54
|
+
fields: DatafnFieldSchema[];
|
|
55
|
+
/**
|
|
56
|
+
* Index hints for optimisation.
|
|
57
|
+
* Can be a simple string[] (treated as base indices) or a structured object.
|
|
58
|
+
*/
|
|
42
59
|
indices?:
|
|
43
60
|
| { base?: string[]; search?: string[]; vector?: string[] }
|
|
44
61
|
| string[];
|
|
62
|
+
/** Optional permissions policy for server-side authorization */
|
|
63
|
+
permissions?: DatafnPermissionsPolicy;
|
|
64
|
+
};
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### DatafnFieldSchema
|
|
68
|
+
|
|
69
|
+
Every field has a name, type, and a rich set of optional validation constraints.
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
type DatafnFieldSchema = {
|
|
73
|
+
name: string;
|
|
74
|
+
type: "string" | "number" | "boolean" | "object" | "array" | "date" | "file";
|
|
75
|
+
required: boolean;
|
|
76
|
+
/** Allow explicit null values */
|
|
77
|
+
nullable?: boolean;
|
|
78
|
+
/** Prevent mutation after initial insert */
|
|
79
|
+
readonly?: boolean;
|
|
80
|
+
/** Default value applied on insert when the field is omitted */
|
|
81
|
+
default?: unknown;
|
|
82
|
+
/** Restrict to a fixed set of allowed values */
|
|
83
|
+
enum?: unknown[];
|
|
84
|
+
/** Minimum numeric value or minimum array length */
|
|
85
|
+
min?: number;
|
|
86
|
+
/** Maximum numeric value or maximum array length */
|
|
87
|
+
max?: number;
|
|
88
|
+
/** Minimum string length */
|
|
89
|
+
minLength?: number;
|
|
90
|
+
/** Maximum string length */
|
|
91
|
+
maxLength?: number;
|
|
92
|
+
/** Regex pattern the string value must match */
|
|
93
|
+
pattern?: string;
|
|
94
|
+
/**
|
|
95
|
+
* Uniqueness constraint.
|
|
96
|
+
* - `true` — globally unique
|
|
97
|
+
* - `string` — unique within a composite group
|
|
98
|
+
*/
|
|
99
|
+
unique?: boolean | string;
|
|
100
|
+
/** Encrypt the field value at rest */
|
|
101
|
+
encrypt?: boolean;
|
|
102
|
+
/** Volatile fields are excluded from sync and persistence */
|
|
103
|
+
volatile?: boolean;
|
|
45
104
|
};
|
|
46
105
|
```
|
|
47
106
|
|
|
48
|
-
|
|
107
|
+
**Supported field types:**
|
|
108
|
+
|
|
109
|
+
| Type | Description |
|
|
110
|
+
|------|-------------|
|
|
111
|
+
| `string` | Text values |
|
|
112
|
+
| `number` | Numeric values (integer or float) |
|
|
113
|
+
| `boolean` | `true` / `false` |
|
|
114
|
+
| `object` | Arbitrary JSON objects (stored as JSONB / JSON) |
|
|
115
|
+
| `array` | Arbitrary JSON arrays (stored as JSONB / JSON) |
|
|
116
|
+
| `date` | Date/time values (stored as timestamps) |
|
|
117
|
+
| `file` | File references |
|
|
118
|
+
|
|
119
|
+
### DatafnRelationSchema
|
|
120
|
+
|
|
121
|
+
Relations describe how resources are connected.
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
type DatafnRelationSchema = {
|
|
125
|
+
/** Source resource name(s) */
|
|
126
|
+
from: string | string[];
|
|
127
|
+
/** Target resource name(s) */
|
|
128
|
+
to: string | string[];
|
|
129
|
+
/** Relation cardinality */
|
|
130
|
+
type: "one-many" | "many-one" | "many-many" | "htree";
|
|
131
|
+
/** Forward relation name (e.g. "tags") */
|
|
132
|
+
relation?: string;
|
|
133
|
+
/** Inverse relation name (e.g. "todos") */
|
|
134
|
+
inverse?: string;
|
|
135
|
+
/** Cache relation data for faster reads */
|
|
136
|
+
cache?: boolean;
|
|
137
|
+
/** Extra metadata fields on the join row (many-many only) */
|
|
138
|
+
metadata?: Array<{
|
|
139
|
+
name: string;
|
|
140
|
+
type: "string" | "number" | "boolean" | "date" | "object";
|
|
141
|
+
}>;
|
|
142
|
+
/** Foreign key field name (many-one / one-many) */
|
|
143
|
+
fkField?: string;
|
|
144
|
+
/** Materialized path field (htree) */
|
|
145
|
+
pathField?: string;
|
|
146
|
+
};
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Relation types:**
|
|
150
|
+
|
|
151
|
+
| Type | Description | Storage |
|
|
152
|
+
|------|-------------|---------|
|
|
153
|
+
| `one-many` | Parent has many children | FK on child |
|
|
154
|
+
| `many-one` | Child belongs to parent | FK on child |
|
|
155
|
+
| `many-many` | Both sides have many | Join table |
|
|
156
|
+
| `htree` | Hierarchical tree | Materialized path |
|
|
49
157
|
|
|
50
|
-
|
|
158
|
+
### DatafnPermissionsPolicy
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
type DatafnPermissionsPolicy = {
|
|
162
|
+
read?: { fields: string[] };
|
|
163
|
+
write?: { fields: string[] };
|
|
164
|
+
ownerField?: string;
|
|
165
|
+
};
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Events
|
|
171
|
+
|
|
172
|
+
Events represent lifecycle notifications for mutations and sync operations.
|
|
173
|
+
|
|
174
|
+
### DatafnEvent
|
|
51
175
|
|
|
52
176
|
```typescript
|
|
53
177
|
interface DatafnEvent {
|
|
54
178
|
type:
|
|
55
|
-
| "mutation_applied"
|
|
56
|
-
| "mutation_rejected"
|
|
57
|
-
| "sync_applied"
|
|
58
|
-
| "sync_failed";
|
|
59
|
-
resource?: string;
|
|
60
|
-
ids?: string[];
|
|
61
|
-
mutationId?: string;
|
|
62
|
-
clientId?: string;
|
|
63
|
-
timestampMs: number;
|
|
64
|
-
context?: unknown;
|
|
65
|
-
action?: string;
|
|
66
|
-
fields?: string[];
|
|
179
|
+
| "mutation_applied" // A mutation was successfully applied
|
|
180
|
+
| "mutation_rejected" // A mutation was rejected (validation, conflict, etc.)
|
|
181
|
+
| "sync_applied" // A sync operation completed successfully
|
|
182
|
+
| "sync_failed"; // A sync operation failed
|
|
183
|
+
resource?: string; // Affected resource name
|
|
184
|
+
ids?: string[]; // Affected record IDs
|
|
185
|
+
mutationId?: string; // Mutation identifier
|
|
186
|
+
clientId?: string; // Originating client identifier
|
|
187
|
+
timestampMs: number; // Event timestamp in milliseconds
|
|
188
|
+
context?: unknown; // Arbitrary context data
|
|
189
|
+
action?: string; // Mutation action (insert, merge, delete, etc.)
|
|
190
|
+
fields?: string[]; // Changed fields
|
|
67
191
|
}
|
|
68
192
|
```
|
|
69
193
|
|
|
70
|
-
|
|
194
|
+
### DatafnEventFilter
|
|
71
195
|
|
|
72
|
-
|
|
196
|
+
Filter which events you receive when subscribing.
|
|
73
197
|
|
|
74
198
|
```typescript
|
|
75
|
-
type
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
199
|
+
type DatafnEventFilter = Partial<{
|
|
200
|
+
type: DatafnEvent["type"] | Array<DatafnEvent["type"]>;
|
|
201
|
+
resource: string | string[];
|
|
202
|
+
ids: string | string[];
|
|
203
|
+
mutationId: string | string[];
|
|
204
|
+
action: string | string[];
|
|
205
|
+
fields: string | string[];
|
|
206
|
+
contextKeys: string[];
|
|
207
|
+
context: Record<string, unknown>;
|
|
208
|
+
}>;
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Signals
|
|
214
|
+
|
|
215
|
+
Signals are reactive data containers that represent live query results. They are the bridge between DataFn and your UI framework.
|
|
80
216
|
|
|
217
|
+
### DatafnSignal\<T\>
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
interface DatafnSignal<T> {
|
|
221
|
+
/** Get the current value synchronously */
|
|
222
|
+
get(): T;
|
|
223
|
+
/** Subscribe to value changes. Returns an unsubscribe function. */
|
|
224
|
+
subscribe(handler: (value: T) => void): () => void;
|
|
225
|
+
/** True while the initial fetch is in progress */
|
|
226
|
+
readonly loading: boolean;
|
|
227
|
+
/** Non-null if the last fetch/refresh failed */
|
|
228
|
+
readonly error: DatafnError | null;
|
|
229
|
+
/** True while a background refresh is in progress (after initial load) */
|
|
230
|
+
readonly refreshing: boolean;
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Plugins
|
|
237
|
+
|
|
238
|
+
Plugins intercept and extend queries, mutations, and sync operations on both client and server.
|
|
239
|
+
|
|
240
|
+
### DatafnPlugin
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
81
243
|
interface DatafnPlugin {
|
|
82
244
|
name: string;
|
|
83
245
|
runsOn: Array<"client" | "server">;
|
|
246
|
+
|
|
247
|
+
/** Intercept queries before execution. Return modified query or throw to reject. */
|
|
84
248
|
beforeQuery?: (ctx: DatafnHookContext, q: unknown) => Promise<unknown> | unknown;
|
|
249
|
+
/** Process query results. Return modified result. */
|
|
85
250
|
afterQuery?: (ctx: DatafnHookContext, q: unknown, result: unknown) => Promise<unknown> | unknown;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
251
|
+
|
|
252
|
+
/** Intercept mutations before execution. Return modified mutation or throw to reject. */
|
|
253
|
+
beforeMutation?: (ctx: DatafnHookContext, m: unknown | unknown[]) => Promise<unknown> | unknown;
|
|
254
|
+
/** React to mutation results. */
|
|
255
|
+
afterMutation?: (ctx: DatafnHookContext, m: unknown | unknown[], result: unknown) => Promise<void> | void;
|
|
256
|
+
|
|
257
|
+
/** Intercept sync operations before execution. Return modified payload or throw to reject. */
|
|
258
|
+
beforeSync?: (
|
|
259
|
+
ctx: DatafnHookContext,
|
|
260
|
+
phase: "seed" | "clone" | "pull" | "push" | "cloneUp" | "reconcile",
|
|
261
|
+
payload: unknown,
|
|
262
|
+
) => Promise<unknown> | unknown;
|
|
263
|
+
/** React to sync results. */
|
|
264
|
+
afterSync?: (
|
|
265
|
+
ctx: DatafnHookContext,
|
|
266
|
+
phase: "seed" | "clone" | "pull" | "push" | "cloneUp" | "reconcile",
|
|
267
|
+
payload: unknown,
|
|
268
|
+
result: unknown,
|
|
269
|
+
) => Promise<void> | void;
|
|
89
270
|
}
|
|
90
271
|
```
|
|
91
272
|
|
|
92
|
-
|
|
273
|
+
### DatafnHookContext
|
|
93
274
|
|
|
94
|
-
|
|
275
|
+
```typescript
|
|
276
|
+
type DatafnHookContext = {
|
|
277
|
+
env: "client" | "server";
|
|
278
|
+
schema: DatafnSchema;
|
|
279
|
+
context?: unknown;
|
|
280
|
+
};
|
|
281
|
+
```
|
|
95
282
|
|
|
96
|
-
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## DFQL Types
|
|
286
|
+
|
|
287
|
+
DFQL (DataFn Query Language) defines the structure of queries, mutations, and transactions.
|
|
288
|
+
|
|
289
|
+
### DfqlQuery
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
type DfqlQuery = {
|
|
293
|
+
resource: string;
|
|
294
|
+
version: number;
|
|
295
|
+
select?: string[]; // Fields to include
|
|
296
|
+
omit?: string[]; // Fields to exclude
|
|
297
|
+
filters?: Record<string, unknown>; // Where clause
|
|
298
|
+
search?: Record<string, unknown>; // Full-text search
|
|
299
|
+
sort?: DfqlSort; // Ordering (e.g. ["-createdAt", "name"])
|
|
300
|
+
limit?: number; // Max results
|
|
301
|
+
offset?: number; // Skip N results
|
|
302
|
+
cursor?: DfqlCursor; // Cursor-based pagination
|
|
303
|
+
count?: boolean; // Return count only
|
|
304
|
+
groupBy?: string[]; // Group by fields
|
|
305
|
+
aggregations?: Record<string, unknown>; // Aggregate functions
|
|
306
|
+
having?: Record<string, unknown>; // Having clause for groups
|
|
307
|
+
};
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### DfqlQueryFragment
|
|
311
|
+
|
|
312
|
+
Omits `resource` and `version` — used with the Table API where those are implicit.
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
type DfqlQueryFragment = Omit<DfqlQuery, "resource" | "version">;
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### DfqlMutation
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
type DfqlMutation = {
|
|
322
|
+
resource: string;
|
|
323
|
+
version: number;
|
|
324
|
+
operation: string; // "insert" | "merge" | "replace" | "delete" | "relate" | "unrelate" | "modifyRelation"
|
|
325
|
+
id?: string | string[]; // Target record ID(s)
|
|
326
|
+
record?: Record<string, unknown>;
|
|
327
|
+
records?: Array<Record<string, unknown>>;
|
|
328
|
+
clientId?: string; // For idempotency
|
|
329
|
+
mutationId?: string; // For idempotency
|
|
330
|
+
timestamp?: number | string;
|
|
331
|
+
context?: unknown;
|
|
332
|
+
relations?: Record<string, unknown>;
|
|
333
|
+
if?: Record<string, unknown>; // Optimistic concurrency guards
|
|
334
|
+
cascade?: unknown;
|
|
335
|
+
};
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### DfqlMutationFragment
|
|
339
|
+
|
|
340
|
+
Omits `resource` and `version` — used with the Table API.
|
|
341
|
+
|
|
342
|
+
```typescript
|
|
343
|
+
type DfqlMutationFragment = Omit<DfqlMutation, "resource" | "version">;
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### DfqlTransact
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
type DfqlTransact = {
|
|
350
|
+
transactionId?: string;
|
|
351
|
+
atomic?: boolean;
|
|
352
|
+
steps: Array<{ query?: DfqlQuery; mutation?: DfqlMutation }>;
|
|
353
|
+
};
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### Sort & Cursor
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
type DfqlSort = string[];
|
|
360
|
+
// e.g. ["name", "-createdAt"] → name ASC, createdAt DESC (prefix "-" = descending)
|
|
361
|
+
|
|
362
|
+
type DfqlCursor = {
|
|
363
|
+
after?: Record<string, unknown>;
|
|
364
|
+
before?: Record<string, unknown>;
|
|
365
|
+
};
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
## Functions
|
|
371
|
+
|
|
372
|
+
### validateSchema
|
|
373
|
+
|
|
374
|
+
Validates and normalizes a DataFn schema. Returns an envelope.
|
|
97
375
|
|
|
98
376
|
```typescript
|
|
99
377
|
import { validateSchema, unwrapEnvelope } from "@datafn/core";
|
|
100
378
|
|
|
101
|
-
// Validate and unwrap (throws if invalid)
|
|
102
379
|
const schema = unwrapEnvelope(
|
|
103
380
|
validateSchema({
|
|
104
381
|
resources: [
|
|
@@ -106,7 +383,7 @@ const schema = unwrapEnvelope(
|
|
|
106
383
|
name: "user",
|
|
107
384
|
version: 1,
|
|
108
385
|
fields: [
|
|
109
|
-
{ name: "email", type: "string", required: true },
|
|
386
|
+
{ name: "email", type: "string", required: true, unique: true },
|
|
110
387
|
{ name: "name", type: "string", required: true },
|
|
111
388
|
],
|
|
112
389
|
},
|
|
@@ -115,100 +392,199 @@ const schema = unwrapEnvelope(
|
|
|
115
392
|
);
|
|
116
393
|
```
|
|
117
394
|
|
|
118
|
-
|
|
395
|
+
**Normalization applied:**
|
|
396
|
+
- Converts `indices: string[]` → `{ base: string[], search: [], vector: [] }`
|
|
397
|
+
- Defaults `relations` to `[]` if omitted
|
|
398
|
+
- Validates unique resource names, field names, and required properties
|
|
119
399
|
|
|
120
|
-
|
|
400
|
+
### normalizeDfql
|
|
121
401
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
#### normalizeDfql(dfql: unknown): unknown
|
|
402
|
+
Recursively normalizes a value for deterministic comparison: sorts object keys, removes `undefined` values, preserves primitives, arrays, and `null`.
|
|
125
403
|
|
|
126
404
|
```typescript
|
|
127
405
|
import { normalizeDfql } from "@datafn/core";
|
|
128
406
|
|
|
129
|
-
|
|
130
|
-
//
|
|
407
|
+
normalizeDfql({ b: 2, a: 1, c: undefined });
|
|
408
|
+
// → { a: 1, b: 2 }
|
|
131
409
|
```
|
|
132
410
|
|
|
133
|
-
|
|
411
|
+
### dfqlKey
|
|
134
412
|
|
|
135
|
-
|
|
413
|
+
Returns a stable JSON string for a DFQL value. Used as cache keys for signals.
|
|
136
414
|
|
|
137
415
|
```typescript
|
|
138
416
|
import { dfqlKey } from "@datafn/core";
|
|
139
417
|
|
|
140
418
|
const key = dfqlKey({ resource: "user", filters: { id: "user:1" } });
|
|
141
|
-
//
|
|
419
|
+
// Deterministic JSON string suitable for Map/Set keys
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### unwrapEnvelope
|
|
423
|
+
|
|
424
|
+
Unwraps a `DatafnEnvelope`: returns `result` on success, throws the `DatafnError` on failure.
|
|
425
|
+
|
|
426
|
+
```typescript
|
|
427
|
+
import { unwrapEnvelope } from "@datafn/core";
|
|
428
|
+
|
|
429
|
+
const result = unwrapEnvelope(someEnvelope);
|
|
430
|
+
// Throws DatafnError if envelope.ok === false
|
|
142
431
|
```
|
|
143
432
|
|
|
144
|
-
|
|
433
|
+
### ok / err
|
|
434
|
+
|
|
435
|
+
Helpers to create envelopes.
|
|
145
436
|
|
|
146
|
-
|
|
147
|
-
|
|
437
|
+
```typescript
|
|
438
|
+
import { ok, err } from "@datafn/core";
|
|
148
439
|
|
|
149
|
-
|
|
440
|
+
const success = ok({ data: [1, 2, 3] });
|
|
441
|
+
// { ok: true, result: { data: [1, 2, 3] } }
|
|
442
|
+
|
|
443
|
+
const failure = err("NOT_FOUND", "User not found", { path: "users" });
|
|
444
|
+
// { ok: false, error: { code: "NOT_FOUND", message: "User not found", details: { path: "users" } } }
|
|
445
|
+
```
|
|
150
446
|
|
|
151
|
-
|
|
447
|
+
---
|
|
152
448
|
|
|
153
|
-
|
|
449
|
+
## KV Utilities
|
|
154
450
|
|
|
155
|
-
|
|
451
|
+
The built-in KV (key-value) resource provides a schemaless store that syncs alongside your typed resources.
|
|
452
|
+
|
|
453
|
+
### ensureBuiltinKv
|
|
454
|
+
|
|
455
|
+
Ensures the schema includes the built-in `kv` resource. If it already exists, validates its shape; otherwise appends it.
|
|
156
456
|
|
|
157
457
|
```typescript
|
|
158
|
-
|
|
458
|
+
import { ensureBuiltinKv } from "@datafn/core";
|
|
459
|
+
|
|
460
|
+
const schemaWithKv = ensureBuiltinKv(mySchema);
|
|
461
|
+
// Now includes { name: "kv", version: 1, fields: [{ name: "value", type: "object" }] }
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
### kvId
|
|
465
|
+
|
|
466
|
+
Converts a plain key string to the canonical KV record ID.
|
|
467
|
+
|
|
468
|
+
```typescript
|
|
469
|
+
import { kvId } from "@datafn/core";
|
|
470
|
+
|
|
471
|
+
kvId("theme"); // → "kv:theme"
|
|
472
|
+
kvId("user:prefs"); // → "kv:user:prefs"
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
### KV_RESOURCE_NAME
|
|
476
|
+
|
|
477
|
+
The canonical resource name for the built-in KV store: `"kv"`.
|
|
478
|
+
|
|
479
|
+
---
|
|
480
|
+
|
|
481
|
+
## Error Handling
|
|
482
|
+
|
|
483
|
+
### DatafnError
|
|
484
|
+
|
|
485
|
+
A plain object (not a class) with a structured shape.
|
|
486
|
+
|
|
487
|
+
```typescript
|
|
488
|
+
type DatafnError = {
|
|
159
489
|
code: DatafnErrorCode;
|
|
160
490
|
message: string;
|
|
161
|
-
details?:
|
|
162
|
-
|
|
163
|
-
[key: string]: unknown;
|
|
164
|
-
};
|
|
165
|
-
}
|
|
491
|
+
details?: unknown;
|
|
492
|
+
};
|
|
166
493
|
```
|
|
167
494
|
|
|
168
|
-
|
|
495
|
+
### DatafnErrorCode
|
|
496
|
+
|
|
497
|
+
```typescript
|
|
498
|
+
type DatafnErrorCode =
|
|
499
|
+
| "SCHEMA_INVALID"
|
|
500
|
+
| "DFQL_INVALID"
|
|
501
|
+
| "DFQL_UNKNOWN_RESOURCE"
|
|
502
|
+
| "DFQL_UNKNOWN_FIELD"
|
|
503
|
+
| "DFQL_UNKNOWN_RELATION"
|
|
504
|
+
| "DFQL_UNSUPPORTED"
|
|
505
|
+
| "LIMIT_EXCEEDED"
|
|
506
|
+
| "FORBIDDEN"
|
|
507
|
+
| "NOT_FOUND"
|
|
508
|
+
| "CONFLICT"
|
|
509
|
+
| "INTERNAL";
|
|
510
|
+
```
|
|
169
511
|
|
|
170
|
-
|
|
512
|
+
### DatafnEnvelope\<T\>
|
|
171
513
|
|
|
172
514
|
```typescript
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
details: {
|
|
177
|
-
path: "filters.status",
|
|
178
|
-
expected: ["active", "archived"]
|
|
179
|
-
}
|
|
180
|
-
};
|
|
515
|
+
type DatafnEnvelope<T> =
|
|
516
|
+
| { ok: true; result: T }
|
|
517
|
+
| { ok: false; error: DatafnError };
|
|
181
518
|
```
|
|
182
519
|
|
|
183
|
-
|
|
520
|
+
---
|
|
521
|
+
|
|
522
|
+
## Full Schema Example
|
|
184
523
|
|
|
185
524
|
```typescript
|
|
186
|
-
import { DatafnSchema } from "@datafn/core";
|
|
525
|
+
import type { DatafnSchema } from "@datafn/core";
|
|
187
526
|
|
|
188
527
|
const schema: DatafnSchema = {
|
|
189
528
|
resources: [
|
|
190
529
|
{
|
|
191
|
-
name: "
|
|
530
|
+
name: "project",
|
|
192
531
|
version: 1,
|
|
532
|
+
idPrefix: "proj",
|
|
193
533
|
fields: [
|
|
194
|
-
{ name: "
|
|
195
|
-
{ name: "
|
|
196
|
-
{ name: "
|
|
197
|
-
{ name: "
|
|
534
|
+
{ name: "id", type: "string", required: true, unique: true },
|
|
535
|
+
{ name: "name", type: "string", required: true, maxLength: 200 },
|
|
536
|
+
{ name: "description", type: "string", required: false },
|
|
537
|
+
{ name: "ownerId", type: "string", required: true },
|
|
538
|
+
{ name: "createdAt", type: "date", required: true },
|
|
539
|
+
],
|
|
540
|
+
indices: { base: ["ownerId", "createdAt"] },
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
name: "task",
|
|
544
|
+
version: 1,
|
|
545
|
+
idPrefix: "task",
|
|
546
|
+
fields: [
|
|
547
|
+
{ name: "id", type: "string", required: true, unique: true },
|
|
548
|
+
{ name: "title", type: "string", required: true, minLength: 1 },
|
|
549
|
+
{ name: "completed", type: "boolean", required: true, default: false },
|
|
550
|
+
{ name: "priority", type: "number", required: false, min: 1, max: 5 },
|
|
551
|
+
{ name: "dueDate", type: "date", required: false },
|
|
552
|
+
{ name: "projectId", type: "string", required: true },
|
|
553
|
+
],
|
|
554
|
+
indices: { base: ["projectId", "completed"] },
|
|
555
|
+
},
|
|
556
|
+
{
|
|
557
|
+
name: "tag",
|
|
558
|
+
version: 1,
|
|
559
|
+
idPrefix: "tag",
|
|
560
|
+
fields: [
|
|
561
|
+
{ name: "id", type: "string", required: true, unique: true },
|
|
562
|
+
{ name: "name", type: "string", required: true, unique: true },
|
|
563
|
+
{ name: "color", type: "string", required: false },
|
|
198
564
|
],
|
|
199
|
-
indices: {
|
|
200
|
-
base: ["authorId"],
|
|
201
|
-
search: ["title", "content"],
|
|
202
|
-
},
|
|
203
565
|
},
|
|
204
566
|
],
|
|
205
567
|
relations: [
|
|
206
568
|
{
|
|
207
|
-
from: "
|
|
208
|
-
to: "
|
|
569
|
+
from: "task",
|
|
570
|
+
to: "project",
|
|
209
571
|
type: "many-one",
|
|
210
|
-
relation: "
|
|
211
|
-
fkField: "
|
|
572
|
+
relation: "project",
|
|
573
|
+
fkField: "projectId",
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
from: "task",
|
|
577
|
+
to: "tag",
|
|
578
|
+
type: "many-many",
|
|
579
|
+
relation: "tags",
|
|
580
|
+
inverse: "tasks",
|
|
581
|
+
},
|
|
582
|
+
{
|
|
583
|
+
from: "project",
|
|
584
|
+
to: "task",
|
|
585
|
+
type: "one-many",
|
|
586
|
+
relation: "tasks",
|
|
587
|
+
inverse: "project",
|
|
212
588
|
},
|
|
213
589
|
],
|
|
214
590
|
};
|
|
@@ -216,4 +592,4 @@ const schema: DatafnSchema = {
|
|
|
216
592
|
|
|
217
593
|
## License
|
|
218
594
|
|
|
219
|
-
MIT
|
|
595
|
+
MIT
|