@chipmobilesdk/rn-local-db 0.2.0 → 0.3.0

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.
Files changed (84) hide show
  1. package/CHANGELOG.md +191 -102
  2. package/README.md +728 -728
  3. package/package.json +62 -62
  4. package/skills/sdk-rn-local-db/SKILL.md +0 -1
  5. package/src/adapter/budgets.ts +67 -67
  6. package/src/adapter/collection.ts +303 -298
  7. package/src/adapter/deletion.ts +252 -252
  8. package/src/adapter/index.ts +64 -68
  9. package/src/adapter/lifecycle.ts +517 -520
  10. package/src/adapter/transaction.ts +71 -71
  11. package/src/artifacts/availability.ts +67 -67
  12. package/src/artifacts/cascade.ts +114 -114
  13. package/src/artifacts/paths.ts +29 -29
  14. package/src/artifacts/reconcile.ts +63 -63
  15. package/src/artifacts/store.ts +334 -345
  16. package/src/contract/adapter.ts +76 -78
  17. package/src/contract/attachment.ts +75 -78
  18. package/src/contract/backup.ts +28 -28
  19. package/src/contract/config.ts +54 -54
  20. package/src/contract/encryption.ts +34 -34
  21. package/src/contract/errors.ts +76 -76
  22. package/src/contract/migration.ts +42 -42
  23. package/src/contract/query.ts +81 -81
  24. package/src/contract/record.ts +46 -51
  25. package/src/contract/schema.ts +36 -36
  26. package/src/contract/scope.ts +36 -36
  27. package/src/contract/transaction.ts +52 -54
  28. package/src/crypto/chunked-cipher.ts +153 -150
  29. package/src/crypto/derive.ts +64 -64
  30. package/src/crypto/key-provider.ts +78 -78
  31. package/src/crypto/provider.ts +75 -81
  32. package/src/diagnostics/dev-log.ts +132 -108
  33. package/src/diagnostics/errors.ts +63 -66
  34. package/src/diagnostics/faults.ts +58 -64
  35. package/src/diagnostics/map-native.ts +108 -106
  36. package/src/diagnostics/redact.ts +119 -141
  37. package/src/index.ts +80 -110
  38. package/src/migration/executor.ts +132 -136
  39. package/src/migration/ledger.ts +95 -97
  40. package/src/migration/validate-chain.ts +65 -65
  41. package/src/platform/backup-android-verify.ts +88 -88
  42. package/src/platform/backup-ios.ts +49 -48
  43. package/src/platform/bytes.ts +157 -144
  44. package/src/platform/fs.ts +90 -89
  45. package/src/platform/location.ts +135 -135
  46. package/src/query/budgets.ts +54 -54
  47. package/src/query/compare.ts +91 -91
  48. package/src/query/cursor.ts +100 -100
  49. package/src/query/filter.ts +82 -82
  50. package/src/query/paginate.ts +51 -51
  51. package/src/query/sort.ts +55 -56
  52. package/src/query/validate.ts +171 -171
  53. package/src/schema/declare.ts +42 -45
  54. package/src/schema/validate-config.ts +158 -158
  55. package/src/scope/hash.ts +131 -134
  56. package/src/scope/normalize.ts +83 -90
  57. package/src/scope/validate.ts +83 -83
  58. package/src/sqlite/adapter.ts +26 -26
  59. package/src/sqlite/batch.ts +242 -242
  60. package/src/sqlite/catalog.ts +105 -108
  61. package/src/sqlite/connection.ts +227 -227
  62. package/src/sqlite/index-coverage.ts +54 -56
  63. package/src/sqlite/native.ts +84 -83
  64. package/src/sqlite/row-codec.ts +217 -221
  65. package/src/sqlite/sql-builder.ts +244 -244
  66. package/src/sqlite/transaction-queue.ts +87 -87
  67. package/src/sqlite/transactions.ts +33 -33
  68. package/src/sqlite/values.ts +24 -24
  69. package/src/testing/conformance/attachments.ts +139 -136
  70. package/src/testing/conformance/encryption.ts +151 -150
  71. package/src/testing/conformance/index.ts +161 -161
  72. package/src/testing/conformance/lifecycle.ts +99 -99
  73. package/src/testing/conformance/migrations.ts +177 -177
  74. package/src/testing/conformance/queries.ts +204 -167
  75. package/src/testing/conformance/records.ts +169 -169
  76. package/src/testing/conformance/scopes.ts +86 -88
  77. package/src/testing/conformance/transactions.ts +149 -149
  78. package/src/testing/faults.ts +21 -21
  79. package/src/testing/index.ts +17 -21
  80. package/src/testing/memory-adapter/collection.ts +214 -219
  81. package/src/testing/memory-adapter/index.ts +511 -492
  82. package/src/testing/memory-adapter/migrations.ts +117 -119
  83. package/src/testing/memory-adapter/reset.ts +50 -50
  84. package/src/testing/memory-adapter/store.ts +128 -134
