ignotum 0.0.6 → 0.0.8
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 +85 -101
- package/dist/cli/bin.mjs +1487 -168
- package/dist/cli/bin.mjs.map +1 -1
- package/dist/runtime/{api-DtX8qPrq.js → api-BTeMI5tx.js} +27 -3
- package/dist/runtime/api-BTeMI5tx.js.map +1 -0
- package/dist/runtime/{api-D2bsAz4T.d.ts → api-D9lV-5av.d.ts} +7 -10
- package/dist/runtime/client.d.ts +27 -3
- package/dist/runtime/client.js +468 -68
- package/dist/runtime/client.js.map +1 -1
- package/dist/runtime/{descriptor-t6BOEGw9-BTHR-ZMv.js → descriptor-C5VA9qRl-C338iC6l.js} +63 -5
- package/dist/runtime/descriptor-C5VA9qRl-C338iC6l.js.map +1 -0
- package/dist/runtime/file-BXf63ulU.js +166 -0
- package/dist/runtime/file-BXf63ulU.js.map +1 -0
- package/dist/runtime/{result-BgcHn25t.d.ts → id-Btwac71X-DGj0DQuu.d.ts} +56 -4
- package/dist/runtime/{index-Dl_VQGmA.d.ts → index-CF04_Dps.d.ts} +96 -86
- package/dist/runtime/internal/api.d.ts +2 -2
- package/dist/runtime/internal/api.js +1 -1
- package/dist/runtime/internal/host.d.ts +25 -8
- package/dist/runtime/internal/host.js +209 -15
- package/dist/runtime/internal/host.js.map +1 -1
- package/dist/runtime/internal/server.d.ts +1 -1
- package/dist/runtime/internal/server.js +1 -1
- package/dist/runtime/internal/types.d.ts +1 -1
- package/dist/runtime/internal/types.js +1 -1
- package/dist/runtime/pagination-BNFhAjns.d.ts +1 -0
- package/dist/runtime/pagination-DrOowBve-Bnipd34u.d.ts +84 -0
- package/dist/runtime/{schema-B-jMZEbs.js → schema-DJfwfq87.js} +108 -30
- package/dist/runtime/schema-DJfwfq87.js.map +1 -0
- package/dist/runtime/server.d.ts +3 -3
- package/dist/runtime/server.js +2 -2
- package/dist/runtime/server.js.map +1 -1
- package/dist/runtime/sync-mIXn9eKv.d.ts +8 -0
- package/package.json +2 -2
- package/src/cli/agent-files.ts +56 -13
- package/src/cli/app-configuration.ts +4 -1
- package/src/cli/build/server.ts +83 -6
- package/src/cli/new-app.ts +15 -0
- package/src/client/files.ts +168 -0
- package/src/client/hooks.ts +166 -17
- package/src/client/index.ts +9 -1
- package/src/client/sync.ts +137 -21
- package/src/dev-runtime/database.ts +621 -78
- package/src/dev-runtime/files.ts +338 -0
- package/src/dev-runtime/functions.ts +14 -6
- package/src/dev-runtime/migrations.ts +44 -0
- package/src/dev-runtime/sync.ts +123 -5
- package/src/internal/api.ts +16 -1
- package/src/server/index.ts +8 -1
- package/dist/runtime/api-DtX8qPrq.js.map +0 -1
- package/dist/runtime/descriptor-t6BOEGw9-BTHR-ZMv.js.map +0 -1
- package/dist/runtime/id-Btwac71X-B9OeBzvq.d.ts +0 -9
- package/dist/runtime/id-D570vudg.js +0 -26
- package/dist/runtime/id-D570vudg.js.map +0 -1
- package/dist/runtime/schema-B-jMZEbs.js.map +0 -1
|
@@ -11,8 +11,27 @@ import {
|
|
|
11
11
|
dependencyKey,
|
|
12
12
|
documentDependency,
|
|
13
13
|
tableDependency,
|
|
14
|
-
|
|
14
|
+
indexPointInvalidation,
|
|
15
|
+
indexRangeDependency,
|
|
16
|
+
type ReadDependency,
|
|
17
|
+
type WriteInvalidation,
|
|
15
18
|
} from "@ignotum/contracts/runtime/hosted";
|
|
19
|
+
import {
|
|
20
|
+
encodeIndexKey,
|
|
21
|
+
encodeIndexRange,
|
|
22
|
+
EncodedIndexKey,
|
|
23
|
+
IndexScalarValue,
|
|
24
|
+
indexIdentity,
|
|
25
|
+
successorIndexPrefix,
|
|
26
|
+
type IndexScalar,
|
|
27
|
+
} from "@ignotum/contracts/runtime/index";
|
|
28
|
+
import {
|
|
29
|
+
decodePaginationCursor,
|
|
30
|
+
encodePaginationCursor,
|
|
31
|
+
paginationQueryIdentity,
|
|
32
|
+
type PaginationOptions,
|
|
33
|
+
type PaginationPage,
|
|
34
|
+
} from "@ignotum/contracts/runtime/pagination";
|
|
16
35
|
import {
|
|
17
36
|
documentNotFound,
|
|
18
37
|
resultFromEffect,
|
|
@@ -23,7 +42,12 @@ import type {
|
|
|
23
42
|
RuntimeSchemaDefinition,
|
|
24
43
|
RuntimeTableDefinition,
|
|
25
44
|
} from "@ignotum/contracts/runtime/schema";
|
|
26
|
-
import {
|
|
45
|
+
import {
|
|
46
|
+
getValueDescriptor,
|
|
47
|
+
SchemaDefinitionTypeId,
|
|
48
|
+
type ValueDescriptor,
|
|
49
|
+
} from "@ignotum/contracts/schema";
|
|
50
|
+
import type { FileValue } from "@ignotum/contracts/schema/file";
|
|
27
51
|
|
|
28
52
|
import { DevelopmentDatabase } from "./migrations.js";
|
|
29
53
|
|
|
@@ -36,6 +60,7 @@ type RuntimeDocumentValue =
|
|
|
36
60
|
| number
|
|
37
61
|
| string
|
|
38
62
|
| Date
|
|
63
|
+
| FileValue
|
|
39
64
|
| ReadonlyArray<RuntimeDocumentValue>
|
|
40
65
|
| RuntimeObject;
|
|
41
66
|
type RuntimeValue = RuntimeObject;
|
|
@@ -47,19 +72,46 @@ export interface RuntimeDocument {
|
|
|
47
72
|
readonly updatedAt: Date;
|
|
48
73
|
}
|
|
49
74
|
|
|
75
|
+
interface LocalIndexRange {
|
|
76
|
+
readonly eq: (field: string, value: RuntimeDocumentValue) => LocalIndexRange;
|
|
77
|
+
readonly gt: (field: string, value: RuntimeDocumentValue) => LocalIndexRange;
|
|
78
|
+
readonly gte: (field: string, value: RuntimeDocumentValue) => LocalIndexRange;
|
|
79
|
+
readonly lt: (field: string, value: RuntimeDocumentValue) => LocalIndexRange;
|
|
80
|
+
readonly lte: (field: string, value: RuntimeDocumentValue) => LocalIndexRange;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface LocalDatabaseQuery {
|
|
84
|
+
readonly index: (
|
|
85
|
+
name: string,
|
|
86
|
+
range?: (builder: LocalIndexRange) => LocalIndexRange,
|
|
87
|
+
) => LocalDatabaseQuery;
|
|
88
|
+
readonly order: (direction: "asc" | "desc") => LocalDatabaseQuery;
|
|
89
|
+
readonly collect: () => Result<ReadonlyArray<RuntimeDocument>, never>;
|
|
90
|
+
readonly take: (count: number) => Result<ReadonlyArray<RuntimeDocument>, never>;
|
|
91
|
+
readonly first: () => Result<RuntimeDocument | undefined, never>;
|
|
92
|
+
readonly unique: () => Result<RuntimeDocument | undefined, never>;
|
|
93
|
+
readonly paginate: (options: PaginationOptions) => Result<PaginationPage<RuntimeDocument>, never>;
|
|
94
|
+
}
|
|
95
|
+
|
|
50
96
|
export interface LocalDatabaseReader {
|
|
51
97
|
readonly find: (tableName: string, id: string) => Result<RuntimeDocument | undefined, never>;
|
|
52
98
|
readonly get: (tableName: string, id: string) => Result<RuntimeDocument, DocumentNotFoundValue>;
|
|
53
|
-
readonly query: (tableName: string) =>
|
|
54
|
-
readonly collect: () => Result<ReadonlyArray<RuntimeDocument>, never>;
|
|
55
|
-
};
|
|
99
|
+
readonly query: (tableName: string) => LocalDatabaseQuery;
|
|
56
100
|
}
|
|
57
101
|
|
|
58
102
|
export interface LocalDatabaseWriter extends LocalDatabaseReader {
|
|
59
|
-
readonly delete: (tableName: string, id: string) => Result<void,
|
|
103
|
+
readonly delete: (tableName: string, id: string) => Result<void, DocumentNotFoundValue>;
|
|
60
104
|
readonly insert: (tableName: string, value: RuntimeValue) => Result<GeneratedIdType, never>;
|
|
61
|
-
readonly patch: (
|
|
62
|
-
|
|
105
|
+
readonly patch: (
|
|
106
|
+
tableName: string,
|
|
107
|
+
id: string,
|
|
108
|
+
value: RuntimeValue,
|
|
109
|
+
) => Result<void, DocumentNotFoundValue>;
|
|
110
|
+
readonly replace: (
|
|
111
|
+
tableName: string,
|
|
112
|
+
id: string,
|
|
113
|
+
value: RuntimeValue,
|
|
114
|
+
) => Result<void, DocumentNotFoundValue>;
|
|
63
115
|
}
|
|
64
116
|
|
|
65
117
|
export interface LocalQueryContext {
|
|
@@ -82,7 +134,7 @@ interface LocalDatabaseService {
|
|
|
82
134
|
) => Effect.Effect<
|
|
83
135
|
{
|
|
84
136
|
readonly value: Success;
|
|
85
|
-
readonly dependencies: ReadonlyArray<
|
|
137
|
+
readonly dependencies: ReadonlyArray<ReadDependency>;
|
|
86
138
|
readonly observedRevision: AppStateRevision;
|
|
87
139
|
},
|
|
88
140
|
Failure | SqlError.SqlError
|
|
@@ -97,7 +149,7 @@ interface LocalDatabaseService {
|
|
|
97
149
|
) => Effect.Effect<
|
|
98
150
|
{
|
|
99
151
|
readonly value: Success;
|
|
100
|
-
readonly invalidations: ReadonlyArray<
|
|
152
|
+
readonly invalidations: ReadonlyArray<WriteInvalidation>;
|
|
101
153
|
readonly committedRevision: AppStateRevision;
|
|
102
154
|
},
|
|
103
155
|
Failure | SqlError.SqlError
|
|
@@ -111,6 +163,15 @@ const StoredDocument = Schema.Struct({
|
|
|
111
163
|
fields: Schema.String,
|
|
112
164
|
});
|
|
113
165
|
type StoredDocument = typeof StoredDocument.Type;
|
|
166
|
+
const IndexedStoredDocument = Schema.Struct({ ...StoredDocument.fields, key: Schema.String });
|
|
167
|
+
type IndexedStoredDocument = typeof IndexedStoredDocument.Type;
|
|
168
|
+
const StoredIndex = Schema.Struct({
|
|
169
|
+
id: Schema.Natural,
|
|
170
|
+
identity: Schema.String,
|
|
171
|
+
name: Schema.String,
|
|
172
|
+
fields: Schema.String,
|
|
173
|
+
});
|
|
174
|
+
type StoredIndex = typeof StoredIndex.Type;
|
|
114
175
|
|
|
115
176
|
const StoredTable = Schema.Struct({ id: TableId, name: Schema.String });
|
|
116
177
|
type StoredTable = typeof StoredTable.Type;
|
|
@@ -128,13 +189,20 @@ const TableInsert = Schema.Struct({ id: TableId, name: Schema.String });
|
|
|
128
189
|
const RevisionRow = Schema.Struct({ revision: AppStateRevision });
|
|
129
190
|
type DatabaseOperation = "collect" | "get" | "insert" | "patch" | "replace";
|
|
130
191
|
|
|
131
|
-
interface
|
|
132
|
-
readonly
|
|
133
|
-
readonly
|
|
192
|
+
interface ResolvedIndex extends StoredIndex {
|
|
193
|
+
readonly definition: { readonly name: string; readonly fields: readonly string[] };
|
|
194
|
+
readonly descriptors: ReadonlyArray<ValueDescriptor>;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
interface DependencyRecorder<Dependency extends ReadDependency | WriteInvalidation> {
|
|
198
|
+
readonly record: (...dependencies: ReadonlyArray<Dependency>) => void;
|
|
199
|
+
readonly values: () => ReadonlyArray<Dependency>;
|
|
134
200
|
}
|
|
135
201
|
|
|
136
|
-
const makeDependencyRecorder =
|
|
137
|
-
|
|
202
|
+
const makeDependencyRecorder = <
|
|
203
|
+
Dependency extends ReadDependency | WriteInvalidation,
|
|
204
|
+
>(): DependencyRecorder<Dependency> => {
|
|
205
|
+
const dependencies = new Map<string, Dependency>();
|
|
138
206
|
return {
|
|
139
207
|
record: (...added) => {
|
|
140
208
|
for (const dependency of added) dependencies.set(dependencyKey(dependency), dependency);
|
|
@@ -240,6 +308,25 @@ const toRuntimeDocument = Effect.fn("LocalDatabase.toRuntimeDocument")(function*
|
|
|
240
308
|
const nextUpdatedAt = (now: DateTime.DateTime, previous: number): number =>
|
|
241
309
|
Math.max(DateTime.toEpochMillis(now), previous + 1);
|
|
242
310
|
|
|
311
|
+
const decodeIndexScalar = Schema.decodeUnknownSync(IndexScalarValue);
|
|
312
|
+
|
|
313
|
+
const indexDescriptors = (
|
|
314
|
+
table: RuntimeTableDefinition,
|
|
315
|
+
fields: ReadonlyArray<string>,
|
|
316
|
+
): ReadonlyArray<ValueDescriptor> =>
|
|
317
|
+
fields.map((field) => {
|
|
318
|
+
const schema = table.fields[field];
|
|
319
|
+
const descriptor = schema === undefined ? undefined : getValueDescriptor(schema);
|
|
320
|
+
if (descriptor === undefined) throw new Error(`Index field '${field}' has no descriptor.`);
|
|
321
|
+
return descriptor;
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const storedIndexKey = (index: ResolvedIndex, row: StoredDocument): EncodedIndexKey => {
|
|
325
|
+
const fields = Schema.decodeSync(Schema.fromJsonString(Schema.JsonObject))(row.fields);
|
|
326
|
+
const values = index.definition.fields.map((field) => decodeIndexScalar(fields[field]));
|
|
327
|
+
return encodeIndexKey(index.descriptors, values, row.createdAt, row.id);
|
|
328
|
+
};
|
|
329
|
+
|
|
243
330
|
export class LocalDatabase extends Context.Service<LocalDatabase, LocalDatabaseService>()(
|
|
244
331
|
"ignotum/dev-runtime/database/LocalDatabase",
|
|
245
332
|
) {
|
|
@@ -291,6 +378,31 @@ export class LocalDatabase extends Context.Service<LocalDatabase, LocalDatabaseS
|
|
|
291
378
|
ORDER BY createdAt ASC, id ASC
|
|
292
379
|
`,
|
|
293
380
|
});
|
|
381
|
+
const listIndexes = SqlSchema.findAll({
|
|
382
|
+
Request: TableLookup,
|
|
383
|
+
Result: StoredIndex,
|
|
384
|
+
execute: ({ tableId }) => sql`
|
|
385
|
+
SELECT id, identity, name, fields
|
|
386
|
+
FROM indexes
|
|
387
|
+
WHERE tableId = ${tableId}
|
|
388
|
+
ORDER BY id ASC
|
|
389
|
+
`,
|
|
390
|
+
});
|
|
391
|
+
const insertIndex = SqlSchema.findOne({
|
|
392
|
+
Request: Schema.Struct({
|
|
393
|
+
tableId: TableId,
|
|
394
|
+
identity: Schema.String,
|
|
395
|
+
name: Schema.String,
|
|
396
|
+
fields: Schema.String,
|
|
397
|
+
}),
|
|
398
|
+
Result: StoredIndex,
|
|
399
|
+
execute: ({ tableId, identity, name, fields }) => sql`
|
|
400
|
+
INSERT INTO indexes (tableId, identity, name, fields)
|
|
401
|
+
VALUES (${tableId}, ${identity}, ${name}, ${fields})
|
|
402
|
+
ON CONFLICT (identity) DO UPDATE SET identity = excluded.identity
|
|
403
|
+
RETURNING id, identity, name, fields
|
|
404
|
+
`,
|
|
405
|
+
});
|
|
294
406
|
const currentRevision = SqlSchema.findOne({
|
|
295
407
|
Request: Schema.Void,
|
|
296
408
|
Result: RevisionRow,
|
|
@@ -324,11 +436,96 @@ export class LocalDatabase extends Context.Service<LocalDatabase, LocalDatabaseS
|
|
|
324
436
|
return { ...stored, definition };
|
|
325
437
|
});
|
|
326
438
|
|
|
439
|
+
const resolvedIndexes = Effect.fn("LocalDatabase.resolvedIndexes")(function* (
|
|
440
|
+
table: ResolvedTable,
|
|
441
|
+
) {
|
|
442
|
+
const stored = yield* listIndexes({ tableId: table.id });
|
|
443
|
+
return Object.values(table.definition.indexes).map((definition) => {
|
|
444
|
+
const identity = indexIdentity(table.id, definition.name, definition.fields);
|
|
445
|
+
const row = stored.find((candidate) => candidate.identity === identity);
|
|
446
|
+
if (row === undefined) throw new Error(`Index '${definition.name}' is not prepared.`);
|
|
447
|
+
return {
|
|
448
|
+
...row,
|
|
449
|
+
definition,
|
|
450
|
+
descriptors: indexDescriptors(table.definition, definition.fields),
|
|
451
|
+
} satisfies ResolvedIndex;
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
const prepareIndexes = Effect.fn("LocalDatabase.prepareIndexes")(function* (
|
|
456
|
+
schema: RuntimeSchemaDefinition,
|
|
457
|
+
) {
|
|
458
|
+
for (const [tableName, definition] of Object.entries(schema[SchemaDefinitionTypeId])) {
|
|
459
|
+
const table = yield* resolveTable(schema, tableName);
|
|
460
|
+
const stored = yield* listIndexes({ tableId: table.id });
|
|
461
|
+
const expected = new Set<string>();
|
|
462
|
+
for (const index of Object.values(definition.indexes)) {
|
|
463
|
+
const identity = indexIdentity(table.id, index.name, index.fields);
|
|
464
|
+
expected.add(identity);
|
|
465
|
+
if (stored.some((candidate) => candidate.identity === identity)) continue;
|
|
466
|
+
const fields = JSON.stringify(index.fields);
|
|
467
|
+
const created = yield* insertIndex({
|
|
468
|
+
tableId: table.id,
|
|
469
|
+
identity,
|
|
470
|
+
name: index.name,
|
|
471
|
+
fields,
|
|
472
|
+
}).pipe(Effect.catchTags({ NoSuchElementError: Effect.die }));
|
|
473
|
+
const resolved: ResolvedIndex = {
|
|
474
|
+
...created,
|
|
475
|
+
definition: index,
|
|
476
|
+
descriptors: indexDescriptors(definition, index.fields),
|
|
477
|
+
};
|
|
478
|
+
const documents = yield* collectStored({ tableId: table.id });
|
|
479
|
+
yield* Effect.forEach(
|
|
480
|
+
documents,
|
|
481
|
+
(document) =>
|
|
482
|
+
sql`
|
|
483
|
+
INSERT INTO indexEntries (indexId, tableId, documentId, key)
|
|
484
|
+
VALUES (${resolved.id}, ${table.id}, ${document.id}, ${storedIndexKey(resolved, document)})
|
|
485
|
+
`,
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
yield* Effect.forEach(
|
|
489
|
+
stored.filter((index) => !expected.has(index.identity)),
|
|
490
|
+
(index) => sql`DELETE FROM indexes WHERE id = ${index.id}`,
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
const indexKeys = Effect.fn("LocalDatabase.indexKeys")(function* (
|
|
496
|
+
table: ResolvedTable,
|
|
497
|
+
row: StoredDocument,
|
|
498
|
+
) {
|
|
499
|
+
const indexes = yield* resolvedIndexes(table);
|
|
500
|
+
return indexes.map((index) => ({
|
|
501
|
+
index,
|
|
502
|
+
key: storedIndexKey(index, row),
|
|
503
|
+
identity: indexIdentity(table.id, index.name, index.definition.fields),
|
|
504
|
+
}));
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
const writeIndexEntries = Effect.fn("LocalDatabase.writeIndexEntries")(function* (
|
|
508
|
+
table: ResolvedTable,
|
|
509
|
+
row: StoredDocument,
|
|
510
|
+
) {
|
|
511
|
+
const keys = yield* indexKeys(table, row);
|
|
512
|
+
yield* Effect.forEach(
|
|
513
|
+
keys,
|
|
514
|
+
({ index, key }) =>
|
|
515
|
+
sql`
|
|
516
|
+
INSERT INTO indexEntries (indexId, tableId, documentId, key)
|
|
517
|
+
VALUES (${index.id}, ${table.id}, ${row.id}, ${key})
|
|
518
|
+
ON CONFLICT (indexId, documentId) DO UPDATE SET key = excluded.key
|
|
519
|
+
`,
|
|
520
|
+
);
|
|
521
|
+
return keys;
|
|
522
|
+
});
|
|
523
|
+
|
|
327
524
|
const find = Effect.fn("LocalDatabase.find")(function* (
|
|
328
525
|
schema: RuntimeSchemaDefinition,
|
|
329
526
|
tableName: string,
|
|
330
527
|
id: string,
|
|
331
|
-
dependencies?: DependencyRecorder
|
|
528
|
+
dependencies?: DependencyRecorder<ReadDependency>,
|
|
332
529
|
) {
|
|
333
530
|
const table = yield* resolveTable(schema, tableName);
|
|
334
531
|
const documentId = GeneratedId.make(id);
|
|
@@ -338,24 +535,178 @@ export class LocalDatabase extends Context.Service<LocalDatabase, LocalDatabaseS
|
|
|
338
535
|
return yield* toRuntimeDocument(table.definition, tableName, row.value, "get");
|
|
339
536
|
});
|
|
340
537
|
|
|
341
|
-
const
|
|
538
|
+
const queryDocuments = Effect.fn("LocalDatabase.queryDocuments")(function* (
|
|
342
539
|
schema: RuntimeSchemaDefinition,
|
|
343
540
|
tableName: string,
|
|
344
|
-
|
|
541
|
+
selectedIndex: string | undefined,
|
|
542
|
+
bounds: { readonly lower?: EncodedIndexKey; readonly upper?: EncodedIndexKey },
|
|
543
|
+
order: "Asc" | "Desc",
|
|
544
|
+
limit: number | undefined,
|
|
545
|
+
pagination: PaginationOptions | undefined,
|
|
546
|
+
dependencies?: DependencyRecorder<ReadDependency>,
|
|
345
547
|
) {
|
|
346
548
|
const table = yield* resolveTable(schema, tableName);
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
549
|
+
if (selectedIndex === undefined) {
|
|
550
|
+
dependencies?.record(tableDependency(table.id));
|
|
551
|
+
const queryIdentity = paginationQueryIdentity(
|
|
552
|
+
table.id,
|
|
553
|
+
null,
|
|
554
|
+
undefined,
|
|
555
|
+
undefined,
|
|
556
|
+
order,
|
|
557
|
+
);
|
|
558
|
+
const decodedCursor =
|
|
559
|
+
pagination?.cursor === null || pagination === undefined
|
|
560
|
+
? undefined
|
|
561
|
+
: decodePaginationCursor(pagination.cursor);
|
|
562
|
+
if (decodedCursor !== undefined && decodedCursor.query !== queryIdentity) {
|
|
563
|
+
throw new Error("A pagination cursor belongs to another query.");
|
|
564
|
+
}
|
|
565
|
+
const cursor = decodedCursor?.position;
|
|
566
|
+
if (cursor !== undefined && cursor.type !== "Table") {
|
|
567
|
+
throw new Error("A pagination cursor has the wrong position type.");
|
|
568
|
+
}
|
|
569
|
+
const clauses = ["tableId = ?"];
|
|
570
|
+
const parameters: Array<number | string> = [table.id];
|
|
571
|
+
if (cursor !== undefined) {
|
|
572
|
+
const operator = order === "Asc" ? ">" : "<";
|
|
573
|
+
clauses.push(`(createdAt ${operator} ? OR (createdAt = ? AND id ${operator} ?))`);
|
|
574
|
+
parameters.push(cursor.createdAt, cursor.createdAt, cursor.id);
|
|
575
|
+
}
|
|
576
|
+
const storageLimit = pagination === undefined ? limit : pagination.pageSize + 1;
|
|
577
|
+
if (storageLimit !== undefined) parameters.push(storageLimit);
|
|
578
|
+
const rows = yield* sql.unsafe<StoredDocument>(
|
|
579
|
+
`SELECT id, createdAt, updatedAt, fields
|
|
580
|
+
FROM documents
|
|
581
|
+
WHERE ${clauses.join(" AND ")}
|
|
582
|
+
ORDER BY createdAt ${order === "Asc" ? "ASC" : "DESC"}, id ${order === "Asc" ? "ASC" : "DESC"}${storageLimit === undefined ? "" : " LIMIT ?"}`,
|
|
583
|
+
parameters,
|
|
584
|
+
);
|
|
585
|
+
const decoded = yield* Schema.decodeUnknownEffect(Schema.Array(StoredDocument))(rows);
|
|
586
|
+
const hasMore = pagination !== undefined && decoded.length > pagination.pageSize;
|
|
587
|
+
const pageRows =
|
|
588
|
+
pagination === undefined ? decoded : decoded.slice(0, pagination.pageSize);
|
|
589
|
+
const documents = yield* Effect.forEach(pageRows, (row) =>
|
|
590
|
+
toRuntimeDocument(table.definition, tableName, row, "collect"),
|
|
591
|
+
);
|
|
592
|
+
const last = pageRows.at(-1);
|
|
593
|
+
const nextCursor =
|
|
594
|
+
!hasMore || last === undefined
|
|
595
|
+
? null
|
|
596
|
+
: encodePaginationCursor(queryIdentity, {
|
|
597
|
+
type: "Table",
|
|
598
|
+
createdAt: last.createdAt,
|
|
599
|
+
id: last.id,
|
|
600
|
+
});
|
|
601
|
+
return { documents, nextCursor };
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const indexes = yield* resolvedIndexes(table);
|
|
605
|
+
const index = indexes.find((candidate) => candidate.name === selectedIndex);
|
|
606
|
+
if (index === undefined) throw new Error(`Unknown index '${selectedIndex}'.`);
|
|
607
|
+
const identity = indexIdentity(table.id, index.name, index.definition.fields);
|
|
608
|
+
const queryIdentity = paginationQueryIdentity(
|
|
609
|
+
table.id,
|
|
610
|
+
identity,
|
|
611
|
+
bounds.lower,
|
|
612
|
+
bounds.upper,
|
|
613
|
+
order,
|
|
614
|
+
);
|
|
615
|
+
const decodedCursor =
|
|
616
|
+
pagination?.cursor === null || pagination === undefined
|
|
617
|
+
? undefined
|
|
618
|
+
: decodePaginationCursor(pagination.cursor);
|
|
619
|
+
if (decodedCursor !== undefined && decodedCursor.query !== queryIdentity) {
|
|
620
|
+
throw new Error("A pagination cursor belongs to another query.");
|
|
621
|
+
}
|
|
622
|
+
const cursor = decodedCursor?.position;
|
|
623
|
+
if (cursor !== undefined && cursor.type !== "Index") {
|
|
624
|
+
throw new Error("A pagination cursor has the wrong position type.");
|
|
625
|
+
}
|
|
626
|
+
const cursorLower =
|
|
627
|
+
cursor !== undefined && order === "Asc" ? successorIndexPrefix(cursor.key) : undefined;
|
|
628
|
+
const cursorUpper = cursor !== undefined && order === "Desc" ? cursor.key : undefined;
|
|
629
|
+
const effectiveBounds = {
|
|
630
|
+
lower:
|
|
631
|
+
bounds.lower === undefined
|
|
632
|
+
? cursorLower
|
|
633
|
+
: cursorLower === undefined || bounds.lower > cursorLower
|
|
634
|
+
? bounds.lower
|
|
635
|
+
: cursorLower,
|
|
636
|
+
upper:
|
|
637
|
+
bounds.upper === undefined
|
|
638
|
+
? cursorUpper
|
|
639
|
+
: cursorUpper === undefined || bounds.upper < cursorUpper
|
|
640
|
+
? bounds.upper
|
|
641
|
+
: cursorUpper,
|
|
642
|
+
};
|
|
643
|
+
const clauses = ["entries.indexId = ?"];
|
|
644
|
+
const parameters: Array<number | string> = [index.id];
|
|
645
|
+
if (effectiveBounds.lower !== undefined) {
|
|
646
|
+
clauses.push("entries.key >= ?");
|
|
647
|
+
parameters.push(effectiveBounds.lower);
|
|
648
|
+
}
|
|
649
|
+
if (effectiveBounds.upper !== undefined) {
|
|
650
|
+
clauses.push("entries.key < ?");
|
|
651
|
+
parameters.push(effectiveBounds.upper);
|
|
652
|
+
}
|
|
653
|
+
const storageLimit = pagination === undefined ? limit : pagination.pageSize + 1;
|
|
654
|
+
if (storageLimit !== undefined) parameters.push(storageLimit);
|
|
655
|
+
const rows = yield* sql.unsafe<IndexedStoredDocument>(
|
|
656
|
+
`SELECT documents.id, documents.createdAt, documents.updatedAt, documents.fields,
|
|
657
|
+
entries.key
|
|
658
|
+
FROM indexEntries AS entries
|
|
659
|
+
JOIN documents
|
|
660
|
+
ON documents.tableId = entries.tableId
|
|
661
|
+
AND documents.id = entries.documentId
|
|
662
|
+
WHERE ${clauses.join(" AND ")}
|
|
663
|
+
ORDER BY entries.key ${order === "Asc" ? "ASC" : "DESC"}${storageLimit === undefined ? "" : " LIMIT ?"}`,
|
|
664
|
+
parameters,
|
|
665
|
+
);
|
|
666
|
+
const decoded = yield* Schema.decodeUnknownEffect(Schema.Array(IndexedStoredDocument))(
|
|
667
|
+
rows,
|
|
668
|
+
);
|
|
669
|
+
const hasMore = pagination !== undefined && decoded.length > pagination.pageSize;
|
|
670
|
+
const pageRows = pagination === undefined ? decoded : decoded.slice(0, pagination.pageSize);
|
|
671
|
+
let dependencyBounds = effectiveBounds;
|
|
672
|
+
const shouldCrop =
|
|
673
|
+
pagination === undefined
|
|
674
|
+
? limit !== undefined && limit > 0 && decoded.length === limit
|
|
675
|
+
: hasMore;
|
|
676
|
+
if (shouldCrop) {
|
|
677
|
+
const last = pageRows.at(-1);
|
|
678
|
+
if (last !== undefined) {
|
|
679
|
+
dependencyBounds =
|
|
680
|
+
order === "Asc"
|
|
681
|
+
? {
|
|
682
|
+
...effectiveBounds,
|
|
683
|
+
upper: successorIndexPrefix(EncodedIndexKey.make(last.key)),
|
|
684
|
+
}
|
|
685
|
+
: { ...effectiveBounds, lower: EncodedIndexKey.make(last.key) };
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
dependencies?.record(
|
|
689
|
+
indexRangeDependency(table.id, identity, dependencyBounds.lower, dependencyBounds.upper),
|
|
690
|
+
);
|
|
691
|
+
const documents = yield* Effect.forEach(pageRows, (row) =>
|
|
350
692
|
toRuntimeDocument(table.definition, tableName, row, "collect"),
|
|
351
693
|
);
|
|
694
|
+
const last = pageRows.at(-1);
|
|
695
|
+
const nextCursor =
|
|
696
|
+
!hasMore || last === undefined
|
|
697
|
+
? null
|
|
698
|
+
: encodePaginationCursor(queryIdentity, {
|
|
699
|
+
type: "Index",
|
|
700
|
+
key: EncodedIndexKey.make(last.key),
|
|
701
|
+
});
|
|
702
|
+
return { documents, nextCursor };
|
|
352
703
|
});
|
|
353
704
|
|
|
354
705
|
const insert = Effect.fn("LocalDatabase.insert")(function* (
|
|
355
706
|
schema: RuntimeSchemaDefinition,
|
|
356
707
|
tableName: string,
|
|
357
708
|
value: RuntimeValue,
|
|
358
|
-
invalidations: DependencyRecorder
|
|
709
|
+
invalidations: DependencyRecorder<WriteInvalidation>,
|
|
359
710
|
) {
|
|
360
711
|
const table = yield* resolveTable(schema, tableName);
|
|
361
712
|
const fields = yield* encodeFields(table.definition, tableName, value, "insert", null);
|
|
@@ -365,7 +716,13 @@ export class LocalDatabase extends Context.Service<LocalDatabase, LocalDatabaseS
|
|
|
365
716
|
INSERT INTO documents (id, tableId, createdAt, updatedAt, fields)
|
|
366
717
|
VALUES (${id}, ${table.id}, ${now}, ${now}, ${fields})
|
|
367
718
|
`;
|
|
368
|
-
|
|
719
|
+
const row = { id, createdAt: now, updatedAt: now, fields } satisfies StoredDocument;
|
|
720
|
+
const keys = yield* writeIndexEntries(table, row);
|
|
721
|
+
invalidations.record(
|
|
722
|
+
tableDependency(table.id),
|
|
723
|
+
documentDependency(table.id, id),
|
|
724
|
+
...keys.map(({ identity, key }) => indexPointInvalidation(table.id, identity, key)),
|
|
725
|
+
);
|
|
369
726
|
return id;
|
|
370
727
|
});
|
|
371
728
|
|
|
@@ -373,12 +730,21 @@ export class LocalDatabase extends Context.Service<LocalDatabase, LocalDatabaseS
|
|
|
373
730
|
schema: RuntimeSchemaDefinition,
|
|
374
731
|
tableName: string,
|
|
375
732
|
id: string,
|
|
376
|
-
invalidations: DependencyRecorder
|
|
733
|
+
invalidations: DependencyRecorder<WriteInvalidation>,
|
|
377
734
|
) {
|
|
378
|
-
const table = yield* resolveTable(schema, tableName);
|
|
735
|
+
const table = yield* resolveTable(schema, tableName).pipe(Effect.orDie);
|
|
379
736
|
const documentId = GeneratedId.make(id);
|
|
380
|
-
|
|
381
|
-
|
|
737
|
+
const stored = yield* findStored({ id, tableId: table.id }).pipe(Effect.orDie);
|
|
738
|
+
if (Option.isNone(stored)) return yield* Effect.fail(documentNotFound(tableName, id));
|
|
739
|
+
const keys = yield* indexKeys(table, stored.value).pipe(Effect.orDie);
|
|
740
|
+
yield* sql`DELETE FROM documents WHERE id = ${id} AND tableId = ${table.id}`.pipe(
|
|
741
|
+
Effect.orDie,
|
|
742
|
+
);
|
|
743
|
+
invalidations.record(
|
|
744
|
+
tableDependency(table.id),
|
|
745
|
+
documentDependency(table.id, documentId),
|
|
746
|
+
...keys.map(({ identity, key }) => indexPointInvalidation(table.id, identity, key)),
|
|
747
|
+
);
|
|
382
748
|
});
|
|
383
749
|
|
|
384
750
|
const patch = Effect.fn("LocalDatabase.patch")(function* (
|
|
@@ -386,28 +752,44 @@ export class LocalDatabase extends Context.Service<LocalDatabase, LocalDatabaseS
|
|
|
386
752
|
tableName: string,
|
|
387
753
|
id: string,
|
|
388
754
|
value: RuntimeValue,
|
|
389
|
-
invalidations: DependencyRecorder
|
|
755
|
+
invalidations: DependencyRecorder<WriteInvalidation>,
|
|
390
756
|
) {
|
|
391
|
-
const table = yield* resolveTable(schema, tableName);
|
|
757
|
+
const table = yield* resolveTable(schema, tableName).pipe(Effect.orDie);
|
|
392
758
|
const documentId = GeneratedId.make(id);
|
|
393
|
-
const stored = yield* findStored({ id, tableId: table.id });
|
|
394
|
-
if (Option.
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
759
|
+
const stored = yield* findStored({ id, tableId: table.id }).pipe(Effect.orDie);
|
|
760
|
+
if (Option.isNone(stored)) return yield* Effect.fail(documentNotFound(tableName, id));
|
|
761
|
+
const oldKeys = yield* indexKeys(table, stored.value).pipe(Effect.orDie);
|
|
762
|
+
const current = yield* decodeFields(
|
|
763
|
+
table.definition,
|
|
764
|
+
tableName,
|
|
765
|
+
stored.value,
|
|
766
|
+
"patch",
|
|
767
|
+
).pipe(Effect.orDie);
|
|
768
|
+
const fields = yield* encodeFields(
|
|
769
|
+
table.definition,
|
|
770
|
+
tableName,
|
|
771
|
+
{ ...current, ...value },
|
|
772
|
+
"patch",
|
|
773
|
+
id,
|
|
774
|
+
).pipe(Effect.orDie);
|
|
775
|
+
const updatedAt = nextUpdatedAt(yield* DateTime.now, stored.value.updatedAt);
|
|
776
|
+
yield* sql`
|
|
777
|
+
UPDATE documents
|
|
778
|
+
SET fields = ${fields}, updatedAt = ${updatedAt}
|
|
779
|
+
WHERE id = ${id} AND tableId = ${table.id}
|
|
780
|
+
`.pipe(Effect.orDie);
|
|
781
|
+
const newKeys = yield* writeIndexEntries(table, {
|
|
782
|
+
...stored.value,
|
|
783
|
+
fields,
|
|
784
|
+
updatedAt,
|
|
785
|
+
}).pipe(Effect.orDie);
|
|
786
|
+
invalidations.record(
|
|
787
|
+
tableDependency(table.id),
|
|
788
|
+
documentDependency(table.id, documentId),
|
|
789
|
+
...[...oldKeys, ...newKeys].map(({ identity, key }) =>
|
|
790
|
+
indexPointInvalidation(table.id, identity, key),
|
|
791
|
+
),
|
|
792
|
+
);
|
|
411
793
|
});
|
|
412
794
|
|
|
413
795
|
const replace = Effect.fn("LocalDatabase.replace")(function* (
|
|
@@ -415,28 +797,194 @@ export class LocalDatabase extends Context.Service<LocalDatabase, LocalDatabaseS
|
|
|
415
797
|
tableName: string,
|
|
416
798
|
id: string,
|
|
417
799
|
value: RuntimeValue,
|
|
418
|
-
invalidations: DependencyRecorder
|
|
800
|
+
invalidations: DependencyRecorder<WriteInvalidation>,
|
|
419
801
|
) {
|
|
420
|
-
const table = yield* resolveTable(schema, tableName);
|
|
802
|
+
const table = yield* resolveTable(schema, tableName).pipe(Effect.orDie);
|
|
421
803
|
const documentId = GeneratedId.make(id);
|
|
422
|
-
const stored = yield* findStored({ id, tableId: table.id });
|
|
423
|
-
if (Option.
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
804
|
+
const stored = yield* findStored({ id, tableId: table.id }).pipe(Effect.orDie);
|
|
805
|
+
if (Option.isNone(stored)) return yield* Effect.fail(documentNotFound(tableName, id));
|
|
806
|
+
const oldKeys = yield* indexKeys(table, stored.value).pipe(Effect.orDie);
|
|
807
|
+
const fields = yield* encodeFields(table.definition, tableName, value, "replace", id).pipe(
|
|
808
|
+
Effect.orDie,
|
|
809
|
+
);
|
|
810
|
+
const updatedAt = nextUpdatedAt(yield* DateTime.now, stored.value.updatedAt);
|
|
811
|
+
yield* sql`
|
|
812
|
+
UPDATE documents
|
|
813
|
+
SET fields = ${fields}, updatedAt = ${updatedAt}
|
|
814
|
+
WHERE id = ${id} AND tableId = ${table.id}
|
|
815
|
+
`.pipe(Effect.orDie);
|
|
816
|
+
const newKeys = yield* writeIndexEntries(table, {
|
|
817
|
+
...stored.value,
|
|
818
|
+
fields,
|
|
819
|
+
updatedAt,
|
|
820
|
+
}).pipe(Effect.orDie);
|
|
821
|
+
invalidations.record(
|
|
822
|
+
tableDependency(table.id),
|
|
823
|
+
documentDependency(table.id, documentId),
|
|
824
|
+
...[...oldKeys, ...newKeys].map(({ identity, key }) =>
|
|
825
|
+
indexPointInvalidation(table.id, identity, key),
|
|
826
|
+
),
|
|
827
|
+
);
|
|
433
828
|
});
|
|
434
829
|
|
|
435
830
|
const makeReader = (
|
|
436
831
|
schema: RuntimeSchemaDefinition,
|
|
437
|
-
dependencies?: DependencyRecorder
|
|
438
|
-
): LocalDatabaseReader =>
|
|
439
|
-
|
|
832
|
+
dependencies?: DependencyRecorder<ReadDependency>,
|
|
833
|
+
): LocalDatabaseReader => {
|
|
834
|
+
const databaseQuery = (
|
|
835
|
+
tableName: string,
|
|
836
|
+
selectedIndex?: string,
|
|
837
|
+
selectedBounds: {
|
|
838
|
+
readonly lower?: EncodedIndexKey;
|
|
839
|
+
readonly upper?: EncodedIndexKey;
|
|
840
|
+
} = {},
|
|
841
|
+
order: "Asc" | "Desc" = "Asc",
|
|
842
|
+
): LocalDatabaseQuery => {
|
|
843
|
+
const table = schema[SchemaDefinitionTypeId][tableName];
|
|
844
|
+
if (table === undefined) throw new Error(`Unknown database table '${tableName}'.`);
|
|
845
|
+
const run = (limit?: number) =>
|
|
846
|
+
queryDocuments(
|
|
847
|
+
schema,
|
|
848
|
+
tableName,
|
|
849
|
+
selectedIndex,
|
|
850
|
+
selectedBounds,
|
|
851
|
+
order,
|
|
852
|
+
limit,
|
|
853
|
+
undefined,
|
|
854
|
+
dependencies,
|
|
855
|
+
).pipe(
|
|
856
|
+
Effect.map(({ documents }) => documents),
|
|
857
|
+
Effect.orDie,
|
|
858
|
+
);
|
|
859
|
+
const runPage = (pagination: PaginationOptions) =>
|
|
860
|
+
queryDocuments(
|
|
861
|
+
schema,
|
|
862
|
+
tableName,
|
|
863
|
+
selectedIndex,
|
|
864
|
+
selectedBounds,
|
|
865
|
+
order,
|
|
866
|
+
undefined,
|
|
867
|
+
pagination,
|
|
868
|
+
dependencies,
|
|
869
|
+
).pipe(Effect.orDie);
|
|
870
|
+
const validateLimit = (count: number) => {
|
|
871
|
+
if (!Number.isInteger(count) || count < 0 || count > 1_000) {
|
|
872
|
+
throw new Error("Query limits must be integers between 0 and 1000.");
|
|
873
|
+
}
|
|
874
|
+
return count;
|
|
875
|
+
};
|
|
876
|
+
const result: LocalDatabaseQuery = {
|
|
877
|
+
index: (name, buildRange) => {
|
|
878
|
+
if (selectedIndex !== undefined)
|
|
879
|
+
throw new Error("A query can select only one index.");
|
|
880
|
+
const definition = table.indexes[name];
|
|
881
|
+
if (definition === undefined) {
|
|
882
|
+
throw new Error(`Unknown index '${name}' on table '${tableName}'.`);
|
|
883
|
+
}
|
|
884
|
+
if (buildRange === undefined) return databaseQuery(tableName, name, {}, order);
|
|
885
|
+
const fields = [...definition.fields, "createdAt", "id"];
|
|
886
|
+
const descriptors = [
|
|
887
|
+
...indexDescriptors(table, definition.fields),
|
|
888
|
+
{ type: "date" } as const,
|
|
889
|
+
{ type: "id", table: tableName } as const,
|
|
890
|
+
];
|
|
891
|
+
const equal: IndexScalar[] = [];
|
|
892
|
+
let lower: { value: IndexScalar; inclusive: boolean } | undefined;
|
|
893
|
+
let upper: { value: IndexScalar; inclusive: boolean } | undefined;
|
|
894
|
+
let position = 0;
|
|
895
|
+
const encodeValue = (field: string, value: RuntimeDocumentValue): IndexScalar => {
|
|
896
|
+
const codec =
|
|
897
|
+
field === "createdAt"
|
|
898
|
+
? Schema.DateFromMillis
|
|
899
|
+
: field === "id"
|
|
900
|
+
? GeneratedId
|
|
901
|
+
: table.fields[field];
|
|
902
|
+
if (codec === undefined) throw new Error(`Unknown index field '${field}'.`);
|
|
903
|
+
// SAFETY: defineSchema only accepts service-free application field codecs.
|
|
904
|
+
return decodeIndexScalar(
|
|
905
|
+
Schema.encodeUnknownSync(codec as Schema.Codec<unknown, unknown, never>)(value),
|
|
906
|
+
);
|
|
907
|
+
};
|
|
908
|
+
const expectField = (field: string) => {
|
|
909
|
+
const expected = fields[position];
|
|
910
|
+
if (field !== expected) {
|
|
911
|
+
throw new Error(`Expected index field '${expected}', received '${field}'.`);
|
|
912
|
+
}
|
|
913
|
+
};
|
|
914
|
+
const builder: LocalIndexRange = {
|
|
915
|
+
eq: (field, value) => {
|
|
916
|
+
if (lower !== undefined || upper !== undefined) {
|
|
917
|
+
throw new Error("Equality cannot follow an index bound.");
|
|
918
|
+
}
|
|
919
|
+
expectField(field);
|
|
920
|
+
equal.push(encodeValue(field, value));
|
|
921
|
+
position += 1;
|
|
922
|
+
return builder;
|
|
923
|
+
},
|
|
924
|
+
gt: (field, value) => {
|
|
925
|
+
expectField(field);
|
|
926
|
+
if (lower !== undefined) throw new Error("An index range has one lower bound.");
|
|
927
|
+
lower = { value: encodeValue(field, value), inclusive: false };
|
|
928
|
+
return builder;
|
|
929
|
+
},
|
|
930
|
+
gte: (field, value) => {
|
|
931
|
+
expectField(field);
|
|
932
|
+
if (lower !== undefined) throw new Error("An index range has one lower bound.");
|
|
933
|
+
lower = { value: encodeValue(field, value), inclusive: true };
|
|
934
|
+
return builder;
|
|
935
|
+
},
|
|
936
|
+
lt: (field, value) => {
|
|
937
|
+
expectField(field);
|
|
938
|
+
if (upper !== undefined) throw new Error("An index range has one upper bound.");
|
|
939
|
+
upper = { value: encodeValue(field, value), inclusive: false };
|
|
940
|
+
return builder;
|
|
941
|
+
},
|
|
942
|
+
lte: (field, value) => {
|
|
943
|
+
expectField(field);
|
|
944
|
+
if (upper !== undefined) throw new Error("An index range has one upper bound.");
|
|
945
|
+
upper = { value: encodeValue(field, value), inclusive: true };
|
|
946
|
+
return builder;
|
|
947
|
+
},
|
|
948
|
+
};
|
|
949
|
+
buildRange(builder);
|
|
950
|
+
return databaseQuery(
|
|
951
|
+
tableName,
|
|
952
|
+
name,
|
|
953
|
+
encodeIndexRange(descriptors, equal, lower, upper),
|
|
954
|
+
order,
|
|
955
|
+
);
|
|
956
|
+
},
|
|
957
|
+
order: (direction) =>
|
|
958
|
+
databaseQuery(
|
|
959
|
+
tableName,
|
|
960
|
+
selectedIndex,
|
|
961
|
+
selectedBounds,
|
|
962
|
+
direction === "asc" ? "Asc" : "Desc",
|
|
963
|
+
),
|
|
964
|
+
collect: () => resultFromEffect(run()),
|
|
965
|
+
take: (count) => resultFromEffect(run(validateLimit(count))),
|
|
966
|
+
first: () => resultFromEffect(run(1).pipe(Effect.map((documents) => documents[0]))),
|
|
967
|
+
unique: () =>
|
|
968
|
+
resultFromEffect(
|
|
969
|
+
run(2).pipe(
|
|
970
|
+
Effect.flatMap((documents) =>
|
|
971
|
+
documents.length > 1
|
|
972
|
+
? Effect.die(new Error("A unique query matched more than one document."))
|
|
973
|
+
: Effect.succeed(documents[0]),
|
|
974
|
+
),
|
|
975
|
+
),
|
|
976
|
+
),
|
|
977
|
+
paginate: (options) =>
|
|
978
|
+
resultFromEffect(
|
|
979
|
+
runPage(options).pipe(
|
|
980
|
+
Effect.map(({ documents, nextCursor }) => ({ items: documents, nextCursor })),
|
|
981
|
+
),
|
|
982
|
+
),
|
|
983
|
+
};
|
|
984
|
+
return Object.freeze(result);
|
|
985
|
+
};
|
|
986
|
+
|
|
987
|
+
return Object.freeze({
|
|
440
988
|
find: (tableName: string, id: string) =>
|
|
441
989
|
resultFromEffect(find(schema, tableName, id, dependencies).pipe(Effect.orDie)),
|
|
442
990
|
get: (tableName: string, id: string) =>
|
|
@@ -450,32 +998,25 @@ export class LocalDatabase extends Context.Service<LocalDatabase, LocalDatabaseS
|
|
|
450
998
|
),
|
|
451
999
|
),
|
|
452
1000
|
),
|
|
453
|
-
query: (tableName: string) =>
|
|
454
|
-
Object.freeze({
|
|
455
|
-
collect: () =>
|
|
456
|
-
resultFromEffect(collect(schema, tableName, dependencies).pipe(Effect.orDie)),
|
|
457
|
-
}),
|
|
1001
|
+
query: (tableName: string) => databaseQuery(tableName),
|
|
458
1002
|
});
|
|
1003
|
+
};
|
|
459
1004
|
|
|
460
1005
|
const makeWriter = (
|
|
461
1006
|
schema: RuntimeSchemaDefinition,
|
|
462
|
-
invalidations: DependencyRecorder
|
|
1007
|
+
invalidations: DependencyRecorder<WriteInvalidation>,
|
|
463
1008
|
): LocalDatabaseWriter => {
|
|
464
1009
|
const reader = makeReader(schema);
|
|
465
1010
|
return Object.freeze({
|
|
466
1011
|
...reader,
|
|
467
1012
|
delete: (tableName: string, id: string) =>
|
|
468
|
-
resultFromEffect(
|
|
469
|
-
deleteDocument(schema, tableName, id, invalidations).pipe(Effect.orDie),
|
|
470
|
-
),
|
|
1013
|
+
resultFromEffect(deleteDocument(schema, tableName, id, invalidations)),
|
|
471
1014
|
insert: (tableName: string, value: RuntimeValue) =>
|
|
472
1015
|
resultFromEffect(insert(schema, tableName, value, invalidations).pipe(Effect.orDie)),
|
|
473
1016
|
patch: (tableName: string, id: string, value: RuntimeValue) =>
|
|
474
|
-
resultFromEffect(patch(schema, tableName, id, value, invalidations)
|
|
1017
|
+
resultFromEffect(patch(schema, tableName, id, value, invalidations)),
|
|
475
1018
|
replace: (tableName: string, id: string, value: RuntimeValue) =>
|
|
476
|
-
resultFromEffect(
|
|
477
|
-
replace(schema, tableName, id, value, invalidations).pipe(Effect.orDie),
|
|
478
|
-
),
|
|
1019
|
+
resultFromEffect(replace(schema, tableName, id, value, invalidations)),
|
|
479
1020
|
});
|
|
480
1021
|
};
|
|
481
1022
|
|
|
@@ -485,7 +1026,8 @@ export class LocalDatabase extends Context.Service<LocalDatabase, LocalDatabaseS
|
|
|
485
1026
|
) =>
|
|
486
1027
|
sql.withTransaction(
|
|
487
1028
|
Effect.gen(function* () {
|
|
488
|
-
|
|
1029
|
+
yield* prepareIndexes(schema).pipe(Effect.orDie);
|
|
1030
|
+
const dependencies = makeDependencyRecorder<ReadDependency>();
|
|
489
1031
|
const context = Object.freeze({
|
|
490
1032
|
db: makeReader(schema, dependencies),
|
|
491
1033
|
});
|
|
@@ -502,7 +1044,8 @@ export class LocalDatabase extends Context.Service<LocalDatabase, LocalDatabaseS
|
|
|
502
1044
|
) =>
|
|
503
1045
|
sql.withTransaction(
|
|
504
1046
|
Effect.gen(function* () {
|
|
505
|
-
|
|
1047
|
+
yield* prepareIndexes(schema).pipe(Effect.orDie);
|
|
1048
|
+
const invalidations = makeDependencyRecorder<WriteInvalidation>();
|
|
506
1049
|
const context = Object.freeze({
|
|
507
1050
|
db: makeWriter(schema, invalidations),
|
|
508
1051
|
});
|