@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,167 +1,204 @@
1
- import { DiagnosticCodes } from '../../contract/errors';
2
- import { isCode } from '../../diagnostics/errors';
3
- import type { ConformanceContext } from './index';
4
-
5
- /** US3: filter, sort, page, count — and paging that loses nothing. */
6
- export function registerQueryConformance(context: ConformanceContext): void {
7
- describe('conformance: queries', () => {
8
- beforeEach(() => context.reset());
9
-
10
- const scope = { kind: 'guest' } as const;
11
- const SIZE = 40;
12
-
13
- async function seeded() {
14
- const handle = await context.adapter.open(context.configFor(scope));
15
- const notes = handle.collection('notes');
16
- for (let i = 0; i < SIZE; i += 1) {
17
- await notes.insert({
18
- id: `id-${String(i).padStart(3, '0')}`,
19
- data: { title: `note ${i}`, score: i % 10, done: i % 2 === 0 },
20
- });
21
- }
22
- return handle;
23
- }
24
-
25
- it('filters to the matching records only', async () => {
26
- const handle = await seeded();
27
- const page = await handle
28
- .collection('notes')
29
- .list({ filter: { op: 'eq', field: 'score', value: 3 }, page: { size: 100 } });
30
- expect(page.records).toHaveLength(4);
31
- expect(page.records.every(r => r.data.score === 3)).toBe(true);
32
- await handle.close();
33
- });
34
-
35
- it('agrees between count and a full listing', async () => {
36
- const handle = await seeded();
37
- const notes = handle.collection('notes');
38
- const filter = { op: 'gte', field: 'score', value: 5 } as const;
39
-
40
- const counted = await notes.count(filter);
41
- const listed = await notes.list({ filter, page: { size: 500 } });
42
- expect(counted).toBe(listed.records.length);
43
- await handle.close();
44
- });
45
-
46
- it('walks every record exactly once with no gaps or duplicates', async () => {
47
- const handle = await seeded();
48
- const notes = handle.collection('notes');
49
- const seen: string[] = [];
50
- let cursor: string | undefined;
51
- let guard = 0;
52
-
53
- do {
54
- const page = await notes.list({
55
- sort: [{ field: 'score' }],
56
- page: { size: 7, cursor },
57
- });
58
- seen.push(...page.records.map(r => r.id));
59
- cursor = page.cursor;
60
- guard += 1;
61
- } while (cursor && guard < 50);
62
-
63
- expect(seen).toHaveLength(SIZE);
64
- expect(new Set(seen).size).toBe(SIZE);
65
- await handle.close();
66
- });
67
-
68
- it('sorts, and breaks ties deterministically', async () => {
69
- const handle = await seeded();
70
- const page = await handle
71
- .collection('notes')
72
- .list({ sort: [{ field: 'score' }], page: { size: 500 } });
73
-
74
- const scores = page.records.map(r => r.data.score as number);
75
- expect([...scores].sort((a, b) => a - b)).toEqual(scores);
76
-
77
- const again = await handle
78
- .collection('notes')
79
- .list({ sort: [{ field: 'score' }], page: { size: 500 } });
80
- expect(again.records.map(r => r.id)).toEqual(page.records.map(r => r.id));
81
- await handle.close();
82
- });
83
-
84
- it('sorts descending when asked', async () => {
85
- const handle = await seeded();
86
- const page = await handle
87
- .collection('notes')
88
- .list({ sort: [{ field: 'score', direction: 'desc' }], page: { size: 5 } });
89
- expect(page.records[0]!.data.score).toBe(9);
90
- await handle.close();
91
- });
92
-
93
- it('returns an empty result rather than an error when nothing matches', async () => {
94
- const handle = await seeded();
95
- const page = await handle
96
- .collection('notes')
97
- .list({ filter: { op: 'eq', field: 'score', value: 999 } });
98
- expect(page.records).toEqual([]);
99
- expect(page.hasMore).toBe(false);
100
- await handle.close();
101
- });
102
-
103
- it('rejects a filter on an undeclared field', async () => {
104
- const handle = await seeded();
105
- let thrown: unknown;
106
- try {
107
- await handle.collection('notes').list({ filter: { op: 'eq', field: 'nope', value: 1 } });
108
- } catch (error) {
109
- thrown = error;
110
- }
111
- expect(isCode(thrown, DiagnosticCodes.QUERY_INVALID)).toBe(true);
112
- await handle.close();
113
- });
114
-
115
- it.each([0, -1, 501])('rejects page size %p', async size => {
116
- const handle = await seeded();
117
- let thrown: unknown;
118
- try {
119
- await handle.collection('notes').list({ page: { size } });
120
- } catch (error) {
121
- thrown = error;
122
- }
123
- expect(isCode(thrown, DiagnosticCodes.PAGE_SIZE_INVALID)).toBe(true);
124
- await handle.close();
125
- });
126
-
127
- it('excludes soft-deleted records from queries and counts by default', async () => {
128
- const handle = await seeded();
129
- const notes = handle.collection('notes');
130
- await notes.softDelete('id-000');
131
-
132
- expect(await notes.count()).toBe(SIZE - 1);
133
- const page = await notes.list({ page: { size: 500 } });
134
- expect(page.records.some(r => r.id === 'id-000')).toBe(false);
135
- await handle.close();
136
- });
137
-
138
- it('supports the full operator set', async () => {
139
- const handle = await seeded();
140
- const notes = handle.collection('notes');
141
-
142
- expect((await notes.list({ filter: { op: 'ne', field: 'score', value: 0 }, page: { size: 500 } })).records).toHaveLength(36);
143
- expect((await notes.list({ filter: { op: 'in', field: 'score', value: [0, 1] }, page: { size: 500 } })).records).toHaveLength(8);
144
- expect((await notes.list({ filter: { op: 'nin', field: 'score', value: [0] }, page: { size: 500 } })).records).toHaveLength(36);
145
- expect((await notes.list({ filter: { op: 'between', field: 'score', value: [0, 1] }, page: { size: 500 } })).records).toHaveLength(8);
146
- expect((await notes.list({ filter: { op: 'lt', field: 'score', value: 1 }, page: { size: 500 } })).records).toHaveLength(4);
147
- expect((await notes.list({ filter: { op: 'exists', field: 'title', value: true }, page: { size: 500 } })).records).toHaveLength(SIZE);
148
- expect((await notes.list({ filter: { op: 'isNull', field: 'title' }, page: { size: 500 } })).records).toHaveLength(0);
149
- expect(
150
- (
151
- await notes.list({
152
- filter: {
153
- op: 'and',
154
- nodes: [
155
- { op: 'gte', field: 'score', value: 5 },
156
- { op: 'eq', field: 'done', value: true },
157
- ],
158
- },
159
- page: { size: 500 },
160
- })
161
- ).records.length,
162
- ).toBeGreaterThan(0);
163
-
164
- await handle.close();
165
- });
166
- });
167
- }
1
+ import { DiagnosticCodes } from '../../contract/errors';
2
+ import { isCode } from '../../diagnostics/errors';
3
+ import type { ConformanceContext } from './index';
4
+
5
+ /** US3: filter, sort, page, count — and paging that loses nothing. */
6
+ export function registerQueryConformance(context: ConformanceContext): void {
7
+ describe('conformance: queries', () => {
8
+ beforeEach(() => context.reset());
9
+
10
+ const scope = { kind: 'guest' } as const;
11
+ const SIZE = 40;
12
+
13
+ async function seeded() {
14
+ const handle = await context.adapter.open(context.configFor(scope));
15
+ const notes = handle.collection('notes');
16
+ for (let i = 0; i < SIZE; i += 1) {
17
+ await notes.insert({
18
+ id: `id-${String(i).padStart(3, '0')}`,
19
+ data: { title: `note ${i}`, score: i % 10, done: i % 2 === 0 },
20
+ });
21
+ }
22
+ return handle;
23
+ }
24
+
25
+ it('filters to the matching records only', async () => {
26
+ const handle = await seeded();
27
+ const page = await handle
28
+ .collection('notes')
29
+ .list({ filter: { op: 'eq', field: 'score', value: 3 }, page: { size: 100 } });
30
+ expect(page.records).toHaveLength(4);
31
+ expect(page.records.every(r => r.data.score === 3)).toBe(true);
32
+ await handle.close();
33
+ });
34
+
35
+ it('agrees between count and a full listing', async () => {
36
+ const handle = await seeded();
37
+ const notes = handle.collection('notes');
38
+ const filter = { op: 'gte', field: 'score', value: 5 } as const;
39
+
40
+ const counted = await notes.count(filter);
41
+ const listed = await notes.list({ filter, page: { size: 500 } });
42
+ expect(counted).toBe(listed.records.length);
43
+ await handle.close();
44
+ });
45
+
46
+ it('walks every record exactly once with no gaps or duplicates', async () => {
47
+ const handle = await seeded();
48
+ const notes = handle.collection('notes');
49
+ const seen: string[] = [];
50
+ let cursor: string | undefined;
51
+ let guard = 0;
52
+
53
+ do {
54
+ const page = await notes.list({
55
+ sort: [{ field: 'score' }],
56
+ page: { size: 7, cursor },
57
+ });
58
+ seen.push(...page.records.map(r => r.id));
59
+ cursor = page.cursor;
60
+ guard += 1;
61
+ } while (cursor && guard < 50);
62
+
63
+ expect(seen).toHaveLength(SIZE);
64
+ expect(new Set(seen).size).toBe(SIZE);
65
+ await handle.close();
66
+ });
67
+
68
+ it('sorts, and breaks ties deterministically', async () => {
69
+ const handle = await seeded();
70
+ const page = await handle
71
+ .collection('notes')
72
+ .list({ sort: [{ field: 'score' }], page: { size: 500 } });
73
+
74
+ const scores = page.records.map(r => r.data.score as number);
75
+ expect([...scores].sort((a, b) => a - b)).toEqual(scores);
76
+
77
+ const again = await handle
78
+ .collection('notes')
79
+ .list({ sort: [{ field: 'score' }], page: { size: 500 } });
80
+ expect(again.records.map(r => r.id)).toEqual(page.records.map(r => r.id));
81
+ await handle.close();
82
+ });
83
+
84
+ it('sorts descending when asked', async () => {
85
+ const handle = await seeded();
86
+ const page = await handle
87
+ .collection('notes')
88
+ .list({ sort: [{ field: 'score', direction: 'desc' }], page: { size: 5 } });
89
+ expect(page.records[0]!.data.score).toBe(9);
90
+ await handle.close();
91
+ });
92
+
93
+ it('returns an empty result rather than an error when nothing matches', async () => {
94
+ const handle = await seeded();
95
+ const page = await handle
96
+ .collection('notes')
97
+ .list({ filter: { op: 'eq', field: 'score', value: 999 } });
98
+ expect(page.records).toEqual([]);
99
+ expect(page.hasMore).toBe(false);
100
+ await handle.close();
101
+ });
102
+
103
+ it('rejects a filter on an undeclared field', async () => {
104
+ const handle = await seeded();
105
+ let thrown: unknown;
106
+ try {
107
+ await handle.collection('notes').list({ filter: { op: 'eq', field: 'nope', value: 1 } });
108
+ } catch (error) {
109
+ thrown = error;
110
+ }
111
+ expect(isCode(thrown, DiagnosticCodes.QUERY_INVALID)).toBe(true);
112
+ await handle.close();
113
+ });
114
+
115
+ it.each([0, -1, 501])('rejects page size %p', async size => {
116
+ const handle = await seeded();
117
+ let thrown: unknown;
118
+ try {
119
+ await handle.collection('notes').list({ page: { size } });
120
+ } catch (error) {
121
+ thrown = error;
122
+ }
123
+ expect(isCode(thrown, DiagnosticCodes.PAGE_SIZE_INVALID)).toBe(true);
124
+ await handle.close();
125
+ });
126
+
127
+ it('excludes soft-deleted records from queries and counts by default', async () => {
128
+ const handle = await seeded();
129
+ const notes = handle.collection('notes');
130
+ await notes.softDelete('id-000');
131
+
132
+ expect(await notes.count()).toBe(SIZE - 1);
133
+ const page = await notes.list({ page: { size: 500 } });
134
+ expect(page.records.some(r => r.id === 'id-000')).toBe(false);
135
+ await handle.close();
136
+ });
137
+
138
+ it('supports the full operator set', async () => {
139
+ const handle = await seeded();
140
+ const notes = handle.collection('notes');
141
+
142
+ expect(
143
+ (await notes.list({ filter: { op: 'ne', field: 'score', value: 0 }, page: { size: 500 } }))
144
+ .records,
145
+ ).toHaveLength(36);
146
+ expect(
147
+ (
148
+ await notes.list({
149
+ filter: { op: 'in', field: 'score', value: [0, 1] },
150
+ page: { size: 500 },
151
+ })
152
+ ).records,
153
+ ).toHaveLength(8);
154
+ expect(
155
+ (
156
+ await notes.list({
157
+ filter: { op: 'nin', field: 'score', value: [0] },
158
+ page: { size: 500 },
159
+ })
160
+ ).records,
161
+ ).toHaveLength(36);
162
+ expect(
163
+ (
164
+ await notes.list({
165
+ filter: { op: 'between', field: 'score', value: [0, 1] },
166
+ page: { size: 500 },
167
+ })
168
+ ).records,
169
+ ).toHaveLength(8);
170
+ expect(
171
+ (await notes.list({ filter: { op: 'lt', field: 'score', value: 1 }, page: { size: 500 } }))
172
+ .records,
173
+ ).toHaveLength(4);
174
+ expect(
175
+ (
176
+ await notes.list({
177
+ filter: { op: 'exists', field: 'title', value: true },
178
+ page: { size: 500 },
179
+ })
180
+ ).records,
181
+ ).toHaveLength(SIZE);
182
+ expect(
183
+ (await notes.list({ filter: { op: 'isNull', field: 'title' }, page: { size: 500 } }))
184
+ .records,
185
+ ).toHaveLength(0);
186
+ expect(
187
+ (
188
+ await notes.list({
189
+ filter: {
190
+ op: 'and',
191
+ nodes: [
192
+ { op: 'gte', field: 'score', value: 5 },
193
+ { op: 'eq', field: 'done', value: true },
194
+ ],
195
+ },
196
+ page: { size: 500 },
197
+ })
198
+ ).records.length,
199
+ ).toBeGreaterThan(0);
200
+
201
+ await handle.close();
202
+ });
203
+ });
204
+ }