@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.
Files changed (84) hide show
  1. package/CHANGELOG.md +122 -52
  2. package/README.md +663 -661
  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 -238
  8. package/src/adapter/index.ts +64 -68
  9. package/src/adapter/lifecycle.ts +517 -444
  10. package/src/adapter/transaction.ts +71 -65
  11. package/src/artifacts/availability.ts +67 -69
  12. package/src/artifacts/cascade.ts +114 -78
  13. package/src/artifacts/paths.ts +29 -29
  14. package/src/artifacts/reconcile.ts +63 -63
  15. package/src/artifacts/store.ts +334 -323
  16. package/src/contract/adapter.ts +76 -78
  17. package/src/contract/attachment.ts +75 -72
  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 -40
  23. package/src/contract/query.ts +81 -81
  24. package/src/contract/record.ts +46 -42
  25. package/src/contract/schema.ts +36 -36
  26. package/src/contract/scope.ts +36 -36
  27. package/src/contract/transaction.ts +52 -46
  28. package/src/crypto/chunked-cipher.ts +153 -144
  29. package/src/crypto/derive.ts +64 -63
  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 -0
  44. package/src/platform/fs.ts +89 -89
  45. package/src/platform/location.ts +135 -135
  46. package/src/query/budgets.ts +54 -57
  47. package/src/query/compare.ts +91 -91
  48. package/src/query/cursor.ts +100 -99
  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 -232
  60. package/src/sqlite/catalog.ts +105 -108
  61. package/src/sqlite/connection.ts +227 -205
  62. package/src/sqlite/index-coverage.ts +54 -53
  63. package/src/sqlite/native.ts +83 -83
  64. package/src/sqlite/row-codec.ts +217 -201
  65. package/src/sqlite/sql-builder.ts +244 -244
  66. package/src/sqlite/transaction-queue.ts +87 -0
  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 -388
  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,90 +1,83 @@
