@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @datafn/core
2
2
 
3
- Core types, schema validation, and DFQL normalization for DataFn.
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**: Complete TypeScript types for schemas, events, signals, and plugins
14
- - **Schema Validation**: Runtime validation of DataFn schemas
15
- - **DFQL Normalization**: Deterministic normalization for cache keys
16
- - **Error Handling**: Structured error types with envelopes
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
- ## API
21
+ ---
19
22
 
20
- ### Types
23
+ ## Schema Definition
21
24
 
22
- #### DatafnSchema
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
- Defines the complete data model including resources (tables) and their relationships.
36
+ ### DatafnResourceSchema
32
37
 
33
- #### DatafnResourceSchema
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
- fields: DatafnFieldSchema[];
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
- Defines a resource (table) with fields, version, and optional indices.
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
- #### DatafnEvent
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
- Event structure for mutation and sync operations.
194
+ ### DatafnEventFilter
71
195
 
72
- #### DatafnPlugin & DatafnHookContext
196
+ Filter which events you receive when subscribing.
73
197
 
74
198
  ```typescript
75
- type DatafnHookContext = {
76
- env: "client" | "server";
77
- schema: DatafnSchema;
78
- context?: unknown;
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
- beforeMutation?: (ctx: DatafnHookContext, m: unknown) => Promise<unknown> | unknown;
87
- afterMutation?: (ctx: DatafnHookContext, m: unknown, result: unknown) => Promise<void> | void;
88
- // ... hooks for sync (beforeSync, afterSync)
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
- Plugins allow intercepting and modifying queries, mutations, and sync operations.
273
+ ### DatafnHookContext
93
274
 
94
- ### Functions
275
+ ```typescript
276
+ type DatafnHookContext = {
277
+ env: "client" | "server";
278
+ schema: DatafnSchema;
279
+ context?: unknown;
280
+ };
281
+ ```
95
282
 
96
- #### validateSchema(schema: unknown): DatafnEnvelope<DatafnSchema>
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
- Validates a schema and returns an envelope. Use `unwrapEnvelope` to get the result or throw the error.
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
- #### unwrapEnvelope<T>(env: DatafnEnvelope<T>): T
400
+ ### normalizeDfql
121
401
 
122
- Helper to unwrap success result or throw error from an envelope.
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
- const normalized = normalizeDfql({ b: 2, a: 1, c: undefined });
130
- // Returns: { a: 1, b: 2 }
407
+ normalizeDfql({ b: 2, a: 1, c: undefined });
408
+ // { a: 1, b: 2 }
131
409
  ```
132
410
 
133
- Recursively sorts object keys and removes undefined values for deterministic comparison.
411
+ ### dfqlKey
134
412
 
135
- #### dfqlKey(dfql: unknown): string
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
- // Returns: deterministic JSON string for caching
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
- Generates a deterministic cache key from DFQL.
433
+ ### ok / err
434
+
435
+ Helpers to create envelopes.
145
436
 
146
- #### ok<T>(result: T): DatafnEnvelope<T>
147
- #### err(error: DatafnError): DatafnEnvelope<never>
437
+ ```typescript
438
+ import { ok, err } from "@datafn/core";
148
439
 
149
- Helpers to create success and error envelopes.
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
- ### Error Handling
447
+ ---
152
448
 
153
- DatafnError is a plain object interface, not a class.
449
+ ## KV Utilities
154
450
 
155
- #### DatafnError
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
- interface DatafnError {
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
- path: string;
163
- [key: string]: unknown;
164
- };
165
- }
491
+ details?: unknown;
492
+ };
166
493
  ```
167
494
 
168
- The `details.path` property is always present to indicate the location of the error (e.g., `"filters.status"` or `"$"`).
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
- **Example:**
512
+ ### DatafnEnvelope\<T\>
171
513
 
172
514
  ```typescript
173
- const error: DatafnError = {
174
- code: "DFQL_INVALID",
175
- message: "Invalid query filter",
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
- ## Schema Definition Example
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: "post",
530
+ name: "project",
192
531
  version: 1,
532
+ idPrefix: "proj",
193
533
  fields: [
194
- { name: "title", type: "string", required: true },
195
- { name: "content", type: "string", required: true },
196
- { name: "authorId", type: "string", required: true },
197
- { name: "publishedAt", type: "date", required: false },
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: "post",
208
- to: "user",
569
+ from: "task",
570
+ to: "project",
209
571
  type: "many-one",
210
- relation: "author",
211
- fkField: "authorId",
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