@chipmobilesdk/rn-local-db 0.2.0 → 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.
Files changed (84) hide show
  1. package/CHANGELOG.md +122 -102
  2. package/README.md +728 -728
  3. package/package.json +59 -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 +81 -81
  32. package/src/diagnostics/dev-log.ts +112 -108
  33. package/src/diagnostics/errors.ts +62 -66
  34. package/src/diagnostics/faults.ts +58 -64
  35. package/src/diagnostics/map-native.ts +108 -106
  36. package/src/diagnostics/redact.ts +137 -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 +48 -48
  43. package/src/platform/bytes.ts +144 -144
  44. package/src/platform/fs.ts +89 -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 +83 -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 +138 -136
  70. package/src/testing/conformance/encryption.ts +150 -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 +510 -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,33 +1,33 @@
1
- /**
2
- * The transaction primitive.
3
- *
4
- * A transaction never spans two scopes, and not because a check forbids it:
5
- * a transaction runs on one connection, a connection is opened against one
6
- * scope's file, and the public surface offers no way to reach a second one
7
- * from inside. FR-038 is a type error rather than a runtime one (research.md
8
- * R2).
9
- *
10
- * Rollback fidelity is SQLite's, not this package's. A failure at any step
11
- * unwinds through the engine's own `ROLLBACK`, which is the only way SC-006's
12
- * "identical to its pre-transaction state, every time" survives a process
13
- * kill halfway through.
14
- */
15
-
16
- import type { Connection, TransactionExecutor } from './connection';
17
-
18
- /**
19
- * Run `body` inside one transaction on one scope.
20
- *
21
- * Deliberately thin. The interesting behavior — read-your-own-writes,
22
- * terminal-state rejection, what a settled transaction does when used again —
23
- * belongs to `adapter/transaction.ts`, where both adapters can be held to the
24
- * same rules.
25
- */
26
- export function runInTransaction<R>(
27
- connection: Connection,
28
- body: (tx: TransactionExecutor) => Promise<R>,
29
- ): Promise<R> {
30
- return connection.transaction(body);
31
- }
32
-
33
- export type { TransactionExecutor };
1
+ /**
2
+ * The transaction primitive.
3
+ *
4
+ * A transaction never spans two scopes, and not because a check forbids it:
5
+ * a transaction runs on one connection, a connection is opened against one
6
+ * scope's file, and the public surface offers no way to reach a second one
7
+ * from inside. FR-038 is a type error rather than a runtime one (research.md
8
+ * R2).
9
+ *
10
+ * Rollback fidelity is SQLite's, not this package's. A failure at any step
11
+ * unwinds through the engine's own `ROLLBACK`, which is the only way SC-006's
12
+ * "identical to its pre-transaction state, every time" survives a process
13
+ * kill halfway through.
14
+ */
15
+
16
+ import type { Connection, TransactionExecutor } from './connection';
17
+
18
+ /**
19
+ * Run `body` inside one transaction on one scope.
20
+ *
21
+ * Deliberately thin. The interesting behavior — read-your-own-writes,
22
+ * terminal-state rejection, what a settled transaction does when used again —
23
+ * belongs to `adapter/transaction.ts`, where both adapters can be held to the
24
+ * same rules.
25
+ */
26
+ export function runInTransaction<R>(
27
+ connection: Connection,
28
+ body: (tx: TransactionExecutor) => Promise<R>,
29
+ ): Promise<R> {
30
+ return connection.transaction(body);
31
+ }
32
+
33
+ export type { TransactionExecutor };
@@ -1,24 +1,24 @@
1
- /**
2
- * Reading values back out of a result row.
3
- *
4
- * The engine hands back `unknown` per column, and every read site would
5
- * otherwise repeat the same `String(...)` / `Number(...)` coercion with a
6
- * slightly different null check. One place to do it means one place where the
7
- * "NULL means absent, not empty string" rule is decided.
8
- */
9
-
10
- export function text(value: unknown): string {
11
- return typeof value === 'string' ? value : String(value as string | number | boolean);
12
- }
13
-
14
- export function num(value: unknown): number {
15
- return typeof value === 'number' ? value : Number(value as string | number);
16
- }
17
-
18
- export function maybeText(value: unknown): string | null {
19
- return value == null ? null : text(value);
20
- }
21
-
22
- export function maybeNum(value: unknown): number | null {
23
- return value == null ? null : num(value);
24
- }
1
+ /**
2
+ * Reading values back out of a result row.
3
+ *
4
+ * The engine hands back `unknown` per column, and every read site would
5
+ * otherwise repeat the same `String(...)` / `Number(...)` coercion with a
6
+ * slightly different null check. One place to do it means one place where the
7
+ * "NULL means absent, not empty string" rule is decided.
8
+ */
9
+
10
+ export function text(value: unknown): string {
11
+ return typeof value === 'string' ? value : String(value as string | number | boolean);
12
+ }
13
+
14
+ export function num(value: unknown): number {
15
+ return typeof value === 'number' ? value : Number(value as string | number);
16
+ }
17
+
18
+ export function maybeText(value: unknown): string | null {
19
+ return value == null ? null : text(value);
20
+ }
21
+
22
+ export function maybeNum(value: unknown): number | null {
23
+ return value == null ? null : num(value);
24
+ }
@@ -1,136 +1,138 @@
1
- import { DiagnosticCodes } from '../../contract/errors';
2
- import { isCode } from '../../diagnostics/errors';
3
- import type { ConformanceContext } from './index';
4
-
5
- /** US7: artifacts the package owns, and the reconciliation that keeps them honest. */
6
- export function registerAttachmentConformance(context: ConformanceContext): void {
7
- describe('conformance: attachments', () => {
8
- beforeEach(() => context.reset());
9
-
10
- const scope = { kind: 'guest' } as const;
11
- const content = Buffer.from('hello attachment', 'utf8').toString('base64');
12
-
13
- it('stores a reference alongside the artifact', async () => {
14
- const handle = await context.adapter.open(context.configFor(scope));
15
- const attachments = handle.attachments();
16
- const reference = await attachments.write({
17
- id: 'a1',
18
- owner: { collection: 'notes', id: 'n1' },
19
- contentType: 'text/plain',
20
- content,
21
- });
22
-
23
- expect(reference.size).toBe(Buffer.from(content, 'base64').length);
24
- expect(await attachments.read('a1')).toBe(content);
25
- expect((await attachments.availability('a1')).state).toBe('available');
26
- await handle.close();
27
- });
28
-
29
- it('reads a byte range without materialising the whole artifact', async () => {
30
- const handle = await context.adapter.open(context.configFor(scope));
31
- const attachments = handle.attachments();
32
- await attachments.write({ id: 'a1', contentType: 'text/plain', content });
33
-
34
- const slice = await attachments.read('a1', { offset: 0, length: 5 });
35
- expect(Buffer.from(slice, 'base64').toString('utf8')).toBe('hello');
36
- await handle.close();
37
- });
38
-
39
- it('lists the attachments belonging to a record', async () => {
40
- const handle = await context.adapter.open(context.configFor(scope));
41
- const attachments = handle.attachments();
42
- await attachments.write({
43
- id: 'a1',
44
- owner: { collection: 'notes', id: 'n1' },
45
- contentType: 'text/plain',
46
- content,
47
- });
48
- await attachments.write({ id: 'a2', contentType: 'text/plain', content });
49
-
50
- const owned = await attachments.listFor({ collection: 'notes', id: 'n1' });
51
- expect(owned.map(r => r.id)).toEqual(['a1']);
52
- await handle.close();
53
- });
54
-
55
- it('reports a missing artifact without making the record unreadable', async () => {
56
- const handle = await context.adapter.open(context.configFor(scope));
57
- await handle.collection('notes').insert({ id: 'n1', data: { title: 'has media' } });
58
- const attachments = handle.attachments();
59
- await attachments.write({
60
- id: 'a1',
61
- owner: { collection: 'notes', id: 'n1' },
62
- contentType: 'text/plain',
63
- content,
64
- });
65
-
66
- // Remove the artifact behind the package's back.
67
- await context.removeArtifactBehindTheScenes(handle.scopeKey, 'a1');
68
-
69
- expect((await attachments.availability('a1')).state).toBe('missing');
70
- expect((await handle.collection('notes').get('n1')).data.title).toBe('has media');
71
- await handle.close();
72
- });
73
-
74
- it('finds an orphaned artifact and a dangling reference', async () => {
75
- const handle = await context.adapter.open(context.configFor(scope));
76
- const attachments = handle.attachments();
77
- await attachments.write({ id: 'a1', contentType: 'text/plain', content });
78
- await context.removeReferenceBehindTheScenes(handle.scopeKey, 'a1');
79
-
80
- await attachments.write({ id: 'a2', contentType: 'text/plain', content });
81
- await context.removeArtifactBehindTheScenes(handle.scopeKey, 'a2');
82
-
83
- const report = await attachments.reconcile();
84
- expect(report.orphanedArtifacts).toEqual(['a1']);
85
- expect(report.danglingReferences).toEqual(['a2']);
86
-
87
- const removed = await attachments.reconcile({ removeOrphans: true });
88
- expect(removed.removedArtifacts).toEqual(['a1']);
89
- expect((await attachments.reconcile()).orphanedArtifacts).toEqual([]);
90
- await handle.close();
91
- });
92
-
93
- it('rolls a reference back with its transaction', async () => {
94
- const handle = await context.adapter.open(context.configFor(scope));
95
- try {
96
- await handle.transaction(async tx => {
97
- await tx.collection('notes').insert({ id: 'n1', data: { title: 'x' } });
98
- await tx.attachments().write({ id: 'a1', contentType: 'text/plain', content });
99
- throw new Error('injected');
100
- });
101
- } catch {
102
- // Expected.
103
- }
104
-
105
- expect(await handle.collection('notes').find('n1')).toBeNull();
106
- // The reference is gone; the artifact on disk is not, which is
107
- // exactly why reconciliation exists.
108
- const report = await handle.attachments().reconcile();
109
- expect(report.orphanedArtifacts).toEqual(['a1']);
110
- await handle.close();
111
- });
112
-
113
- it('rejects an artifact over the size budget', async () => {
114
- const handle = await context.adapter.open(context.configFor(scope));
115
- const huge = Buffer.alloc(101 * 1024 * 1024).toString('base64');
116
-
117
- let thrown: unknown;
118
- try {
119
- await handle.attachments().write({ id: 'big', contentType: 'application/octet-stream', content: huge });
120
- } catch (error) {
121
- thrown = error;
122
- }
123
- expect(isCode(thrown, DiagnosticCodes.ATTACHMENT_WRITE_FAILED)).toBe(true);
124
- await handle.close();
125
- });
126
-
127
- it('removes artifacts when the scope is deleted', async () => {
128
- const handle = await context.adapter.open(context.configFor(scope));
129
- await handle.attachments().write({ id: 'a1', contentType: 'text/plain', content });
130
- await handle.close();
131
-
132
- const outcome = await context.adapter.deleteScopeData(context.scopeTargetFor(scope));
133
- expect(outcome.removedArtifacts).toContain('a1');
134
- });
135
- });
136
- }
1
+ import { DiagnosticCodes } from '../../contract/errors';
2
+ import { isCode } from '../../diagnostics/errors';
3
+ import type { ConformanceContext } from './index';
4
+
5
+ /** US7: artifacts the package owns, and the reconciliation that keeps them honest. */
6
+ export function registerAttachmentConformance(context: ConformanceContext): void {
7
+ describe('conformance: attachments', () => {
8
+ beforeEach(() => context.reset());
9
+
10
+ const scope = { kind: 'guest' } as const;
11
+ const content = Buffer.from('hello attachment', 'utf8').toString('base64');
12
+
13
+ it('stores a reference alongside the artifact', async () => {
14
+ const handle = await context.adapter.open(context.configFor(scope));
15
+ const attachments = handle.attachments();
16
+ const reference = await attachments.write({
17
+ id: 'a1',
18
+ owner: { collection: 'notes', id: 'n1' },
19
+ contentType: 'text/plain',
20
+ content,
21
+ });
22
+
23
+ expect(reference.size).toBe(Buffer.from(content, 'base64').length);
24
+ expect(await attachments.read('a1')).toBe(content);
25
+ expect((await attachments.availability('a1')).state).toBe('available');
26
+ await handle.close();
27
+ });
28
+
29
+ it('reads a byte range without materialising the whole artifact', async () => {
30
+ const handle = await context.adapter.open(context.configFor(scope));
31
+ const attachments = handle.attachments();
32
+ await attachments.write({ id: 'a1', contentType: 'text/plain', content });
33
+
34
+ const slice = await attachments.read('a1', { offset: 0, length: 5 });
35
+ expect(Buffer.from(slice, 'base64').toString('utf8')).toBe('hello');
36
+ await handle.close();
37
+ });
38
+
39
+ it('lists the attachments belonging to a record', async () => {
40
+ const handle = await context.adapter.open(context.configFor(scope));
41
+ const attachments = handle.attachments();
42
+ await attachments.write({
43
+ id: 'a1',
44
+ owner: { collection: 'notes', id: 'n1' },
45
+ contentType: 'text/plain',
46
+ content,
47
+ });
48
+ await attachments.write({ id: 'a2', contentType: 'text/plain', content });
49
+
50
+ const owned = await attachments.listFor({ collection: 'notes', id: 'n1' });
51
+ expect(owned.map(r => r.id)).toEqual(['a1']);
52
+ await handle.close();
53
+ });
54
+
55
+ it('reports a missing artifact without making the record unreadable', async () => {
56
+ const handle = await context.adapter.open(context.configFor(scope));
57
+ await handle.collection('notes').insert({ id: 'n1', data: { title: 'has media' } });
58
+ const attachments = handle.attachments();
59
+ await attachments.write({
60
+ id: 'a1',
61
+ owner: { collection: 'notes', id: 'n1' },
62
+ contentType: 'text/plain',
63
+ content,
64
+ });
65
+
66
+ // Remove the artifact behind the package's back.
67
+ await context.removeArtifactBehindTheScenes(handle.scopeKey, 'a1');
68
+
69
+ expect((await attachments.availability('a1')).state).toBe('missing');
70
+ expect((await handle.collection('notes').get('n1')).data.title).toBe('has media');
71
+ await handle.close();
72
+ });
73
+
74
+ it('finds an orphaned artifact and a dangling reference', async () => {
75
+ const handle = await context.adapter.open(context.configFor(scope));
76
+ const attachments = handle.attachments();
77
+ await attachments.write({ id: 'a1', contentType: 'text/plain', content });
78
+ await context.removeReferenceBehindTheScenes(handle.scopeKey, 'a1');
79
+
80
+ await attachments.write({ id: 'a2', contentType: 'text/plain', content });
81
+ await context.removeArtifactBehindTheScenes(handle.scopeKey, 'a2');
82
+
83
+ const report = await attachments.reconcile();
84
+ expect(report.orphanedArtifacts).toEqual(['a1']);
85
+ expect(report.danglingReferences).toEqual(['a2']);
86
+
87
+ const removed = await attachments.reconcile({ removeOrphans: true });
88
+ expect(removed.removedArtifacts).toEqual(['a1']);
89
+ expect((await attachments.reconcile()).orphanedArtifacts).toEqual([]);
90
+ await handle.close();
91
+ });
92
+
93
+ it('rolls a reference back with its transaction', async () => {
94
+ const handle = await context.adapter.open(context.configFor(scope));
95
+ try {
96
+ await handle.transaction(async tx => {
97
+ await tx.collection('notes').insert({ id: 'n1', data: { title: 'x' } });
98
+ await tx.attachments().write({ id: 'a1', contentType: 'text/plain', content });
99
+ throw new Error('injected');
100
+ });
101
+ } catch {
102
+ // Expected.
103
+ }
104
+
105
+ expect(await handle.collection('notes').find('n1')).toBeNull();
106
+ // The reference is gone; the artifact on disk is not, which is
107
+ // exactly why reconciliation exists.
108
+ const report = await handle.attachments().reconcile();
109
+ expect(report.orphanedArtifacts).toEqual(['a1']);
110
+ await handle.close();
111
+ });
112
+
113
+ it('rejects an artifact over the size budget', async () => {
114
+ const handle = await context.adapter.open(context.configFor(scope));
115
+ const huge = Buffer.alloc(101 * 1024 * 1024).toString('base64');
116
+
117
+ let thrown: unknown;
118
+ try {
119
+ await handle
120
+ .attachments()
121
+ .write({ id: 'big', contentType: 'application/octet-stream', content: huge });
122
+ } catch (error) {
123
+ thrown = error;
124
+ }
125
+ expect(isCode(thrown, DiagnosticCodes.ATTACHMENT_WRITE_FAILED)).toBe(true);
126
+ await handle.close();
127
+ });
128
+
129
+ it('removes artifacts when the scope is deleted', async () => {
130
+ const handle = await context.adapter.open(context.configFor(scope));
131
+ await handle.attachments().write({ id: 'a1', contentType: 'text/plain', content });
132
+ await handle.close();
133
+
134
+ const outcome = await context.adapter.deleteScopeData(context.scopeTargetFor(scope));
135
+ expect(outcome.removedArtifacts).toContain('a1');
136
+ });
137
+ });
138
+ }