@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,171 +1,171 @@
1
- /**
2
- * Query validation, run before any SQL is built.
3
- *
4
- * Two things fall out of validating first: FR-034 is satisfied, and no
5
- * consumer-supplied string ever reaches SQL construction — field names are
6
- * resolved through the schema to a known column, and values bind as
7
- * parameters. Injection stops being a thing to remember.
8
- */
9
-
10
- import { Budgets } from '../contract/config';
11
- import { DiagnosticCodes } from '../contract/errors';
12
- import type { FilterNode, FilterValue, Query, SortKey } from '../contract/query';
13
- import type { CollectionSchema, FieldType } from '../contract/schema';
14
- import { raise } from '../diagnostics/errors';
15
- import { sanitizeIdentifier } from '../diagnostics/redact';
16
- import { fieldOf } from '../schema/declare';
17
-
18
- function queryInvalid(message: string, collection: string): never {
19
- // The field name is named; its *value* never is (FR-079).
20
- raise(DiagnosticCodes.QUERY_INVALID, { message, detail: { collection } });
21
- }
22
-
23
- const ORDERED_OPERATORS = new Set(['lt', 'lte', 'gt', 'gte', 'between']);
24
-
25
- function typeMatches(type: FieldType, value: FilterValue): boolean {
26
- if (value === null) {
27
- return true;
28
- }
29
- switch (type) {
30
- case 'string':
31
- return typeof value === 'string';
32
- case 'number':
33
- case 'timestamp':
34
- return typeof value === 'number';
35
- case 'boolean':
36
- return typeof value === 'boolean';
37
- default:
38
- return false;
39
- }
40
- }
41
-
42
- function validateField(
43
- collection: CollectionSchema,
44
- field: string,
45
- ): ReturnType<typeof fieldOf> {
46
- const declared = fieldOf(collection, field);
47
- if (!declared) {
48
- // Returning everything or nothing here would be worse than failing:
49
- // a typo would silently change what the user sees.
50
- queryInvalid(`filter references undeclared field ${sanitizeIdentifier(field)}`, collection.name);
51
- }
52
- return declared;
53
- }
54
-
55
- export function validateFilter(collection: CollectionSchema, node: FilterNode): void {
56
- switch (node.op) {
57
- case 'and':
58
- case 'or': {
59
- if (!Array.isArray(node.nodes) || node.nodes.length === 0) {
60
- queryInvalid(`${node.op} requires at least one child`, collection.name);
61
- }
62
- for (const child of node.nodes) {
63
- validateFilter(collection, child);
64
- }
65
- return;
66
- }
67
- case 'not': {
68
- if (!node.node) {
69
- queryInvalid('not requires a child', collection.name);
70
- }
71
- validateFilter(collection, node.node);
72
- return;
73
- }
74
- case 'isNull':
75
- case 'isNotNull': {
76
- validateField(collection, node.field);
77
- return;
78
- }
79
- case 'exists': {
80
- validateField(collection, node.field);
81
- if (typeof node.value !== 'boolean') {
82
- queryInvalid('exists requires a boolean', collection.name);
83
- }
84
- return;
85
- }
86
- case 'in':
87
- case 'nin': {
88
- const declared = validateField(collection, node.field)!;
89
- if (!Array.isArray(node.value) || node.value.length === 0) {
90
- queryInvalid(`${node.op} requires a non-empty array`, collection.name);
91
- }
92
- for (const value of node.value) {
93
- if (!typeMatches(declared.type, value)) {
94
- queryInvalid(`${node.op} value does not match field type`, collection.name);
95
- }
96
- }
97
- return;
98
- }
99
- case 'between': {
100
- const declared = validateField(collection, node.field)!;
101
- if (!Array.isArray(node.value) || node.value.length !== 2) {
102
- queryInvalid('between requires exactly two bounds', collection.name);
103
- }
104
- for (const value of node.value) {
105
- if (!typeMatches(declared.type, value)) {
106
- queryInvalid('between bound does not match field type', collection.name);
107
- }
108
- }
109
- return;
110
- }
111
- case 'eq':
112
- case 'ne':
113
- case 'lt':
114
- case 'lte':
115
- case 'gt':
116
- case 'gte': {
117
- const declared = validateField(collection, node.field)!;
118
- if (declared.type === 'boolean' && ORDERED_OPERATORS.has(node.op)) {
119
- queryInvalid('ordered comparison is not defined for a boolean', collection.name);
120
- }
121
- if (!typeMatches(declared.type, node.value)) {
122
- queryInvalid('filter value does not match field type', collection.name);
123
- }
124
- return;
125
- }
126
- default: {
127
- queryInvalid(
128
- `unsupported operator ${sanitizeIdentifier(String((node as { op: string }).op))}`,
129
- collection.name,
130
- );
131
- }
132
- }
133
- }
134
-
135
- export function validateSort(collection: CollectionSchema, sort: SortKey[]): void {
136
- const seen = new Set<string>();
137
- for (const key of sort) {
138
- validateField(collection, key.field);
139
- if (seen.has(key.field)) {
140
- queryInvalid(`duplicate sort field ${sanitizeIdentifier(key.field)}`, collection.name);
141
- }
142
- seen.add(key.field);
143
- if (key.direction && key.direction !== 'asc' && key.direction !== 'desc') {
144
- queryInvalid('sort direction must be asc or desc', collection.name);
145
- }
146
- }
147
- }
148
-
149
- export function validatePageSize(size: number): void {
150
- if (!Number.isInteger(size) || size < 1 || size > Budgets.MAX_PAGE_SIZE) {
151
- raise(DiagnosticCodes.PAGE_SIZE_INVALID, {
152
- message: 'page size out of range',
153
- detail: { limit: Budgets.MAX_PAGE_SIZE },
154
- });
155
- }
156
- }
157
-
158
- export function validateQuery(collection: CollectionSchema, query: Query | undefined): void {
159
- if (!query) {
160
- return;
161
- }
162
- if (query.filter) {
163
- validateFilter(collection, query.filter);
164
- }
165
- if (query.sort) {
166
- validateSort(collection, query.sort);
167
- }
168
- if (query.page) {
169
- validatePageSize(query.page.size);
170
- }
171
- }
1
+ /**
2
+ * Query validation, run before any SQL is built.
3
+ *
4
+ * Two things fall out of validating first: FR-034 is satisfied, and no
5
+ * consumer-supplied string ever reaches SQL construction — field names are
6
+ * resolved through the schema to a known column, and values bind as
7
+ * parameters. Injection stops being a thing to remember.
8
+ */
9
+
10
+ import { Budgets } from '../contract/config';
11
+ import { DiagnosticCodes } from '../contract/errors';
12
+ import type { FilterNode, FilterValue, Query, SortKey } from '../contract/query';
13
+ import type { CollectionSchema, FieldType } from '../contract/schema';
14
+ import { raise } from '../diagnostics/errors';
15
+ import { sanitizeIdentifier } from '../diagnostics/redact';
16
+ import { fieldOf } from '../schema/declare';
17
+
18
+ function queryInvalid(message: string, collection: string): never {
19
+ // The field name is named; its *value* never is (FR-079).
20
+ raise(DiagnosticCodes.QUERY_INVALID, { message, detail: { collection } });
21
+ }
22
+
23
+ const ORDERED_OPERATORS = new Set(['lt', 'lte', 'gt', 'gte', 'between']);
24
+
25
+ function typeMatches(type: FieldType, value: FilterValue): boolean {
26
+ if (value === null) {
27
+ return true;
28
+ }
29
+ switch (type) {
30
+ case 'string':
31
+ return typeof value === 'string';
32
+ case 'number':
33
+ case 'timestamp':
34
+ return typeof value === 'number';
35
+ case 'boolean':
36
+ return typeof value === 'boolean';
37
+ default:
38
+ return false;
39
+ }
40
+ }
41
+
42
+ function validateField(collection: CollectionSchema, field: string): ReturnType<typeof fieldOf> {
43
+ const declared = fieldOf(collection, field);
44
+ if (!declared) {
45
+ // Returning everything or nothing here would be worse than failing:
46
+ // a typo would silently change what the user sees.
47
+ queryInvalid(
48
+ `filter references undeclared field ${sanitizeIdentifier(field)}`,
49
+ collection.name,
50
+ );
51
+ }
52
+ return declared;
53
+ }
54
+
55
+ export function validateFilter(collection: CollectionSchema, node: FilterNode): void {
56
+ switch (node.op) {
57
+ case 'and':
58
+ case 'or': {
59
+ if (!Array.isArray(node.nodes) || node.nodes.length === 0) {
60
+ queryInvalid(`${node.op} requires at least one child`, collection.name);
61
+ }
62
+ for (const child of node.nodes) {
63
+ validateFilter(collection, child);
64
+ }
65
+ return;
66
+ }
67
+ case 'not': {
68
+ if (!node.node) {
69
+ queryInvalid('not requires a child', collection.name);
70
+ }
71
+ validateFilter(collection, node.node);
72
+ return;
73
+ }
74
+ case 'isNull':
75
+ case 'isNotNull': {
76
+ validateField(collection, node.field);
77
+ return;
78
+ }
79
+ case 'exists': {
80
+ validateField(collection, node.field);
81
+ if (typeof node.value !== 'boolean') {
82
+ queryInvalid('exists requires a boolean', collection.name);
83
+ }
84
+ return;
85
+ }
86
+ case 'in':
87
+ case 'nin': {
88
+ const declared = validateField(collection, node.field)!;
89
+ if (!Array.isArray(node.value) || node.value.length === 0) {
90
+ queryInvalid(`${node.op} requires a non-empty array`, collection.name);
91
+ }
92
+ for (const value of node.value) {
93
+ if (!typeMatches(declared.type, value)) {
94
+ queryInvalid(`${node.op} value does not match field type`, collection.name);
95
+ }
96
+ }
97
+ return;
98
+ }
99
+ case 'between': {
100
+ const declared = validateField(collection, node.field)!;
101
+ if (!Array.isArray(node.value) || node.value.length !== 2) {
102
+ queryInvalid('between requires exactly two bounds', collection.name);
103
+ }
104
+ for (const value of node.value) {
105
+ if (!typeMatches(declared.type, value)) {
106
+ queryInvalid('between bound does not match field type', collection.name);
107
+ }
108
+ }
109
+ return;
110
+ }
111
+ case 'eq':
112
+ case 'ne':
113
+ case 'lt':
114
+ case 'lte':
115
+ case 'gt':
116
+ case 'gte': {
117
+ const declared = validateField(collection, node.field)!;
118
+ if (declared.type === 'boolean' && ORDERED_OPERATORS.has(node.op)) {
119
+ queryInvalid('ordered comparison is not defined for a boolean', collection.name);
120
+ }
121
+ if (!typeMatches(declared.type, node.value)) {
122
+ queryInvalid('filter value does not match field type', collection.name);
123
+ }
124
+ return;
125
+ }
126
+ default: {
127
+ queryInvalid(
128
+ `unsupported operator ${sanitizeIdentifier(String((node as { op: string }).op))}`,
129
+ collection.name,
130
+ );
131
+ }
132
+ }
133
+ }
134
+
135
+ export function validateSort(collection: CollectionSchema, sort: SortKey[]): void {
136
+ const seen = new Set<string>();
137
+ for (const key of sort) {
138
+ validateField(collection, key.field);
139
+ if (seen.has(key.field)) {
140
+ queryInvalid(`duplicate sort field ${sanitizeIdentifier(key.field)}`, collection.name);
141
+ }
142
+ seen.add(key.field);
143
+ if (key.direction && key.direction !== 'asc' && key.direction !== 'desc') {
144
+ queryInvalid('sort direction must be asc or desc', collection.name);
145
+ }
146
+ }
147
+ }
148
+
149
+ export function validatePageSize(size: number): void {
150
+ if (!Number.isInteger(size) || size < 1 || size > Budgets.MAX_PAGE_SIZE) {
151
+ raise(DiagnosticCodes.PAGE_SIZE_INVALID, {
152
+ message: 'page size out of range',
153
+ detail: { limit: Budgets.MAX_PAGE_SIZE },
154
+ });
155
+ }
156
+ }
157
+
158
+ export function validateQuery(collection: CollectionSchema, query: Query | undefined): void {
159
+ if (!query) {
160
+ return;
161
+ }
162
+ if (query.filter) {
163
+ validateFilter(collection, query.filter);
164
+ }
165
+ if (query.sort) {
166
+ validateSort(collection, query.sort);
167
+ }
168
+ if (query.page) {
169
+ validatePageSize(query.page.size);
170
+ }
171
+ }
@@ -1,45 +1,42 @@
1
- /**
2
- * Schema lookup helpers shared by both adapters.
3
- *
4
- * Declaring a field makes it queryable — it does not restrict what a record
5
- * may contain. `isQueryable` is the single answer to "may a filter or sort
6
- * reference this?", so both adapters reject the same field names (FR-034).
7
- */
8
-
9
- import type { CollectionSchema, FieldDeclaration } from '../contract/schema';
10
-
11
- export type SchemaIndex = ReadonlyMap<string, CollectionSchema>;
12
-
13
- export function indexSchemas(collections: CollectionSchema[]): SchemaIndex {
14
- return new Map(collections.map(collection => [collection.name, collection]));
15
- }
16
-
17
- export function fieldOf(
18
- collection: CollectionSchema,
19
- name: string,
20
- ): FieldDeclaration | undefined {
21
- return collection.fields?.find(field => field.name === name);
22
- }
23
-
24
- export function isQueryable(collection: CollectionSchema, name: string): boolean {
25
- return fieldOf(collection, name) !== undefined;
26
- }
27
-
28
- export function hasSoftDelete(collection: CollectionSchema): boolean {
29
- return collection.softDelete === true;
30
- }
31
-
32
- export function hasTimestamps(collection: CollectionSchema): boolean {
33
- return collection.timestamps === true;
34
- }
35
-
36
- /** Column name for a projected queryable field. Prefixed to avoid colliding
37
- * with the reserved columns (`id`, `payload`, `created_at`, ...). */
38
- export function projectedColumn(field: string): string {
39
- return `f_${field}`;
40
- }
41
-
42
- /** Table name for a collection, prefixed for the same reason. */
43
- export function collectionTable(name: string): string {
44
- return `c_${name}`;
45
- }
1
+ /**
2
+ * Schema lookup helpers shared by both adapters.
3
+ *
4
+ * Declaring a field makes it queryable — it does not restrict what a record
5
+ * may contain. `isQueryable` is the single answer to "may a filter or sort
6
+ * reference this?", so both adapters reject the same field names (FR-034).
7
+ */
8
+
9
+ import type { CollectionSchema, FieldDeclaration } from '../contract/schema';
10
+
11
+ export type SchemaIndex = ReadonlyMap<string, CollectionSchema>;
12
+
13
+ export function indexSchemas(collections: CollectionSchema[]): SchemaIndex {
14
+ return new Map(collections.map(collection => [collection.name, collection]));
15
+ }
16
+
17
+ export function fieldOf(collection: CollectionSchema, name: string): FieldDeclaration | undefined {
18
+ return collection.fields?.find(field => field.name === name);
19
+ }
20
+
21
+ export function isQueryable(collection: CollectionSchema, name: string): boolean {
22
+ return fieldOf(collection, name) !== undefined;
23
+ }
24
+
25
+ export function hasSoftDelete(collection: CollectionSchema): boolean {
26
+ return collection.softDelete === true;
27
+ }
28
+
29
+ export function hasTimestamps(collection: CollectionSchema): boolean {
30
+ return collection.timestamps === true;
31
+ }
32
+
33
+ /** Column name for a projected queryable field. Prefixed to avoid colliding
34
+ * with the reserved columns (`id`, `payload`, `created_at`, ...). */
35
+ export function projectedColumn(field: string): string {
36
+ return `f_${field}`;
37
+ }
38
+
39
+ /** Table name for a collection, prefixed for the same reason. */
40
+ export function collectionTable(name: string): string {
41
+ return `c_${name}`;
42
+ }