@@ -1,219 +1,214 @@
1
- /**
2
- * Collection operations over the in-memory store.
3
- *
4
- * Every semantic decision — filtering, sorting, paging — is delegated to
5
- * `query/`, the module the SQLite adapter also validates through. Anything
6
- * decided here instead would be a place the two adapters could drift.
7
- */
8
-
9
- import { DiagnosticCodes } from '../../contract/errors';
10
- import type { FilterNode, Query, ResultPage } from '../../contract/query';
11
- import type {
12
- RecordInput,
13
- StorableRecordData,
14
- StoredRecord,
15
- WriteOutcome,
16
- } from '../../contract/record';
17
- import type { CollectionSchema } from '../../contract/schema';
18
- import type { CollectionOps } from '../../contract/transaction';
19
- import { raise } from '../../diagnostics/errors';
20
- import { throwIfFaulted } from '../../diagnostics/faults';
21
- import { assertRecordWithinBudget } from '../../query/budgets';
22
- import { evaluateFilter } from '../../query/filter';
23
- import { applyCursor, buildPage, resolvePageSize } from '../../query/paginate';
24
- import { sortRecords, withStableTiebreaker } from '../../query/sort';
25
- import { validateQuery } from '../../query/validate';
26
- import { hasSoftDelete, hasTimestamps } from '../../schema/declare';
27
- import {
28
- collectionMap,
29
- encodePayload,
30
- toStoredRecord,
31
- type MemoryScopeStore,
32
- } from './store';
33
-
34
- export type CollectionContext = {
35
- store: MemoryScopeStore;
36
- schema: CollectionSchema;
37
- now: () => number;
38
- assertOpen: () => void;
39
- };
40
-
41
- function notFound(collection: string, id: string): never {
42
- raise(DiagnosticCodes.RECORD_NOT_FOUND, {
43
- message: 'record not found',
44
- detail: { collection, recordId: id },
45
- });
46
- }
47
-
48
- export function createMemoryCollection<T extends StorableRecordData>(
49
- context: CollectionContext,
50
- ): CollectionOps<T> {
51
- const { schema } = context;
52
- const rows = () => collectionMap(context.store, schema.name);
53
-
54
- const readable = (includeSoftDeleted: boolean): StoredRecord<T>[] => {
55
- const out: StoredRecord<T>[] = [];
56
- for (const row of rows().values()) {
57
- if (!includeSoftDeleted && row.deletedAt != null) {
58
- continue;
59
- }
60
- out.push(toStoredRecord<T>(row));
61
- }
62
- return out;
63
- };
64
-
65
- return {
66
- async insert(record: RecordInput<T>): Promise<WriteOutcome> {
67
- context.assertOpen();
68
- throwIfFaulted('write');
69
- assertRecordWithinBudget(schema.name, record.data);
70
- if (rows().has(record.id)) {
71
- // Overwriting silently is how a caller loses data without ever
72
- // seeing a failure.
73
- raise(DiagnosticCodes.RECORD_CONFLICT, {
74
- message: 'record already exists',
75
- detail: { collection: schema.name, recordId: record.id },
76
- });
77
- }
78
- const now = context.now();
79
- rows().set(record.id, {
80
- id: record.id,
81
- payload: encodePayload(record.data),
82
- createdAt: hasTimestamps(schema) ? now : undefined,
83
- updatedAt: hasTimestamps(schema) ? now : undefined,
84
- deletedAt: hasSoftDelete(schema) ? null : undefined,
85
- });
86
- return { operation: 'created', id: record.id };
87
- },
88
-
89
- async update(id, record): Promise<WriteOutcome> {
90
- context.assertOpen();
91
- throwIfFaulted('write');
92
- assertRecordWithinBudget(schema.name, record.data);
93
- const existing = rows().get(id);
94
- if (!existing || existing.deletedAt != null) {
95
- notFound(schema.name, id);
96
- }
97
- existing.payload = encodePayload(record.data);
98
- if (hasTimestamps(schema)) {
99
- existing.updatedAt = context.now();
100
- }
101
- return { operation: 'updated', id };
102
- },
103
-
104
- async upsert(record: RecordInput<T>): Promise<WriteOutcome> {
105
- context.assertOpen();
106
- throwIfFaulted('write');
107
- assertRecordWithinBudget(schema.name, record.data);
108
- const existing = rows().get(record.id);
109
- const now = context.now();
110
- if (existing) {
111
- existing.payload = encodePayload(record.data);
112
- if (hasTimestamps(schema)) {
113
- existing.updatedAt = now;
114
- }
115
- existing.deletedAt = hasSoftDelete(schema) ? null : undefined;
116
- return { operation: 'updated', id: record.id };
117
- }
118
- rows().set(record.id, {
119
- id: record.id,
120
- payload: encodePayload(record.data),
121
- createdAt: hasTimestamps(schema) ? now : undefined,
122
- updatedAt: hasTimestamps(schema) ? now : undefined,
123
- deletedAt: hasSoftDelete(schema) ? null : undefined,
124
- });
125
- return { operation: 'created', id: record.id };
126
- },
127
-
128
- async get(id: string): Promise<StoredRecord<T>> {
129
- context.assertOpen();
130
- throwIfFaulted('read');
131
- const row = rows().get(id);
132
- if (!row || row.deletedAt != null) {
133
- notFound(schema.name, id);
134
- }
135
- return toStoredRecord<T>(row);
136
- },
137
-
138
- async find(id: string): Promise<StoredRecord<T> | null> {
139
- context.assertOpen();
140
- throwIfFaulted('read');
141
- const row = rows().get(id);
142
- if (!row || row.deletedAt != null) {
143
- return null;
144
- }
145
- return toStoredRecord<T>(row);
146
- },
147
-
148
- async delete(id: string): Promise<WriteOutcome> {
149
- context.assertOpen();
150
- throwIfFaulted('write');
151
- if (!rows().delete(id)) {
152
- notFound(schema.name, id);
153
- }
154
- return { operation: 'deleted', id };
155
- },
156
-
157
- async softDelete(id: string): Promise<WriteOutcome> {
158
- context.assertOpen();
159
- throwIfFaulted('write');
160
- if (!hasSoftDelete(schema)) {
161
- raise(DiagnosticCodes.CONFIG_INVALID, {
162
- message: 'collection does not declare soft delete',
163
- detail: { collection: schema.name },
164
- });
165
- }
166
- const row = rows().get(id);
167
- if (!row || row.deletedAt != null) {
168
- notFound(schema.name, id);
169
- }
170
- row.deletedAt = context.now();
171
- return { operation: 'deleted', id };
172
- },
173
-
174
- async restore(id: string): Promise<WriteOutcome> {
175
- context.assertOpen();
176
- throwIfFaulted('write');
177
- if (!hasSoftDelete(schema)) {
178
- raise(DiagnosticCodes.CONFIG_INVALID, {
179
- message: 'collection does not declare soft delete',
180
- detail: { collection: schema.name },
181
- });
182
- }
183
- const row = rows().get(id);
184
- if (!row || row.deletedAt == null) {
185
- notFound(schema.name, id);
186
- }
187
- row.deletedAt = null;
188
- return { operation: 'restored', id };
189
- },
190
-
191
- async list(query?: Query): Promise<ResultPage<T>> {
192
- context.assertOpen();
193
- throwIfFaulted('read');
194
- validateQuery(schema, query);
195
- const sort = withStableTiebreaker(query?.sort);
196
- const size = resolvePageSize(query?.page);
197
-
198
- let matched = readable(query?.includeSoftDeleted === true);
199
- if (query?.filter) {
200
- matched = matched.filter(record => evaluateFilter(record.data, query.filter!));
201
- }
202
- const ordered = applyCursor(sortRecords(matched, sort), sort, query?.page?.cursor);
203
- return buildPage(ordered.slice(0, size + 1), sort, size);
204
- },
205
-
206
- async count(filter?: FilterNode): Promise<number> {
207
- context.assertOpen();
208
- throwIfFaulted('read');
209
- if (filter) {
210
- validateQuery(schema, { filter });
211
- }
212
- const matched = readable(false);
213
- if (!filter) {
214
- return matched.length;
215
- }
216
- return matched.filter(record => evaluateFilter(record.data, filter)).length;
217
- },
218
- };
219
- }
1
+ /**
2
+ * Collection operations over the in-memory store.
3
+ *
4
+ * Every semantic decision — filtering, sorting, paging — is delegated to
5
+ * `query/`, the module the SQLite adapter also validates through. Anything
6
+ * decided here instead would be a place the two adapters could drift.
7
+ */
8
+
9
+ import { DiagnosticCodes } from '../../contract/errors';
10
+ import type { FilterNode, Query, ResultPage } from '../../contract/query';
11
+ import type {
12
+ RecordInput,
13
+ StorableRecordData,
14
+ StoredRecord,
15
+ WriteOutcome,
16
+ } from '../../contract/record';
17
+ import type { CollectionSchema } from '../../contract/schema';
18
+ import type { CollectionOps } from '../../contract/transaction';
19
+ import { raise } from '../../diagnostics/errors';
20
+ import { throwIfFaulted } from '../../diagnostics/faults';
21
+ import { assertRecordWithinBudget } from '../../query/budgets';
22
+ import { evaluateFilter } from '../../query/filter';
23
+ import { applyCursor, buildPage, resolvePageSize } from '../../query/paginate';
24
+ import { sortRecords, withStableTiebreaker } from '../../query/sort';
25
+ import { validateQuery } from '../../query/validate';
26
+ import { hasSoftDelete, hasTimestamps } from '../../schema/declare';
27
+ import { collectionMap, encodePayload, toStoredRecord, type MemoryScopeStore } from './store';
28
+
29
+ export type CollectionContext = {
30
+ store: MemoryScopeStore;
31
+ schema: CollectionSchema;
32
+ now: () => number;
33
+ assertOpen: () => void;
34
+ };
35
+
36
+ function notFound(collection: string, id: string): never {
37
+ raise(DiagnosticCodes.RECORD_NOT_FOUND, {
38
+ message: 'record not found',
39
+ detail: { collection, recordId: id },
40
+ });
41
+ }
42
+
43
+ export function createMemoryCollection<T extends StorableRecordData>(
44
+ context: CollectionContext,
45
+ ): CollectionOps<T> {
46
+ const { schema } = context;
47
+ const rows = () => collectionMap(context.store, schema.name);
48
+
49
+ const readable = (includeSoftDeleted: boolean): StoredRecord<T>[] => {
50
+ const out: StoredRecord<T>[] = [];
51
+ for (const row of rows().values()) {
52
+ if (!includeSoftDeleted && row.deletedAt != null) {
53
+ continue;
54
+ }
55
+ out.push(toStoredRecord<T>(row));
56
+ }
57
+ return out;
58
+ };
59
+
60
+ return {
61
+ async insert(record: RecordInput<T>): Promise<WriteOutcome> {
62
+ context.assertOpen();
63
+ throwIfFaulted('write');
64
+ assertRecordWithinBudget(schema.name, record.data);
65
+ if (rows().has(record.id)) {
66
+ // Overwriting silently is how a caller loses data without ever
67
+ // seeing a failure.
68
+ raise(DiagnosticCodes.RECORD_CONFLICT, {
69
+ message: 'record already exists',
70
+ detail: { collection: schema.name, recordId: record.id },
71
+ });
72
+ }
73
+ const now = context.now();
74
+ rows().set(record.id, {
75
+ id: record.id,
76
+ payload: encodePayload(record.data),
77
+ createdAt: hasTimestamps(schema) ? now : undefined,
78
+ updatedAt: hasTimestamps(schema) ? now : undefined,
79
+ deletedAt: hasSoftDelete(schema) ? null : undefined,
80
+ });
81
+ return { operation: 'created', id: record.id };
82
+ },
83
+
84
+ async update(id, record): Promise<WriteOutcome> {
85
+ context.assertOpen();
86
+ throwIfFaulted('write');
87
+ assertRecordWithinBudget(schema.name, record.data);
88
+ const existing = rows().get(id);
89
+ if (!existing || existing.deletedAt != null) {
90
+ notFound(schema.name, id);
91
+ }
92
+ existing.payload = encodePayload(record.data);
93
+ if (hasTimestamps(schema)) {
94
+ existing.updatedAt = context.now();
95
+ }
96
+ return { operation: 'updated', id };
97
+ },
98
+
99
+ async upsert(record: RecordInput<T>): Promise<WriteOutcome> {
100
+ context.assertOpen();
101
+ throwIfFaulted('write');
102
+ assertRecordWithinBudget(schema.name, record.data);
103
+ const existing = rows().get(record.id);
104
+ const now = context.now();
105
+ if (existing) {
106
+ existing.payload = encodePayload(record.data);
107
+ if (hasTimestamps(schema)) {
108
+ existing.updatedAt = now;
109
+ }
110
+ existing.deletedAt = hasSoftDelete(schema) ? null : undefined;
111
+ return { operation: 'updated', id: record.id };
112
+ }
113
+ rows().set(record.id, {
114
+ id: record.id,
115
+ payload: encodePayload(record.data),
116
+ createdAt: hasTimestamps(schema) ? now : undefined,
117
+ updatedAt: hasTimestamps(schema) ? now : undefined,
118
+ deletedAt: hasSoftDelete(schema) ? null : undefined,
119
+ });
120
+ return { operation: 'created', id: record.id };
121
+ },
122
+
123
+ async get(id: string): Promise<StoredRecord<T>> {
124
+ context.assertOpen();
125
+ throwIfFaulted('read');
126
+ const row = rows().get(id);
127
+ if (!row || row.deletedAt != null) {
128
+ notFound(schema.name, id);
129
+ }
130
+ return toStoredRecord<T>(row);
131
+ },
132
+
133
+ async find(id: string): Promise<StoredRecord<T> | null> {
134
+ context.assertOpen();
135
+ throwIfFaulted('read');
136
+ const row = rows().get(id);
137
+ if (!row || row.deletedAt != null) {
138
+ return null;
139
+ }
140
+ return toStoredRecord<T>(row);
141
+ },
142
+
143
+ async delete(id: string): Promise<WriteOutcome> {
144
+ context.assertOpen();
145
+ throwIfFaulted('write');
146
+ if (!rows().delete(id)) {
147
+ notFound(schema.name, id);
148
+ }
149
+ return { operation: 'deleted', id };
150
+ },
151
+
152
+ async softDelete(id: string): Promise<WriteOutcome> {
153
+ context.assertOpen();
154
+ throwIfFaulted('write');
155
+ if (!hasSoftDelete(schema)) {
156
+ raise(DiagnosticCodes.CONFIG_INVALID, {
157
+ message: 'collection does not declare soft delete',
158
+ detail: { collection: schema.name },
159
+ });
160
+ }
161
+ const row = rows().get(id);
162
+ if (!row || row.deletedAt != null) {
163
+ notFound(schema.name, id);
164
+ }
165
+ row.deletedAt = context.now();
166
+ return { operation: 'deleted', id };
167
+ },
168
+
169
+ async restore(id: string): Promise<WriteOutcome> {
170
+ context.assertOpen();
171
+ throwIfFaulted('write');
172
+ if (!hasSoftDelete(schema)) {
173
+ raise(DiagnosticCodes.CONFIG_INVALID, {
174
+ message: 'collection does not declare soft delete',
175
+ detail: { collection: schema.name },
176
+ });
177
+ }
178
+ const row = rows().get(id);
179
+ if (!row || row.deletedAt == null) {
180
+ notFound(schema.name, id);
181
+ }
182
+ row.deletedAt = null;
183
+ return { operation: 'restored', id };
184
+ },
185
+
186
+ async list(query?: Query): Promise<ResultPage<T>> {
187
+ context.assertOpen();
188
+ throwIfFaulted('read');
189
+ validateQuery(schema, query);
190
+ const sort = withStableTiebreaker(query?.sort);
191
+ const size = resolvePageSize(query?.page);
192
+
193
+ let matched = readable(query?.includeSoftDeleted === true);
194
+ if (query?.filter) {
195
+ matched = matched.filter(record => evaluateFilter(record.data, query.filter!));
196
+ }
197
+ const ordered = applyCursor(sortRecords(matched, sort), sort, query?.page?.cursor);
198
+ return buildPage(ordered.slice(0, size + 1), sort, size);
199
+ },
200
+
201
+ async count(filter?: FilterNode): Promise<number> {
202
+ context.assertOpen();
203
+ throwIfFaulted('read');
204
+ if (filter) {
205
+ validateQuery(schema, { filter });
206
+ }
207
+ const matched = readable(false);
208
+ if (!filter) {
209
+ return matched.length;
210
+ }
211
+ return matched.filter(record => evaluateFilter(record.data, filter)).length;
212
+ },
213
+ };
214
+ }