@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.
- package/CHANGELOG.md +191 -102
- package/README.md +728 -728
- package/package.json +62 -62
- package/skills/sdk-rn-local-db/SKILL.md +0 -1
- package/src/adapter/budgets.ts +67 -67
- package/src/adapter/collection.ts +303 -298
- package/src/adapter/deletion.ts +252 -252
- package/src/adapter/index.ts +64 -68
- package/src/adapter/lifecycle.ts +517 -520
- package/src/adapter/transaction.ts +71 -71
- package/src/artifacts/availability.ts +67 -67
- package/src/artifacts/cascade.ts +114 -114
- package/src/artifacts/paths.ts +29 -29
- package/src/artifacts/reconcile.ts +63 -63
- package/src/artifacts/store.ts +334 -345
- package/src/contract/adapter.ts +76 -78
- package/src/contract/attachment.ts +75 -78
- package/src/contract/backup.ts +28 -28
- package/src/contract/config.ts +54 -54
- package/src/contract/encryption.ts +34 -34
- package/src/contract/errors.ts +76 -76
- package/src/contract/migration.ts +42 -42
- package/src/contract/query.ts +81 -81
- package/src/contract/record.ts +46 -51
- package/src/contract/schema.ts +36 -36
- package/src/contract/scope.ts +36 -36
- package/src/contract/transaction.ts +52 -54
- package/src/crypto/chunked-cipher.ts +153 -150
- package/src/crypto/derive.ts +64 -64
- package/src/crypto/key-provider.ts +78 -78
- package/src/crypto/provider.ts +75 -81
- package/src/diagnostics/dev-log.ts +132 -108
- package/src/diagnostics/errors.ts +63 -66
- package/src/diagnostics/faults.ts +58 -64
- package/src/diagnostics/map-native.ts +108 -106
- package/src/diagnostics/redact.ts +119 -141
- package/src/index.ts +80 -110
- package/src/migration/executor.ts +132 -136
- package/src/migration/ledger.ts +95 -97
- package/src/migration/validate-chain.ts +65 -65
- package/src/platform/backup-android-verify.ts +88 -88
- package/src/platform/backup-ios.ts +49 -48
- package/src/platform/bytes.ts +157 -144
- package/src/platform/fs.ts +90 -89
- package/src/platform/location.ts +135 -135
- package/src/query/budgets.ts +54 -54
- package/src/query/compare.ts +91 -91
- package/src/query/cursor.ts +100 -100
- package/src/query/filter.ts +82 -82
- package/src/query/paginate.ts +51 -51
- package/src/query/sort.ts +55 -56
- package/src/query/validate.ts +171 -171
- package/src/schema/declare.ts +42 -45
- package/src/schema/validate-config.ts +158 -158
- package/src/scope/hash.ts +131 -134
- package/src/scope/normalize.ts +83 -90
- package/src/scope/validate.ts +83 -83
- package/src/sqlite/adapter.ts +26 -26
- package/src/sqlite/batch.ts +242 -242
- package/src/sqlite/catalog.ts +105 -108
- package/src/sqlite/connection.ts +227 -227
- package/src/sqlite/index-coverage.ts +54 -56
- package/src/sqlite/native.ts +84 -83
- package/src/sqlite/row-codec.ts +217 -221
- package/src/sqlite/sql-builder.ts +244 -244
- package/src/sqlite/transaction-queue.ts +87 -87
- package/src/sqlite/transactions.ts +33 -33
- package/src/sqlite/values.ts +24 -24
- package/src/testing/conformance/attachments.ts +139 -136
- package/src/testing/conformance/encryption.ts +151 -150
- package/src/testing/conformance/index.ts +161 -161
- package/src/testing/conformance/lifecycle.ts +99 -99
- package/src/testing/conformance/migrations.ts +177 -177
- package/src/testing/conformance/queries.ts +204 -167
- package/src/testing/conformance/records.ts +169 -169
- package/src/testing/conformance/scopes.ts +86 -88
- package/src/testing/conformance/transactions.ts +149 -149
- package/src/testing/faults.ts +21 -21
- package/src/testing/index.ts +17 -21
- package/src/testing/memory-adapter/collection.ts +214 -219
- package/src/testing/memory-adapter/index.ts +511 -492
- package/src/testing/memory-adapter/migrations.ts +117 -119
- package/src/testing/memory-adapter/reset.ts +50 -50
- package/src/testing/memory-adapter/store.ts +128 -134
package/src/scope/normalize.ts
CHANGED
|
@@ -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
|
-
|
|
37
|
-
|
|
38
|
-
'
|
|
39
|
-
'
|
|
40
|
-
'
|
|
41
|
-
'
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
export
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
+
}
|
package/src/scope/validate.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/sqlite/adapter.ts
CHANGED
|
@@ -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
|
+
}
|