1
- /**
2
- * Scope descriptor normalization.
3
- *
4
- * The package normalizes and validates; it never repairs. A component it
5
- * cannot represent safely is an error (FR-013), because silently sanitizing
6
- * one is how two different descriptors end up sharing a directory — which
7
- * would be a cross-scope leak dressed up as a convenience.
8
- */
9
-
10
- import type { ScopeDescriptor, ScopeKind } from '../contract/scope';
11
-
12
- /**
13
- * Separates components in the canonical form.
14
- *
15
- * NUL rather than a printable character: a component containing the separator
16
- * could forge another descriptor's canonical string, and every printable
17
- * candidate occurs in legitimate identity data. Tenant names contain spaces,
18
- * identifiers contain colons and slashes, emails contain "@" and ".". NUL
19
- * occurs in none of them, so reserving it costs consumers nothing.
20
- */
21
- export const COMPONENT_SEPARATOR = '\u0000';
22
-
23
- /**
24
- * Control characters are rejected wholesale rather than only the separator.
25
- * They have no place in an identity component, and they would be trouble in a
26
- * log line or a diagnostic even where they could not forge a canonical
27
- * string.
28
- */
29
- // eslint-disable-next-line no-control-regex
30
- export const CONTROL_CHARACTER = /[\u0000-\u001f\u007f]/;
31
-
32
- export const MAX_COMPONENT_BYTES = 512;
33
-
34
- /** Fixed order, so the canonical form is stable across callers. */
35
- export const COMPONENT_ORDER = [
36
- 'kind',
37
- 'id',
38
- 'tenant',
39
- 'app',
40
- 'environment',
41
- 'namespace',
42
- ] as const;
43
-
44
- export const SCOPE_KINDS: readonly ScopeKind[] = [
45
- 'guest',
46
- 'identity',
47
- 'tenant',
48
- 'workspace',
49
- 'custom',
50
- ];
51
-
52
- /**
53
- * NFC, trimmed, case preserved.
54
- *
55
- * Case is preserved deliberately: the package does not know whether a
56
- * consumer's identity system is case-sensitive, and folding case would merge
57
- * two scopes the app considers distinct.
58
- */
59
- export function normalizeComponent(value: string): string {
60
- return value.normalize('NFC').trim();
61
- }
62
-
63
- export type NormalizedScope = {
64
- kind: ScopeKind;
65
- components: Record<string, string>;
66
- canonical: string;
67
- };
68
-
69
- /** Normalize without validating. Callers must validate first. */
70
- export function normalizeDescriptor(descriptor: ScopeDescriptor): NormalizedScope {
71
- const components: Record<string, string> = {};
72
- const parts: string[] = [];
73
-
74
- for (const key of COMPONENT_ORDER) {
75
- const raw = descriptor[key as keyof ScopeDescriptor];
76
- if (typeof raw !== 'string' || raw.length === 0) {
77
- parts.push('');
78
- continue;
79
- }
80
- const normalized = normalizeComponent(raw);
81
- components[key] = normalized;
82
- parts.push(normalized);
83
- }
84
-
85
- return {
86
- kind: descriptor.kind,
87
- components,
88
- canonical: parts.join(COMPONENT_SEPARATOR),
89
- };
90
- }
1
+ /**
2
+ * Scope descriptor normalization.
3
+ *
4
+ * The package normalizes and validates; it never repairs. A component it
5
+ * cannot represent safely is an error (FR-013), because silently sanitizing
6
+ * one is how two different descriptors end up sharing a directory — which
7
+ * would be a cross-scope leak dressed up as a convenience.
8
+ */
9
+
10
+ import type { ScopeDescriptor, ScopeKind } from '../contract/scope';
11
+
12
+ /**
13
+ * Separates components in the canonical form.
14
+ *
15
+ * NUL rather than a printable character: a component containing the separator
16
+ * could forge another descriptor's canonical string, and every printable
17
+ * candidate occurs in legitimate identity data. Tenant names contain spaces,
18
+ * identifiers contain colons and slashes, emails contain "@" and ".". NUL
19
+ * occurs in none of them, so reserving it costs consumers nothing.
20
+ */
21
+ export const COMPONENT_SEPARATOR = '\u0000';
22
+
23
+ /**
24
+ * Control characters are rejected wholesale rather than only the separator.
25
+ * They have no place in an identity component, and they would be trouble in a
26
+ * log line or a diagnostic even where they could not forge a canonical
27
+ * string.
28
+ */
29
+ // eslint-disable-next-line no-control-regex
30
+ export const CONTROL_CHARACTER = /[\u0000-\u001f\u007f]/;
31
+
32
+ export const MAX_COMPONENT_BYTES = 512;
33
+
34
+ /** Fixed order, so the canonical form is stable across callers. */
35
+ export const COMPONENT_ORDER = ['kind', 'id', 'tenant', 'app', 'environment', 'namespace'] as const;
36
+
37
+ export const SCOPE_KINDS: readonly ScopeKind[] = [
38
+ 'guest',
39
+ 'identity',
40
+ 'tenant',
41
+ 'workspace',
42
+ 'custom',
43
+ ];
44
+
45
+ /**
46
+ * NFC, trimmed, case preserved.
47
+ *
48
+ * Case is preserved deliberately: the package does not know whether a
49
+ * consumer's identity system is case-sensitive, and folding case would merge
50
+ * two scopes the app considers distinct.
51
+ */
52
+ export function normalizeComponent(value: string): string {
53
+ return value.normalize('NFC').trim();
54
+ }
55
+
56
+ export type NormalizedScope = {
57
+ kind: ScopeKind;
58
+ components: Record<string, string>;
59
+ canonical: string;
60
+ };
61
+
62
+ /** Normalize without validating. Callers must validate first. */
63
+ export function normalizeDescriptor(descriptor: ScopeDescriptor): NormalizedScope {
64
+ const components: Record<string, string> = {};
65
+ const parts: string[] = [];
66
+
67
+ for (const key of COMPONENT_ORDER) {
68
+ const raw = descriptor[key as keyof ScopeDescriptor];
69
+ if (typeof raw !== 'string' || raw.length === 0) {
70
+ parts.push('');
71
+ continue;
72
+ }
73
+ const normalized = normalizeComponent(raw);
74
+ components[key] = normalized;
75
+ parts.push(normalized);
76
+ }
77
+
78
+ return {
79
+ kind: descriptor.kind,
80
+ components,
81
+ canonical: parts.join(COMPONENT_SEPARATOR),
82
+ };
83
+ }
@@ -1,83 +1,83 @@
1
- /**
2
- * Scope validation and resolution.
3
- *
4
- * Validate, never repair (FR-013). The package never invents, derives, or
5
- * defaults an identity of its own (FR-008).
6
- */
7
-
8
- import { DiagnosticCodes } from '../contract/errors';
9
- import type { ResolvedScope, ScopeDescriptor } from '../contract/scope';
10
- import { raise } from '../diagnostics/errors';
11
- import { sha256Hex, utf8ByteLength } from './hash';
12
- import {
13
- COMPONENT_ORDER,
14
- CONTROL_CHARACTER,
15
- MAX_COMPONENT_BYTES,
16
- SCOPE_KINDS,
17
- normalizeComponent,
18
- normalizeDescriptor,
19
- } from './normalize';
20
-
21
- function invalid(reason: string): never {
22
- // The reason names the *component*, never its value — naming the value
23
- // would put a scope component into an error, which FR-014 forbids.
24
- raise(DiagnosticCodes.INVALID_SCOPE, { message: reason });
25
- }
26
-
27
- function validateComponent(key: string, raw: unknown): void {
28
- if (raw === undefined || raw === null) {
29
- return;
30
- }
31
- if (typeof raw !== 'string') {
32
- invalid(`scope component ${key} must be a string`);
33
- }
34
- const normalized = normalizeComponent(raw);
35
- if (normalized.length === 0) {
36
- invalid(`scope component ${key} is empty after normalization`);
37
- }
38
- if (CONTROL_CHARACTER.test(normalized)) {
39
- invalid(`scope component ${key} contains a control character`);
40
- }
41
- if (utf8ByteLength(normalized) > MAX_COMPONENT_BYTES) {
42
- invalid(`scope component ${key} exceeds the maximum length`);
43
- }
44
- }
45
-
46
- export function validateDescriptor(descriptor: ScopeDescriptor | undefined | null): void {
47
- if (!descriptor || typeof descriptor !== 'object') {
48
- invalid('scope descriptor is required');
49
- }
50
- if (!SCOPE_KINDS.includes(descriptor.kind)) {
51
- invalid('scope kind is not recognised');
52
- }
53
-
54
- // Guest is the one kind with no identity, by design: an anonymous session
55
- // still gets its own isolated scope rather than sharing a default.
56
- if (descriptor.kind !== 'guest') {
57
- const id = descriptor.id;
58
- if (typeof id !== 'string' || normalizeComponent(id).length === 0) {
59
- invalid('scope id is required for this kind');
60
- }
61
- }
62
-
63
- for (const key of COMPONENT_ORDER) {
64
- if (key === 'kind') {
65
- continue;
66
- }
67
- validateComponent(key, descriptor[key as keyof ScopeDescriptor]);
68
- }
69
- }
70
-
71
- /**
72
- * Deterministic: identical descriptors always resolve to the same scope, and
73
- * descriptors differing in any component always resolve to different ones
74
- * (FR-010).
75
- */
76
- export function resolveScope(descriptor: ScopeDescriptor): ResolvedScope {
77
- validateDescriptor(descriptor);
78
- const normalized = normalizeDescriptor(descriptor);
79
- return {
80
- key: sha256Hex(normalized.canonical),
81
- kind: descriptor.kind,
82
- };
83
- }
1
+ /**
2
+ * Scope validation and resolution.
3
+ *
4
+ * Validate, never repair (FR-013). The package never invents, derives, or
5
+ * defaults an identity of its own (FR-008).
6
+ */
7
+
8
+ import { DiagnosticCodes } from '../contract/errors';
9
+ import type { ResolvedScope, ScopeDescriptor } from '../contract/scope';
10
+ import { raise } from '../diagnostics/errors';
11
+ import { sha256Hex, utf8ByteLength } from './hash';
12
+ import {
13
+ COMPONENT_ORDER,
14
+ CONTROL_CHARACTER,
15
+ MAX_COMPONENT_BYTES,
16
+ SCOPE_KINDS,
17
+ normalizeComponent,
18
+ normalizeDescriptor,
19
+ } from './normalize';
20
+
21
+ function invalid(reason: string): never {
22
+ // The reason names the *component*, never its value — naming the value
23
+ // would put a scope component into an error, which FR-014 forbids.
24
+ raise(DiagnosticCodes.INVALID_SCOPE, { message: reason });
25
+ }
26
+
27
+ function validateComponent(key: string, raw: unknown): void {
28
+ if (raw === undefined || raw === null) {
29
+ return;
30
+ }
31
+ if (typeof raw !== 'string') {
32
+ invalid(`scope component ${key} must be a string`);
33
+ }
34
+ const normalized = normalizeComponent(raw);
35
+ if (normalized.length === 0) {
36
+ invalid(`scope component ${key} is empty after normalization`);
37
+ }
38
+ if (CONTROL_CHARACTER.test(normalized)) {
39
+ invalid(`scope component ${key} contains a control character`);
40
+ }
41
+ if (utf8ByteLength(normalized) > MAX_COMPONENT_BYTES) {
42
+ invalid(`scope component ${key} exceeds the maximum length`);
43
+ }
44
+ }
45
+
46
+ export function validateDescriptor(descriptor: ScopeDescriptor | undefined | null): void {
47
+ if (!descriptor || typeof descriptor !== 'object') {
48
+ invalid('scope descriptor is required');
49
+ }
50
+ if (!SCOPE_KINDS.includes(descriptor.kind)) {
51
+ invalid('scope kind is not recognised');
52
+ }
53
+
54
+ // Guest is the one kind with no identity, by design: an anonymous session
55
+ // still gets its own isolated scope rather than sharing a default.
56
+ if (descriptor.kind !== 'guest') {
57
+ const id = descriptor.id;
58
+ if (typeof id !== 'string' || normalizeComponent(id).length === 0) {
59
+ invalid('scope id is required for this kind');
60
+ }
61
+ }
62
+
63
+ for (const key of COMPONENT_ORDER) {
64
+ if (key === 'kind') {
65
+ continue;
66
+ }
67
+ validateComponent(key, descriptor[key as keyof ScopeDescriptor]);
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Deterministic: identical descriptors always resolve to the same scope, and
73
+ * descriptors differing in any component always resolve to different ones
74
+ * (FR-010).
75
+ */
76
+ export function resolveScope(descriptor: ScopeDescriptor): ResolvedScope {
77
+ validateDescriptor(descriptor);
78
+ const normalized = normalizeDescriptor(descriptor);
79
+ return {
80
+ key: sha256Hex(normalized.canonical),
81
+ kind: descriptor.kind,
82
+ };
83
+ }
@@ -1,26 +1,26 @@
1
- /**
2
- * The on-device adapter.
3
- *
4
- * Deliberately a seam rather than an implementation. Everything it composes
5
- * lives in `adapter/`, `migration/`, `artifacts/`, and this directory, so the
6
- * in-memory adapter can be held to the same contract by the same conformance
7
- * suite (FR-092) without either one being the "real" version the other
8
- * approximates.
9
- *
10
- * Reached only through the lazy require in `adapter/index.ts`, so a consumer
11
- * that runs the in-memory adapter in tests never loads the native peers.
12
- */
13
-
14
- import type { StorageAdapter } from '../contract/adapter';
15
- import { deleteDatabase, deleteScopeData, resetScope } from '../adapter/deletion';
16
- import { openSqliteDatabase, type OpenOptions } from '../adapter/lifecycle';
17
-
18
- export function createSqliteAdapter(options: OpenOptions = {}): StorageAdapter {
19
- return {
20
- name: 'sqlite',
21
- open: config => openSqliteDatabase(config, options),
22
- deleteScopeData,
23
- deleteDatabase,
24
- resetScope,
25
- };
26
- }
1
+ /**
2
+ * The on-device adapter.
3
+ *
4
+ * Deliberately a seam rather than an implementation. Everything it composes
5
+ * lives in `adapter/`, `migration/`, `artifacts/`, and this directory, so the
6
+ * in-memory adapter can be held to the same contract by the same conformance
7
+ * suite (FR-092) without either one being the "real" version the other
8
+ * approximates.
9
+ *
10
+ * Reached only through the lazy require in `adapter/index.ts`, so a consumer
11
+ * that runs the in-memory adapter in tests never loads the native peers.
12
+ */
13
+
14
+ import type { StorageAdapter } from '../contract/adapter';
15
+ import { deleteDatabase, deleteScopeData, resetScope } from '../adapter/deletion';
16
+ import { openSqliteDatabase, type OpenOptions } from '../adapter/lifecycle';
17
+
18
+ export function createSqliteAdapter(options: OpenOptions = {}): StorageAdapter {
19
+ return {
20
+ name: 'sqlite',
21
+ open: config => openSqliteDatabase(config, options),
22
+ deleteScopeData,
23
+ deleteDatabase,
24
+ resetScope,
25
+ };
26
+ }