@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
|
@@ -1,66 +1,63 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The single error type the package raises.
|
|
3
|
-
*
|
|
4
|
-
* Everything is redacted on the way in (see redact.ts), so there is no path
|
|
5
|
-
* by which a field value, query parameter, scope component, or key reaches an
|
|
6
|
-
* error object at all.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} from '
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
*
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
readonly
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
super(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
this.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export { DiagnosticCodes };
|
|
66
|
-
export type { DiagnosticCode };
|
|
1
|
+
/**
|
|
2
|
+
* The single error type the package raises.
|
|
3
|
+
*
|
|
4
|
+
* Everything is redacted on the way in (see redact.ts), so there is no path
|
|
5
|
+
* by which a field value, query parameter, scope component, or key reaches an
|
|
6
|
+
* error object at all.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { SdkError } from '@chipmobilesdk/rn-core';
|
|
10
|
+
|
|
11
|
+
import { DiagnosticCodes, RETRYABLE_CODES, type DiagnosticCode } from '../contract/errors';
|
|
12
|
+
import type { MigrationFailureDetail } from '../contract/migration';
|
|
13
|
+
import { redactCause, redactDetail, sanitizeAuthoredMessage } from './redact';
|
|
14
|
+
|
|
15
|
+
export type StorageErrorInit = {
|
|
16
|
+
/**
|
|
17
|
+
* A message this package authored. Never foreign text — a native or
|
|
18
|
+
* consumer-thrown message goes in `cause`, which reduces it to an
|
|
19
|
+
* identifier. `authored-messages.test.ts` checks every call site.
|
|
20
|
+
*/
|
|
21
|
+
message?: string;
|
|
22
|
+
detail?: Record<string, unknown> | undefined;
|
|
23
|
+
migration?: MigrationFailureDetail | undefined;
|
|
24
|
+
cause?: unknown;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** The package's error; an `SdkError` (`@chipmobilesdk/rn-core`) with `pkg: 'rn-local-db'`. */
|
|
28
|
+
export class StorageError extends SdkError<DiagnosticCode> {
|
|
29
|
+
readonly detail?: Record<string, string | number | boolean> | undefined;
|
|
30
|
+
readonly migration?: MigrationFailureDetail | undefined;
|
|
31
|
+
|
|
32
|
+
constructor(code: DiagnosticCode, init: StorageErrorInit = {}) {
|
|
33
|
+
// Call sites pass only authored literals (authored-messages.test.ts).
|
|
34
|
+
const message = init.message ? sanitizeAuthoredMessage(init.message) : code;
|
|
35
|
+
// `cause` is deliberately NOT kept: the native error it came from may
|
|
36
|
+
// quote the offending value. Only its redacted identifier survives.
|
|
37
|
+
super('rn-local-db', code, {
|
|
38
|
+
message,
|
|
39
|
+
retryable: RETRYABLE_CODES.has(code),
|
|
40
|
+
redactedCause: redactCause(init.cause),
|
|
41
|
+
});
|
|
42
|
+
this.name = 'StorageError';
|
|
43
|
+
this.detail = redactDetail(init.detail);
|
|
44
|
+
// Version numbers and an outcome enum — no payload can hide in these.
|
|
45
|
+
this.migration = init.migration;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function isStorageError(value: unknown): value is StorageError {
|
|
50
|
+
return value instanceof StorageError;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** So no consumer ever has to parse a message string (FR-082). */
|
|
54
|
+
export function isCode(value: unknown, code: DiagnosticCode): boolean {
|
|
55
|
+
return isStorageError(value) && value.code === code;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function raise(code: DiagnosticCode, init?: StorageErrorInit): never {
|
|
59
|
+
throw new StorageError(code, init);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { DiagnosticCodes };
|
|
63
|
+
export type { DiagnosticCode };
|
|
@@ -1,64 +1,58 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The fault registry.
|
|
3
|
-
*
|
|
4
|
-
* It lives here rather than under `testing/` because FR-095 wants every
|
|
5
|
-
* documented failure reachable from a real app running the real adapter — the
|
|
6
|
-
* demo's fault-injection panel exercises the on-device path, not a simulation
|
|
7
|
-
* of it. So the check has to exist in production code.
|
|
8
|
-
*
|
|
9
|
-
* What does *not* exist in production code is a way to inject: `injectFault`
|
|
10
|
-
* and `clearFaults` are exported only from the `./testing` entrypoint. With
|
|
11
|
-
* nothing injected this is one lookup in an empty map, which is the price of
|
|
12
|
-
* being able to prove a recovery path works before a user finds it broken.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import type { DiagnosticCode } from '../contract/errors';
|
|
16
|
-
import { StorageError } from './errors';
|
|
17
|
-
|
|
18
|
-
export type FaultPoint =
|
|
19
|
-
| '
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
fault.remaining -= 1;
|
|
60
|
-
if (fault.remaining <= 0) {
|
|
61
|
-
faults.delete(point);
|
|
62
|
-
}
|
|
63
|
-
throw new StorageError(fault.code, { message: `injected fault at ${point}` });
|
|
64
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* The fault registry.
|
|
3
|
+
*
|
|
4
|
+
* It lives here rather than under `testing/` because FR-095 wants every
|
|
5
|
+
* documented failure reachable from a real app running the real adapter — the
|
|
6
|
+
* demo's fault-injection panel exercises the on-device path, not a simulation
|
|
7
|
+
* of it. So the check has to exist in production code.
|
|
8
|
+
*
|
|
9
|
+
* What does *not* exist in production code is a way to inject: `injectFault`
|
|
10
|
+
* and `clearFaults` are exported only from the `./testing` entrypoint. With
|
|
11
|
+
* nothing injected this is one lookup in an empty map, which is the price of
|
|
12
|
+
* being able to prove a recovery path works before a user finds it broken.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { DiagnosticCode } from '../contract/errors';
|
|
16
|
+
import { StorageError } from './errors';
|
|
17
|
+
|
|
18
|
+
export type FaultPoint =
|
|
19
|
+
'open' | 'read' | 'write' | 'commit' | 'migration' | 'attachmentWrite' | 'keyProvider';
|
|
20
|
+
|
|
21
|
+
export type FaultSpec = {
|
|
22
|
+
at: FaultPoint;
|
|
23
|
+
code: DiagnosticCode;
|
|
24
|
+
/** Fire this many times, then stop. Omit for "until cleared". */
|
|
25
|
+
times?: number;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const faults = new Map<FaultPoint, { code: DiagnosticCode; remaining: number }>();
|
|
29
|
+
|
|
30
|
+
export function injectFault(spec: FaultSpec): void {
|
|
31
|
+
faults.set(spec.at, {
|
|
32
|
+
code: spec.code,
|
|
33
|
+
remaining: spec.times ?? Number.POSITIVE_INFINITY,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function clearFaults(): void {
|
|
38
|
+
faults.clear();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function activeFaults(): FaultPoint[] {
|
|
42
|
+
return [...faults.keys()];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function throwIfFaulted(point: FaultPoint): void {
|
|
46
|
+
if (faults.size === 0) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const fault = faults.get(point);
|
|
50
|
+
if (!fault || fault.remaining <= 0) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
fault.remaining -= 1;
|
|
54
|
+
if (fault.remaining <= 0) {
|
|
55
|
+
faults.delete(point);
|
|
56
|
+
}
|
|
57
|
+
throw new StorageError(fault.code, { message: `injected fault at ${point}` });
|
|
58
|
+
}
|
|
@@ -1,106 +1,108 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Native error to diagnostic code.
|
|
3
|
-
*
|
|
4
|
-
* The three failures FR-076 insists stay apart — a full disk, a filesystem
|
|
5
|
-
* the app cannot reach, and a corrupt database — arrive from SQLite and from
|
|
6
|
-
* the filesystem peer as ordinary errors with a string on them. Collapsing
|
|
7
|
-
* them into one write failure would be the easy thing and the wrong one: an
|
|
8
|
-
* app frees space for the first, retries after unlock for the second, and has
|
|
9
|
-
* to offer a reset for the third.
|
|
10
|
-
*
|
|
11
|
-
* Only the code is derived here. The message never travels — it is redacted
|
|
12
|
-
* at construction by `StorageError` (FR-079).
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { DiagnosticCodes, type DiagnosticCode } from '../contract/errors';
|
|
16
|
-
import { isStorageError, raise } from './errors';
|
|
17
|
-
|
|
18
|
-
export type NativeContext = {
|
|
19
|
-
/** Where the failure happened, so ambiguous signatures resolve correctly. */
|
|
20
|
-
operation: 'open' | 'read' | 'write' | 'transaction' | 'attachment' | 'delete';
|
|
21
|
-
/**
|
|
22
|
-
* Whether the database was opened with a key. "File is not a database" is
|
|
23
|
-
* SQLCipher's answer to a wrong key and SQLite's answer to a corrupt file;
|
|
24
|
-
* the only thing that separates them is whether a key was in play.
|
|
25
|
-
*/
|
|
26
|
-
encrypted?: boolean;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
function textOf(error: unknown): string {
|
|
30
|
-
if (error instanceof Error) {
|
|
31
|
-
return `${error.message} ${(error as { code?: string }).code ?? ''}`;
|
|
32
|
-
}
|
|
33
|
-
if (typeof error === 'string') {
|
|
34
|
-
return error;
|
|
35
|
-
}
|
|
36
|
-
return '';
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const DISK_FULL = /SQLITE_FULL|database or disk is full|ENOSPC|no space left/i;
|
|
40
|
-
const NOT_A_DATABASE = /SQLITE_NOTADB|file is not a database|encrypted or is not a database/i;
|
|
41
|
-
const CORRUPT = /SQLITE_CORRUPT|database disk image is malformed|malformed database/i;
|
|
42
|
-
const CONFLICT =
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
case '
|
|
78
|
-
return DiagnosticCodes.
|
|
79
|
-
case '
|
|
80
|
-
return DiagnosticCodes.
|
|
81
|
-
case '
|
|
82
|
-
return DiagnosticCodes.
|
|
83
|
-
case '
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Native error to diagnostic code.
|
|
3
|
+
*
|
|
4
|
+
* The three failures FR-076 insists stay apart — a full disk, a filesystem
|
|
5
|
+
* the app cannot reach, and a corrupt database — arrive from SQLite and from
|
|
6
|
+
* the filesystem peer as ordinary errors with a string on them. Collapsing
|
|
7
|
+
* them into one write failure would be the easy thing and the wrong one: an
|
|
8
|
+
* app frees space for the first, retries after unlock for the second, and has
|
|
9
|
+
* to offer a reset for the third.
|
|
10
|
+
*
|
|
11
|
+
* Only the code is derived here. The message never travels — it is redacted
|
|
12
|
+
* at construction by `StorageError` (FR-079).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { DiagnosticCodes, type DiagnosticCode } from '../contract/errors';
|
|
16
|
+
import { isStorageError, raise } from './errors';
|
|
17
|
+
|
|
18
|
+
export type NativeContext = {
|
|
19
|
+
/** Where the failure happened, so ambiguous signatures resolve correctly. */
|
|
20
|
+
operation: 'open' | 'read' | 'write' | 'transaction' | 'attachment' | 'delete';
|
|
21
|
+
/**
|
|
22
|
+
* Whether the database was opened with a key. "File is not a database" is
|
|
23
|
+
* SQLCipher's answer to a wrong key and SQLite's answer to a corrupt file;
|
|
24
|
+
* the only thing that separates them is whether a key was in play.
|
|
25
|
+
*/
|
|
26
|
+
encrypted?: boolean;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function textOf(error: unknown): string {
|
|
30
|
+
if (error instanceof Error) {
|
|
31
|
+
return `${error.message} ${(error as { code?: string }).code ?? ''}`;
|
|
32
|
+
}
|
|
33
|
+
if (typeof error === 'string') {
|
|
34
|
+
return error;
|
|
35
|
+
}
|
|
36
|
+
return '';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const DISK_FULL = /SQLITE_FULL|database or disk is full|ENOSPC|no space left/i;
|
|
40
|
+
const NOT_A_DATABASE = /SQLITE_NOTADB|file is not a database|encrypted or is not a database/i;
|
|
41
|
+
const CORRUPT = /SQLITE_CORRUPT|database disk image is malformed|malformed database/i;
|
|
42
|
+
const CONFLICT =
|
|
43
|
+
/UNIQUE constraint failed|PRIMARY KEY must be unique|SQLITE_CONSTRAINT_PRIMARYKEY/i;
|
|
44
|
+
const ACCESS =
|
|
45
|
+
/SQLITE_CANTOPEN|SQLITE_READONLY|SQLITE_PERM|SQLITE_AUTH|EACCES|EPERM|EROFS|ENOENT|EISDIR/i;
|
|
46
|
+
const BUSY = /SQLITE_BUSY|SQLITE_LOCKED|database is locked/i;
|
|
47
|
+
|
|
48
|
+
export function mapNativeError(error: unknown, context: NativeContext): DiagnosticCode {
|
|
49
|
+
const text = textOf(error);
|
|
50
|
+
|
|
51
|
+
if (DISK_FULL.test(text)) {
|
|
52
|
+
return DiagnosticCodes.DISK_FULL;
|
|
53
|
+
}
|
|
54
|
+
if (NOT_A_DATABASE.test(text)) {
|
|
55
|
+
// A wrong key and a corrupt file are indistinguishable from the engine's
|
|
56
|
+
// answer alone. Reporting corruption for a wrong key would push an app
|
|
57
|
+
// towards destroying recoverable data, so the encrypted case wins.
|
|
58
|
+
return context.encrypted ? DiagnosticCodes.KEY_INVALID : DiagnosticCodes.CORRUPTION;
|
|
59
|
+
}
|
|
60
|
+
if (CORRUPT.test(text)) {
|
|
61
|
+
return DiagnosticCodes.CORRUPTION;
|
|
62
|
+
}
|
|
63
|
+
if (CONFLICT.test(text)) {
|
|
64
|
+
return DiagnosticCodes.RECORD_CONFLICT;
|
|
65
|
+
}
|
|
66
|
+
if (ACCESS.test(text)) {
|
|
67
|
+
return DiagnosticCodes.STORAGE_ACCESS;
|
|
68
|
+
}
|
|
69
|
+
if (BUSY.test(text)) {
|
|
70
|
+
// Retryable, and distinct from a permanent write failure.
|
|
71
|
+
return context.operation === 'transaction'
|
|
72
|
+
? DiagnosticCodes.TRANSACTION_FAILED
|
|
73
|
+
: DiagnosticCodes.WRITE_FAILED;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
switch (context.operation) {
|
|
77
|
+
case 'open':
|
|
78
|
+
return DiagnosticCodes.OPEN_FAILED;
|
|
79
|
+
case 'read':
|
|
80
|
+
return DiagnosticCodes.STORAGE_ACCESS;
|
|
81
|
+
case 'transaction':
|
|
82
|
+
return DiagnosticCodes.TRANSACTION_FAILED;
|
|
83
|
+
case 'attachment':
|
|
84
|
+
return DiagnosticCodes.ATTACHMENT_WRITE_FAILED;
|
|
85
|
+
case 'delete':
|
|
86
|
+
case 'write':
|
|
87
|
+
default:
|
|
88
|
+
return DiagnosticCodes.WRITE_FAILED;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Re-raise a native failure as a typed one, preserving an error the package
|
|
94
|
+
* already classified rather than reclassifying it as a generic write failure.
|
|
95
|
+
*/
|
|
96
|
+
export function raiseNative(error: unknown, context: NativeContext): never {
|
|
97
|
+
if (isStorageError(error)) {
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
raise(mapNativeError(error, context), {
|
|
101
|
+
// The native message is deliberately not the error's message. It goes
|
|
102
|
+
// through `cause`, which redacts it — passing it here as well would be
|
|
103
|
+
// two chances to leak the same string for no extra information.
|
|
104
|
+
message: `native ${context.operation} failed`,
|
|
105
|
+
detail: { operation: context.operation },
|
|
106
|
+
cause: error,
|
|
107
|
+
});
|
|
108
|
+
}
|