@chipmobilesdk/rn-local-db 0.1.2 → 0.2.1
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/CHANGELOG.md +122 -52
- package/README.md +663 -661
- package/package.json +59 -62
- package/skills/sdk-rn-local-db/SKILL.md +0 -1
- package/src/adapter/budgets.ts +67 -67
- package/src/adapter/collection.ts +303 -298
- package/src/adapter/deletion.ts +252 -238
- package/src/adapter/index.ts +64 -68
- package/src/adapter/lifecycle.ts +517 -444
- package/src/adapter/transaction.ts +71 -65
- package/src/artifacts/availability.ts +67 -69
- package/src/artifacts/cascade.ts +114 -78
- package/src/artifacts/paths.ts +29 -29
- package/src/artifacts/reconcile.ts +63 -63
- package/src/artifacts/store.ts +334 -323
- package/src/contract/adapter.ts +76 -78
- package/src/contract/attachment.ts +75 -72
- package/src/contract/backup.ts +28 -28
- package/src/contract/config.ts +54 -54
- package/src/contract/encryption.ts +34 -34
- package/src/contract/errors.ts +76 -76
- package/src/contract/migration.ts +42 -40
- package/src/contract/query.ts +81 -81
- package/src/contract/record.ts +46 -42
- package/src/contract/schema.ts +36 -36
- package/src/contract/scope.ts +36 -36
- package/src/contract/transaction.ts +52 -46
- package/src/crypto/chunked-cipher.ts +153 -144
- package/src/crypto/derive.ts +64 -63
- package/src/crypto/key-provider.ts +78 -78
- package/src/crypto/provider.ts +81 -81
- package/src/diagnostics/dev-log.ts +112 -108
- package/src/diagnostics/errors.ts +62 -66
- package/src/diagnostics/faults.ts +58 -64
- package/src/diagnostics/map-native.ts +108 -106
- package/src/diagnostics/redact.ts +137 -141
- package/src/index.ts +80 -110
- package/src/migration/executor.ts +132 -136
- package/src/migration/ledger.ts +95 -97
- package/src/migration/validate-chain.ts +65 -65
- package/src/platform/backup-android-verify.ts +88 -88
- package/src/platform/backup-ios.ts +48 -48
- package/src/platform/bytes.ts +144 -0
- package/src/platform/fs.ts +89 -89
- package/src/platform/location.ts +135 -135
- package/src/query/budgets.ts +54 -57
- package/src/query/compare.ts +91 -91
- package/src/query/cursor.ts +100 -99
- package/src/query/filter.ts +82 -82
- package/src/query/paginate.ts +51 -51
- package/src/query/sort.ts +55 -56
- package/src/query/validate.ts +171 -171
- package/src/schema/declare.ts +42 -45
- package/src/schema/validate-config.ts +158 -158
- package/src/scope/hash.ts +131 -134
- package/src/scope/normalize.ts +83 -90
- package/src/scope/validate.ts +83 -83
- package/src/sqlite/adapter.ts +26 -26
- package/src/sqlite/batch.ts +242 -232
- package/src/sqlite/catalog.ts +105 -108
- package/src/sqlite/connection.ts +227 -205
- package/src/sqlite/index-coverage.ts +54 -53
- package/src/sqlite/native.ts +83 -83
- package/src/sqlite/row-codec.ts +217 -201
- package/src/sqlite/sql-builder.ts +244 -244
- package/src/sqlite/transaction-queue.ts +87 -0
- package/src/sqlite/transactions.ts +33 -33
- package/src/sqlite/values.ts +24 -24
- package/src/testing/conformance/attachments.ts +138 -136
- package/src/testing/conformance/encryption.ts +150 -150
- package/src/testing/conformance/index.ts +161 -161
- package/src/testing/conformance/lifecycle.ts +99 -99
- package/src/testing/conformance/migrations.ts +177 -177
- package/src/testing/conformance/queries.ts +204 -167
- package/src/testing/conformance/records.ts +169 -169
- package/src/testing/conformance/scopes.ts +86 -88
- package/src/testing/conformance/transactions.ts +149 -149
- package/src/testing/faults.ts +21 -21
- package/src/testing/index.ts +17 -21
- package/src/testing/memory-adapter/collection.ts +214 -219
- package/src/testing/memory-adapter/index.ts +510 -388
- package/src/testing/memory-adapter/migrations.ts +117 -119
- package/src/testing/memory-adapter/reset.ts +50 -50
- package/src/testing/memory-adapter/store.ts +128 -134
package/src/sqlite/batch.ts
CHANGED
|
@@ -1,232 +1,242 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Batch writes: many operations, one atomic unit, one outcome (FR-028).
|
|
3
|
-
*
|
|
4
|
-
* Every statement is resolved before any of them runs. That ordering is the
|
|
5
|
-
* point: a conflict or a missing record is reported with the database still
|
|
6
|
-
* untouched, so "the batch failed" and "the batch was partly applied" are
|
|
7
|
-
* never the same event. The statements then go to the engine's own
|
|
8
|
-
* `executeBatch`, which wraps them in a single transaction — the batch is
|
|
9
|
-
* atomic because SQLite makes it atomic, not because this file is careful.
|
|
10
|
-
*
|
|
11
|
-
* Resolution keeps an overlay of what earlier operations in the same batch
|
|
12
|
-
* did, so `insert` followed by `update` on the same id resolves against the
|
|
13
|
-
* batch's own effect rather than against the database as it was before it.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { DiagnosticCodes } from '../contract/errors';
|
|
17
|
-
import type { WriteOutcome } from '../contract/record';
|
|
18
|
-
import type { CollectionSchema } from '../contract/schema';
|
|
19
|
-
import type { BatchOperation, BatchOutcome } from '../contract/transaction';
|
|
20
|
-
import { raise } from '../diagnostics/errors';
|
|
21
|
-
import { assertBatchWithinBudget, assertRecordWithinBudget } from '../query/budgets';
|
|
22
|
-
import { collectionTable, hasSoftDelete } from '../schema/declare';
|
|
23
|
-
import type { Connection } from './connection';
|
|
24
|
-
import type { SqlValue } from './native';
|
|
25
|
-
import { insertColumns, updateAssignments } from './row-codec';
|
|
26
|
-
|
|
27
|
-
export type BatchContext = {
|
|
28
|
-
connection: Connection;
|
|
29
|
-
/** Raises CONFIG_INVALID for a collection the schema never declared. */
|
|
30
|
-
schemaFor(name: string): CollectionSchema;
|
|
31
|
-
now(): number;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
type Command = [string, SqlValue[]];
|
|
35
|
-
|
|
36
|
-
function quoted(schema: CollectionSchema): string {
|
|
37
|
-
return `"${collectionTable(schema.name)}"`;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function insertCommand(
|
|
41
|
-
schema: CollectionSchema,
|
|
42
|
-
id: string,
|
|
43
|
-
data: Parameters<typeof insertColumns>[2],
|
|
44
|
-
now: number,
|
|
45
|
-
): Command {
|
|
46
|
-
const { columns, values } = insertColumns(schema, id, data, now);
|
|
47
|
-
return [
|
|
48
|
-
`INSERT INTO ${quoted(schema)} (${columns
|
|
49
|
-
.map(name => `"${name}"`)
|
|
50
|
-
.join(', ')}) VALUES (${columns.map(() => '?').join(', ')})`,
|
|
51
|
-
values,
|
|
52
|
-
];
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function updateCommand(
|
|
56
|
-
schema: CollectionSchema,
|
|
57
|
-
id: string,
|
|
58
|
-
data: Parameters<typeof updateAssignments>[1],
|
|
59
|
-
now: number,
|
|
60
|
-
restore: boolean,
|
|
61
|
-
): Command {
|
|
62
|
-
const { columns, values } = updateAssignments(schema, data, now);
|
|
63
|
-
const assignments = columns.map(name => `"${name}" = ?`);
|
|
64
|
-
if (restore && hasSoftDelete(schema)) {
|
|
65
|
-
assignments.push('deleted_at = NULL');
|
|
66
|
-
}
|
|
67
|
-
return [`UPDATE ${quoted(schema)} SET ${assignments.join(', ')} WHERE id = ?`, [...values, id]];
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
resolver
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
resolver
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
resolver
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
case '
|
|
188
|
-
return
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Batch writes: many operations, one atomic unit, one outcome (FR-028).
|
|
3
|
+
*
|
|
4
|
+
* Every statement is resolved before any of them runs. That ordering is the
|
|
5
|
+
* point: a conflict or a missing record is reported with the database still
|
|
6
|
+
* untouched, so "the batch failed" and "the batch was partly applied" are
|
|
7
|
+
* never the same event. The statements then go to the engine's own
|
|
8
|
+
* `executeBatch`, which wraps them in a single transaction — the batch is
|
|
9
|
+
* atomic because SQLite makes it atomic, not because this file is careful.
|
|
10
|
+
*
|
|
11
|
+
* Resolution keeps an overlay of what earlier operations in the same batch
|
|
12
|
+
* did, so `insert` followed by `update` on the same id resolves against the
|
|
13
|
+
* batch's own effect rather than against the database as it was before it.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { DiagnosticCodes } from '../contract/errors';
|
|
17
|
+
import type { WriteOutcome } from '../contract/record';
|
|
18
|
+
import type { CollectionSchema } from '../contract/schema';
|
|
19
|
+
import type { BatchOperation, BatchOutcome } from '../contract/transaction';
|
|
20
|
+
import { raise } from '../diagnostics/errors';
|
|
21
|
+
import { assertBatchWithinBudget, assertRecordWithinBudget } from '../query/budgets';
|
|
22
|
+
import { collectionTable, hasSoftDelete } from '../schema/declare';
|
|
23
|
+
import type { Connection } from './connection';
|
|
24
|
+
import type { SqlValue } from './native';
|
|
25
|
+
import { insertColumns, updateAssignments } from './row-codec';
|
|
26
|
+
|
|
27
|
+
export type BatchContext = {
|
|
28
|
+
connection: Connection;
|
|
29
|
+
/** Raises CONFIG_INVALID for a collection the schema never declared. */
|
|
30
|
+
schemaFor(name: string): CollectionSchema;
|
|
31
|
+
now(): number;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type Command = [string, SqlValue[]];
|
|
35
|
+
|
|
36
|
+
function quoted(schema: CollectionSchema): string {
|
|
37
|
+
return `"${collectionTable(schema.name)}"`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function insertCommand(
|
|
41
|
+
schema: CollectionSchema,
|
|
42
|
+
id: string,
|
|
43
|
+
data: Parameters<typeof insertColumns>[2],
|
|
44
|
+
now: number,
|
|
45
|
+
): Command {
|
|
46
|
+
const { columns, values } = insertColumns(schema, id, data, now);
|
|
47
|
+
return [
|
|
48
|
+
`INSERT INTO ${quoted(schema)} (${columns
|
|
49
|
+
.map(name => `"${name}"`)
|
|
50
|
+
.join(', ')}) VALUES (${columns.map(() => '?').join(', ')})`,
|
|
51
|
+
values,
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function updateCommand(
|
|
56
|
+
schema: CollectionSchema,
|
|
57
|
+
id: string,
|
|
58
|
+
data: Parameters<typeof updateAssignments>[1],
|
|
59
|
+
now: number,
|
|
60
|
+
restore: boolean,
|
|
61
|
+
): Command {
|
|
62
|
+
const { columns, values } = updateAssignments(schema, data, now);
|
|
63
|
+
const assignments = columns.map(name => `"${name}" = ?`);
|
|
64
|
+
if (restore && hasSoftDelete(schema)) {
|
|
65
|
+
assignments.push('deleted_at = NULL');
|
|
66
|
+
}
|
|
67
|
+
return [`UPDATE ${quoted(schema)} SET ${assignments.join(', ')} WHERE id = ?`, [...values, id]];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Where a record stands, as this batch has left it so far. Three states, not
|
|
72
|
+
* two: a soft-deleted row is invisible to reads but still holds its primary
|
|
73
|
+
* key, so an insert over it conflicts and an upsert over it restores it — the
|
|
74
|
+
* same rules the collection API applies. Treating it as absent made a batch
|
|
75
|
+
* upsert issue an INSERT and fail the whole batch on the primary key.
|
|
76
|
+
*/
|
|
77
|
+
type RowState = 'live' | 'softDeleted' | 'absent';
|
|
78
|
+
|
|
79
|
+
type Resolver = {
|
|
80
|
+
schema: CollectionSchema;
|
|
81
|
+
now: number;
|
|
82
|
+
state(id: string): Promise<RowState>;
|
|
83
|
+
stage(id: string, state: RowState): void;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
type Resolved = { command: Command; outcome: WriteOutcome };
|
|
87
|
+
|
|
88
|
+
function conflict(schema: CollectionSchema, id: string): never {
|
|
89
|
+
raise(DiagnosticCodes.RECORD_CONFLICT, {
|
|
90
|
+
message: 'record already exists',
|
|
91
|
+
detail: { collection: schema.name, recordId: id },
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function missing(schema: CollectionSchema, id: string): never {
|
|
96
|
+
raise(DiagnosticCodes.RECORD_NOT_FOUND, {
|
|
97
|
+
message: 'record not found',
|
|
98
|
+
detail: { collection: schema.name, recordId: id },
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function resolveInsert(
|
|
103
|
+
resolver: Resolver,
|
|
104
|
+
operation: Extract<BatchOperation, { type: 'insert' }>,
|
|
105
|
+
): Promise<Resolved> {
|
|
106
|
+
const { schema, now } = resolver;
|
|
107
|
+
assertRecordWithinBudget(schema.name, operation.record.data);
|
|
108
|
+
if ((await resolver.state(operation.record.id)) !== 'absent') {
|
|
109
|
+
conflict(schema, operation.record.id);
|
|
110
|
+
}
|
|
111
|
+
resolver.stage(operation.record.id, 'live');
|
|
112
|
+
return {
|
|
113
|
+
command: insertCommand(schema, operation.record.id, operation.record.data, now),
|
|
114
|
+
outcome: { operation: 'created', id: operation.record.id },
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function resolveUpdate(
|
|
119
|
+
resolver: Resolver,
|
|
120
|
+
operation: Extract<BatchOperation, { type: 'update' }>,
|
|
121
|
+
): Promise<Resolved> {
|
|
122
|
+
const { schema, now } = resolver;
|
|
123
|
+
assertRecordWithinBudget(schema.name, operation.data);
|
|
124
|
+
if ((await resolver.state(operation.id)) !== 'live') {
|
|
125
|
+
missing(schema, operation.id);
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
command: updateCommand(schema, operation.id, operation.data, now, false),
|
|
129
|
+
outcome: { operation: 'updated', id: operation.id },
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function resolveUpsert(
|
|
134
|
+
resolver: Resolver,
|
|
135
|
+
operation: Extract<BatchOperation, { type: 'upsert' }>,
|
|
136
|
+
): Promise<Resolved> {
|
|
137
|
+
const { schema, now } = resolver;
|
|
138
|
+
assertRecordWithinBudget(schema.name, operation.record.data);
|
|
139
|
+
const present = (await resolver.state(operation.record.id)) !== 'absent';
|
|
140
|
+
resolver.stage(operation.record.id, 'live');
|
|
141
|
+
return {
|
|
142
|
+
command: present
|
|
143
|
+
? updateCommand(schema, operation.record.id, operation.record.data, now, true)
|
|
144
|
+
: insertCommand(schema, operation.record.id, operation.record.data, now),
|
|
145
|
+
outcome: { operation: present ? 'updated' : 'created', id: operation.record.id },
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function resolveDelete(
|
|
150
|
+
resolver: Resolver,
|
|
151
|
+
operation: Extract<BatchOperation, { type: 'delete' }>,
|
|
152
|
+
): Promise<Resolved> {
|
|
153
|
+
const { schema } = resolver;
|
|
154
|
+
if ((await resolver.state(operation.id)) !== 'live') {
|
|
155
|
+
missing(schema, operation.id);
|
|
156
|
+
}
|
|
157
|
+
resolver.stage(operation.id, 'absent');
|
|
158
|
+
return {
|
|
159
|
+
command: [`DELETE FROM ${quoted(schema)} WHERE id = ?`, [operation.id]],
|
|
160
|
+
outcome: { operation: 'deleted', id: operation.id },
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function resolveSoftDelete(
|
|
165
|
+
resolver: Resolver,
|
|
166
|
+
operation: Extract<BatchOperation, { type: 'softDelete' }>,
|
|
167
|
+
): Promise<Resolved> {
|
|
168
|
+
const { schema, now } = resolver;
|
|
169
|
+
if (!hasSoftDelete(schema)) {
|
|
170
|
+
raise(DiagnosticCodes.CONFIG_INVALID, {
|
|
171
|
+
message: 'collection does not declare soft delete',
|
|
172
|
+
detail: { collection: schema.name },
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
if ((await resolver.state(operation.id)) !== 'live') {
|
|
176
|
+
missing(schema, operation.id);
|
|
177
|
+
}
|
|
178
|
+
resolver.stage(operation.id, 'softDeleted');
|
|
179
|
+
return {
|
|
180
|
+
command: [`UPDATE ${quoted(schema)} SET deleted_at = ? WHERE id = ?`, [now, operation.id]],
|
|
181
|
+
outcome: { operation: 'deleted', id: operation.id },
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function resolveOperation(resolver: Resolver, operation: BatchOperation): Promise<Resolved> {
|
|
186
|
+
switch (operation.type) {
|
|
187
|
+
case 'insert':
|
|
188
|
+
return resolveInsert(resolver, operation);
|
|
189
|
+
case 'update':
|
|
190
|
+
return resolveUpdate(resolver, operation);
|
|
191
|
+
case 'upsert':
|
|
192
|
+
return resolveUpsert(resolver, operation);
|
|
193
|
+
case 'delete':
|
|
194
|
+
return resolveDelete(resolver, operation);
|
|
195
|
+
case 'softDelete':
|
|
196
|
+
return resolveSoftDelete(resolver, operation);
|
|
197
|
+
default:
|
|
198
|
+
raise(DiagnosticCodes.CONFIG_INVALID, { message: 'unsupported batch operation' });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export async function runBatch(
|
|
203
|
+
context: BatchContext,
|
|
204
|
+
operations: BatchOperation[],
|
|
205
|
+
): Promise<BatchOutcome> {
|
|
206
|
+
assertBatchWithinBudget(operations.length);
|
|
207
|
+
|
|
208
|
+
const commands: Command[] = [];
|
|
209
|
+
const outcomes: WriteOutcome[] = [];
|
|
210
|
+
const overlay = new Map<string, RowState>();
|
|
211
|
+
|
|
212
|
+
for (const operation of operations) {
|
|
213
|
+
const schema = context.schemaFor(operation.collection);
|
|
214
|
+
const resolver: Resolver = {
|
|
215
|
+
schema,
|
|
216
|
+
now: context.now(),
|
|
217
|
+
async state(id) {
|
|
218
|
+
const staged = overlay.get(`${schema.name}:${id}`);
|
|
219
|
+
if (staged !== undefined) {
|
|
220
|
+
return staged;
|
|
221
|
+
}
|
|
222
|
+
const result = await context.connection.execute(
|
|
223
|
+
`SELECT deleted_at FROM ${quoted(schema)} WHERE id = ?`,
|
|
224
|
+
[id],
|
|
225
|
+
);
|
|
226
|
+
const row = result.rows[0];
|
|
227
|
+
if (!row) return 'absent';
|
|
228
|
+
return row.deleted_at === null || row.deleted_at === undefined ? 'live' : 'softDeleted';
|
|
229
|
+
},
|
|
230
|
+
stage(id, state) {
|
|
231
|
+
overlay.set(`${schema.name}:${id}`, state);
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const resolved = await resolveOperation(resolver, operation);
|
|
236
|
+
commands.push(resolved.command);
|
|
237
|
+
outcomes.push(resolved.outcome);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
await context.connection.executeBatch(commands);
|
|
241
|
+
return { applied: outcomes.length, outcomes };
|
|
242
|
+
}
|