@dinoxx/dinox-cli 1.1.1 → 2.0.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/README.md +44 -3
- package/dist/cli.js +4 -0
- package/dist/commands/auth/index.js +1 -2
- package/dist/commands/boxes/index.js +37 -13
- package/dist/commands/boxes/operations.d.ts +3 -3
- package/dist/commands/boxes/operations.js +51 -28
- package/dist/commands/info/index.js +30 -5
- package/dist/commands/meta/repo.js +2 -1
- package/dist/commands/notes/actions.js +20 -37
- package/dist/commands/notes/bulk.d.ts +2 -2
- package/dist/commands/notes/bulk.js +3 -18
- package/dist/commands/notes/contentPatch.d.ts +5 -1
- package/dist/commands/notes/contentPatch.js +12 -1
- package/dist/commands/notes/contentPatchCommand.d.ts +3 -0
- package/dist/commands/notes/contentPatchCommand.js +29 -8
- package/dist/commands/notes/descriptors.js +19 -16
- package/dist/commands/notes/export.js +3 -0
- package/dist/commands/notes/repo.d.ts +9 -0
- package/dist/commands/notes/repo.js +65 -15
- package/dist/commands/notes/searchOutput.d.ts +2 -1
- package/dist/commands/notes/searchOutput.js +2 -1
- package/dist/commands/notes/searchSql.js +9 -3
- package/dist/commands/prompt/index.d.ts +1 -0
- package/dist/commands/prompt/index.js +26 -11
- package/dist/commands/savedViews/index.d.ts +7 -0
- package/dist/commands/savedViews/index.js +458 -0
- package/dist/commands/savedViews/model.d.ts +87 -0
- package/dist/commands/savedViews/model.js +306 -0
- package/dist/commands/savedViews/repo.d.ts +123 -0
- package/dist/commands/savedViews/repo.js +512 -0
- package/dist/commands/savedViews/semantics.d.ts +46 -0
- package/dist/commands/savedViews/semantics.js +738 -0
- package/dist/commands/schema/renderSkills.d.ts +2 -0
- package/dist/commands/schema/renderSkills.js +69 -10
- package/dist/commands/schema/spec.js +4 -0
- package/dist/commands/skills/index.d.ts +54 -0
- package/dist/commands/skills/index.js +468 -0
- package/dist/commands/storage/index.js +2 -2
- package/dist/commands/sync.js +2 -1
- package/dist/commands/tags/index.js +30 -11
- package/dist/commands/tags/operations.d.ts +3 -3
- package/dist/commands/tags/operations.js +45 -20
- package/dist/commands/templates/model.d.ts +50 -0
- package/dist/commands/templates/model.js +239 -0
- package/dist/commands/todo/index.js +3 -3
- package/dist/commands/update.d.ts +1 -0
- package/dist/commands/update.js +46 -9
- package/dist/commands/writeConfirmation.d.ts +13 -0
- package/dist/commands/writeConfirmation.js +54 -0
- package/dist/config/resolve.js +5 -10
- package/dist/config/types.d.ts +3 -6
- package/dist/constants/links.d.ts +18 -1
- package/dist/constants/links.js +26 -1
- package/dist/daemon/client.d.ts +2 -2
- package/dist/daemon/client.js +106 -52
- package/dist/daemon/index.d.ts +5 -3
- package/dist/daemon/index.js +28 -5
- package/dist/daemon/lifecycle.d.ts +7 -2
- package/dist/daemon/lifecycle.js +53 -13
- package/dist/daemon/pid.d.ts +1 -1
- package/dist/daemon/pid.js +6 -3
- package/dist/dinox.js +9 -1
- package/dist/powersync/exclusiveRuntime.d.ts +1 -0
- package/dist/powersync/exclusiveRuntime.js +26 -8
- package/dist/powersync/runtime.d.ts +3 -2
- package/dist/powersync/runtime.js +19 -3
- package/dist/powersync/schema/content.d.ts +1 -78
- package/dist/powersync/schema/content.js +1 -78
- package/dist/powersync/schema/electronCanonical.d.ts +1561 -0
- package/dist/powersync/schema/electronCanonical.js +1154 -0
- package/dist/powersync/schema/index.d.ts +573 -129
- package/dist/powersync/schema/index.js +3 -32
- package/dist/powersync/schema/local.d.ts +1 -56
- package/dist/powersync/schema/local.js +1 -60
- package/dist/powersync/schema/note.d.ts +1 -35
- package/dist/powersync/schema/note.js +1 -35
- package/dist/powersync/schema/notesExtras.d.ts +1 -62
- package/dist/powersync/schema/notesExtras.js +1 -71
- package/dist/powersync/schema/projects.d.ts +1 -104
- package/dist/powersync/schema/projects.js +1 -104
- package/dist/powersync/schema/savedViews.d.ts +1 -0
- package/dist/powersync/schema/savedViews.js +1 -0
- package/dist/powersync/schema/storage.d.ts +1 -24
- package/dist/powersync/schema/storage.js +1 -24
- package/dist/powersync/schema/tags.d.ts +1 -37
- package/dist/powersync/schema/tags.js +1 -37
- package/dist/powersync/syncGate.d.ts +20 -2
- package/dist/powersync/uploader.d.ts +16 -2
- package/dist/powersync/uploader.js +152 -158
- package/dist/skills/check.d.ts +1 -0
- package/dist/skills/check.js +538 -70
- package/dist/skills/retired.d.ts +60 -0
- package/dist/skills/retired.js +58 -0
- package/dist/utils/deadline.d.ts +8 -0
- package/dist/utils/deadline.js +31 -0
- package/dist/utils/dryRun.d.ts +20 -1
- package/dist/utils/dryRun.js +22 -3
- package/dist/utils/errors.d.ts +2 -0
- package/dist/utils/errors.js +2 -0
- package/dist/utils/processExit.d.ts +1 -0
- package/dist/utils/processExit.js +33 -0
- package/dist/utils/updateNotice.d.ts +1 -0
- package/dist/utils/updateNotice.js +1 -1
- package/package.json +8 -5
|
@@ -1,24 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export const c_storage = new Table({
|
|
3
|
-
id: column.text,
|
|
4
|
-
user_id: column.text,
|
|
5
|
-
alias_name: column.text,
|
|
6
|
-
protocol: column.text,
|
|
7
|
-
provider: column.text,
|
|
8
|
-
is_active: column.integer,
|
|
9
|
-
sort_rank: column.integer,
|
|
10
|
-
endpoint: column.text,
|
|
11
|
-
bucket: column.text,
|
|
12
|
-
region: column.text,
|
|
13
|
-
access_key_id: column.text,
|
|
14
|
-
secret_access_key: column.text,
|
|
15
|
-
force_path_style: column.integer,
|
|
16
|
-
path_prefix: column.text,
|
|
17
|
-
custom_domain: column.text,
|
|
18
|
-
legacy_domains: column.text,
|
|
19
|
-
is_private: column.integer,
|
|
20
|
-
extra_config: column.text,
|
|
21
|
-
created_at: column.text,
|
|
22
|
-
updated_at: column.text,
|
|
23
|
-
deleted_at: column.text,
|
|
24
|
-
});
|
|
1
|
+
export { c_storage } from './electronCanonical.js';
|
|
@@ -1,37 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export declare const c_tag: Table<{
|
|
3
|
-
id: import("@powersync/common").BaseColumnType<string | null>;
|
|
4
|
-
tags: import("@powersync/common").BaseColumnType<string | null>;
|
|
5
|
-
insert_at: import("@powersync/common").BaseColumnType<string | null>;
|
|
6
|
-
extra_data: import("@powersync/common").BaseColumnType<string | null>;
|
|
7
|
-
update_at: import("@powersync/common").BaseColumnType<string | null>;
|
|
8
|
-
user_id: import("@powersync/common").BaseColumnType<string | null>;
|
|
9
|
-
is_del: import("@powersync/common").BaseColumnType<number | null>;
|
|
10
|
-
favorite_tags: import("@powersync/common").BaseColumnType<string | null>;
|
|
11
|
-
}>;
|
|
12
|
-
export declare const c_tag_node: Table<{
|
|
13
|
-
id: import("@powersync/common").BaseColumnType<string | null>;
|
|
14
|
-
user_id: import("@powersync/common").BaseColumnType<string | null>;
|
|
15
|
-
emoji: import("@powersync/common").BaseColumnType<string | null>;
|
|
16
|
-
full_path: import("@powersync/common").BaseColumnType<string | null>;
|
|
17
|
-
path_key: import("@powersync/common").BaseColumnType<string | null>;
|
|
18
|
-
parent_id: import("@powersync/common").BaseColumnType<string | null>;
|
|
19
|
-
depth: import("@powersync/common").BaseColumnType<number | null>;
|
|
20
|
-
is_actual: import("@powersync/common").BaseColumnType<number | null>;
|
|
21
|
-
is_del: import("@powersync/common").BaseColumnType<number | null>;
|
|
22
|
-
created_at: import("@powersync/common").BaseColumnType<string | null>;
|
|
23
|
-
updated_at: import("@powersync/common").BaseColumnType<string | null>;
|
|
24
|
-
meta: import("@powersync/common").BaseColumnType<string | null>;
|
|
25
|
-
}>;
|
|
26
|
-
export declare const c_tag_preference: Table<{
|
|
27
|
-
id: import("@powersync/common").BaseColumnType<string | null>;
|
|
28
|
-
user_id: import("@powersync/common").BaseColumnType<string | null>;
|
|
29
|
-
tag_id: import("@powersync/common").BaseColumnType<string | null>;
|
|
30
|
-
is_favorite: import("@powersync/common").BaseColumnType<number | null>;
|
|
31
|
-
is_expanded: import("@powersync/common").BaseColumnType<number | null>;
|
|
32
|
-
pinned_at: import("@powersync/common").BaseColumnType<string | null>;
|
|
33
|
-
sort_rank: import("@powersync/common").BaseColumnType<number | null>;
|
|
34
|
-
pinned_rank: import("@powersync/common").BaseColumnType<number | null>;
|
|
35
|
-
created_at: import("@powersync/common").BaseColumnType<string | null>;
|
|
36
|
-
updated_at: import("@powersync/common").BaseColumnType<string | null>;
|
|
37
|
-
}>;
|
|
1
|
+
export { c_tag_node, c_tag_preference } from './electronCanonical.js';
|
|
@@ -1,37 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export const c_tag = new Table({
|
|
3
|
-
id: column.text,
|
|
4
|
-
tags: column.text,
|
|
5
|
-
insert_at: column.text,
|
|
6
|
-
extra_data: column.text,
|
|
7
|
-
update_at: column.text,
|
|
8
|
-
user_id: column.text,
|
|
9
|
-
is_del: column.integer,
|
|
10
|
-
favorite_tags: column.text,
|
|
11
|
-
});
|
|
12
|
-
export const c_tag_node = new Table({
|
|
13
|
-
id: column.text,
|
|
14
|
-
user_id: column.text,
|
|
15
|
-
emoji: column.text,
|
|
16
|
-
full_path: column.text,
|
|
17
|
-
path_key: column.text,
|
|
18
|
-
parent_id: column.text,
|
|
19
|
-
depth: column.integer,
|
|
20
|
-
is_actual: column.integer,
|
|
21
|
-
is_del: column.integer,
|
|
22
|
-
created_at: column.text,
|
|
23
|
-
updated_at: column.text,
|
|
24
|
-
meta: column.text,
|
|
25
|
-
});
|
|
26
|
-
export const c_tag_preference = new Table({
|
|
27
|
-
id: column.text,
|
|
28
|
-
user_id: column.text,
|
|
29
|
-
tag_id: column.text,
|
|
30
|
-
is_favorite: column.integer,
|
|
31
|
-
is_expanded: column.integer,
|
|
32
|
-
pinned_at: column.text,
|
|
33
|
-
sort_rank: column.integer,
|
|
34
|
-
pinned_rank: column.integer,
|
|
35
|
-
created_at: column.text,
|
|
36
|
-
updated_at: column.text,
|
|
37
|
-
});
|
|
1
|
+
export { c_tag_node, c_tag_preference } from './electronCanonical.js';
|
|
@@ -1,4 +1,22 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { SyncStatus } from '@powersync/common';
|
|
2
|
+
/**
|
|
3
|
+
* Plain copy of the PowerSync sync status, reported as `status.raw` in JSON output.
|
|
4
|
+
* Keeps the 1.x `SyncStatusOptions` shape (PowerSync 2 made SyncStatus an interface
|
|
5
|
+
* and changed its toJSON() to an internal format).
|
|
6
|
+
*/
|
|
7
|
+
export type SyncStatusRaw = {
|
|
8
|
+
connected?: boolean;
|
|
9
|
+
connecting?: boolean;
|
|
10
|
+
hasSynced?: boolean;
|
|
11
|
+
lastSyncedAt?: Date;
|
|
12
|
+
dataFlow?: {
|
|
13
|
+
downloading?: boolean;
|
|
14
|
+
uploading?: boolean;
|
|
15
|
+
downloadError?: Error;
|
|
16
|
+
uploadError?: Error;
|
|
17
|
+
};
|
|
18
|
+
priorityStatusEntries?: SyncStatus['priorityStatusEntries'];
|
|
19
|
+
};
|
|
2
20
|
import { DinoxError } from '../utils/errors.js';
|
|
3
21
|
export type SyncGateFailureReason = 'offline' | 'first-sync-timeout' | 'checkpoint-timeout' | 'data-flow-timeout' | 'index-timeout' | 'not-connected' | 'not-synced' | 'download-error';
|
|
4
22
|
export type SyncGateResult = {
|
|
@@ -20,7 +38,7 @@ export type SyncGateInput = {
|
|
|
20
38
|
message: string;
|
|
21
39
|
};
|
|
22
40
|
};
|
|
23
|
-
raw?:
|
|
41
|
+
raw?: SyncStatusRaw;
|
|
24
42
|
};
|
|
25
43
|
};
|
|
26
44
|
export declare function evaluateSyncGate(input: SyncGateInput): SyncGateResult;
|
|
@@ -1,8 +1,22 @@
|
|
|
1
|
+
import { type CrudTransaction } from '@powersync/common';
|
|
1
2
|
import type { UploadHandler } from './connector.js';
|
|
3
|
+
export type UploadOperation = {
|
|
4
|
+
op_id: number;
|
|
5
|
+
op: 'PUT' | 'PATCH' | 'DELETE';
|
|
6
|
+
type: string;
|
|
7
|
+
id: string;
|
|
8
|
+
tx_id?: number;
|
|
9
|
+
data?: Record<string, unknown>;
|
|
10
|
+
old?: Record<string, unknown>;
|
|
11
|
+
old_sha256?: Record<string, string>;
|
|
12
|
+
metadata?: string;
|
|
13
|
+
};
|
|
14
|
+
type CrudOperation = CrudTransaction['crud'][number];
|
|
15
|
+
export declare function toUploadOperation(op: CrudOperation): UploadOperation;
|
|
2
16
|
export declare function createPowerSyncUploadHandler(options: {
|
|
3
17
|
authorization: string;
|
|
4
18
|
uploadBaseUrl: string;
|
|
5
|
-
|
|
6
|
-
uploadV2Path: string;
|
|
19
|
+
uploadPath: string;
|
|
7
20
|
timeoutMs?: number;
|
|
8
21
|
}): UploadHandler;
|
|
22
|
+
export {};
|
|
@@ -1,135 +1,164 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
1
2
|
import { UpdateType } from '@powersync/common';
|
|
2
3
|
import { createTimeoutSignal, mergeAbortSignals } from '../utils/abort.js';
|
|
3
4
|
import { toAuthorizationHeaderValue } from '../utils/authToken.js';
|
|
4
5
|
import { sanitizeForTerminal } from '../utils/terminalSanitization.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Uploads each PowerSync CRUD transaction as ONE request to the backend v6 batch
|
|
8
|
+
* endpoint (same contract as dinox-electron / dinox-web / dinox-app). The backend
|
|
9
|
+
* applies the batch atomically in arrival order and answers `applied` or
|
|
10
|
+
* `rejected`, so a transaction either lands whole or not at all.
|
|
11
|
+
*/
|
|
12
|
+
const NON_RETRYABLE_UPLOAD_HTTP_STATUSES = new Set([400, 409, 422]);
|
|
13
|
+
// Pre-edit values longer than this travel as a SHA-256 so long note bodies are not
|
|
14
|
+
// re-uploaded as conflict-detection bases; the backend hashes its stored text.
|
|
15
|
+
const OLD_VALUE_HASH_THRESHOLD = 256;
|
|
16
|
+
const UPLOAD_OMITTED_COLUMNS_BY_TABLE = new Map([
|
|
17
|
+
// The backend v6 tag-node mapper does not consume this local metadata column.
|
|
18
|
+
['c_tag_node', new Set(['meta'])],
|
|
19
|
+
// Server-generated scheduled-task columns must not be echoed back.
|
|
20
|
+
['c_scheduled_task', new Set(['last_run_at', 'last_run_id'])],
|
|
21
|
+
]);
|
|
22
|
+
const UPLOAD_REQUIRED_INSERT_DEFAULTS_BY_TABLE = new Map([
|
|
23
|
+
// PowerSync omits NULL values from PUT payloads, while the backend requires these.
|
|
24
|
+
['c_note_template', { content: '', extra_data: '{}' }],
|
|
25
|
+
]);
|
|
26
|
+
function isRecord(value) {
|
|
27
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
9
28
|
}
|
|
10
29
|
function normalizeBaseUrl(baseUrl) {
|
|
11
30
|
return baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
|
|
12
31
|
}
|
|
13
|
-
function
|
|
14
|
-
return `${normalizeBaseUrl(baseUrl)}${uploadV4Path}`;
|
|
15
|
-
}
|
|
16
|
-
async function readErrorBody(response) {
|
|
17
|
-
const contentType = response.headers.get('content-type') ?? '';
|
|
18
|
-
const text = await response.text();
|
|
19
|
-
if (contentType.includes('application/json')) {
|
|
20
|
-
try {
|
|
21
|
-
const json = JSON.parse(text);
|
|
22
|
-
const code = json?.error?.code ?? json?.code;
|
|
23
|
-
const message = json?.error?.message ?? json?.message;
|
|
24
|
-
return {
|
|
25
|
-
code: typeof code === 'string' ? code : undefined,
|
|
26
|
-
message: typeof message === 'string' ? message : undefined,
|
|
27
|
-
text,
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
catch {
|
|
31
|
-
return { text };
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return { text };
|
|
35
|
-
}
|
|
36
|
-
function createUploadError(args) {
|
|
37
|
-
const details = args.message ?? args.text;
|
|
38
|
-
const message = details
|
|
39
|
-
? `PowerSync upload failed (${args.method} ${args.url}) [${args.status}]: ${details}`
|
|
40
|
-
: `PowerSync upload failed (${args.method} ${args.url}) [${args.status}]`;
|
|
32
|
+
function createUploadError(message, extra = {}) {
|
|
41
33
|
const error = new Error(message);
|
|
42
34
|
error.name = 'PowerSyncUploadError';
|
|
43
|
-
error.code =
|
|
44
|
-
error.status =
|
|
35
|
+
error.code = extra.code;
|
|
36
|
+
error.status = extra.status;
|
|
37
|
+
error.fatal = extra.fatal;
|
|
45
38
|
return error;
|
|
46
39
|
}
|
|
47
|
-
function
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
40
|
+
function toUploadOperationName(op) {
|
|
41
|
+
switch (op.op) {
|
|
42
|
+
case UpdateType.PUT:
|
|
43
|
+
return 'PUT';
|
|
44
|
+
case UpdateType.PATCH:
|
|
45
|
+
return 'PATCH';
|
|
46
|
+
case UpdateType.DELETE:
|
|
47
|
+
return 'DELETE';
|
|
48
|
+
default:
|
|
49
|
+
throw createUploadError(`Unsupported PowerSync CRUD operation: ${String(op.op)} (${String(op.table)}:${String(op.id ?? '')})`, { code: 'UNSUPPORTED_CRUD_OPERATION', fatal: true });
|
|
54
50
|
}
|
|
55
|
-
return typeof uploadError.code === 'string'
|
|
56
|
-
&& FATAL_RESPONSE_CODES.some(regex => regex.test(uploadError.code));
|
|
57
51
|
}
|
|
58
|
-
function
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
52
|
+
function omitColumns(table, values) {
|
|
53
|
+
const omitted = UPLOAD_OMITTED_COLUMNS_BY_TABLE.get(table);
|
|
54
|
+
if (!omitted)
|
|
55
|
+
return values;
|
|
56
|
+
const next = { ...values };
|
|
57
|
+
for (const column of omitted)
|
|
58
|
+
delete next[column];
|
|
59
|
+
return next;
|
|
64
60
|
}
|
|
65
|
-
|
|
66
|
-
|
|
61
|
+
/** `old` is the conflict-detection base (trackPrevious); long values move to `old_sha256`. */
|
|
62
|
+
function compactPreviousValues(previous) {
|
|
63
|
+
if (!previous)
|
|
64
|
+
return {};
|
|
65
|
+
const old = {};
|
|
66
|
+
const oldSha256 = {};
|
|
67
|
+
for (const [column, value] of Object.entries(previous)) {
|
|
68
|
+
if (typeof value === 'string' && value.length > OLD_VALUE_HASH_THRESHOLD) {
|
|
69
|
+
oldSha256[column] = crypto.createHash('sha256').update(value, 'utf8').digest('hex');
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
old[column] = value;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
67
75
|
return {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
table: String(op.table ?? ''),
|
|
76
|
+
...(Object.keys(old).length > 0 ? { old } : {}),
|
|
77
|
+
...(Object.keys(oldSha256).length > 0 ? { old_sha256: oldSha256 } : {}),
|
|
71
78
|
};
|
|
72
79
|
}
|
|
73
|
-
function
|
|
74
|
-
|
|
75
|
-
|
|
80
|
+
export function toUploadOperation(op) {
|
|
81
|
+
const name = toUploadOperationName(op);
|
|
82
|
+
const table = String(op.table);
|
|
83
|
+
let data = isRecord(op.opData) ? { ...op.opData } : undefined;
|
|
84
|
+
if (name === 'PUT') {
|
|
85
|
+
data ??= {};
|
|
86
|
+
for (const [column, value] of Object.entries(UPLOAD_REQUIRED_INSERT_DEFAULTS_BY_TABLE.get(table) ?? {})) {
|
|
87
|
+
if (data[column] == null)
|
|
88
|
+
data[column] = value;
|
|
89
|
+
}
|
|
76
90
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
91
|
+
return {
|
|
92
|
+
op_id: op.clientId,
|
|
93
|
+
op: name,
|
|
94
|
+
type: table,
|
|
95
|
+
id: String(op.id),
|
|
96
|
+
...(op.transactionId !== undefined ? { tx_id: op.transactionId } : {}),
|
|
97
|
+
...(data ? { data: omitColumns(table, data) } : {}),
|
|
98
|
+
// DELETE needs no base: deletes win (PowerSync default), nothing to compare.
|
|
99
|
+
...(name === 'PATCH' ? compactPreviousValues(op.previousValues) : {}),
|
|
100
|
+
...(op.metadata !== undefined ? { metadata: op.metadata } : {}),
|
|
80
101
|
};
|
|
81
102
|
}
|
|
82
|
-
function
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
103
|
+
function isEmptyPatch(operation) {
|
|
104
|
+
return operation.op === 'PATCH' && (!operation.data || Object.keys(operation.data).length === 0);
|
|
105
|
+
}
|
|
106
|
+
function isUploadResponse(value) {
|
|
107
|
+
if (!isRecord(value))
|
|
108
|
+
return false;
|
|
109
|
+
if (value.status !== 'applied' && value.status !== 'rejected')
|
|
110
|
+
return false;
|
|
111
|
+
return typeof value.operationCount === 'number' && Number.isFinite(value.operationCount);
|
|
112
|
+
}
|
|
113
|
+
async function readResponseBody(response) {
|
|
114
|
+
const text = await response.text();
|
|
115
|
+
try {
|
|
116
|
+
return { json: JSON.parse(text), text };
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return { text };
|
|
87
120
|
}
|
|
88
|
-
const failedOperation = context.operations[context.failedIndex];
|
|
89
|
-
const failedOperationText = failedOperation
|
|
90
|
-
? `${failedOperation.op}:${failedOperation.table}:${failedOperation.id || '(no-id)'}`
|
|
91
|
-
: `index ${context.failedIndex}`;
|
|
92
|
-
const succeededBeforeFailure = Math.min(context.failedIndex, context.operations.length);
|
|
93
|
-
const replayWarning = succeededBeforeFailure > 0
|
|
94
|
-
? ` ${succeededBeforeFailure} earlier operation(s) already returned success and may be sent again on retry; the legacy upload endpoints do not provide a transaction-level acknowledgement.`
|
|
95
|
-
: '';
|
|
96
|
-
process.stderr.write(`${sanitizeForTerminal(`warning: fatal PowerSync upload error (${error.code ?? error.status ?? 'unknown'}) at ${failedOperationText}; PowerSync retained the entire ${context.operations.length}-operation transaction.${replayWarning}`)}\n`);
|
|
97
121
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
122
|
+
function errorDetails(json) {
|
|
123
|
+
if (!isRecord(json))
|
|
124
|
+
return {};
|
|
125
|
+
const nested = isRecord(json.error) ? json.error : json;
|
|
126
|
+
return {
|
|
127
|
+
code: typeof nested.code === 'string' ? nested.code : undefined,
|
|
128
|
+
message: typeof nested.message === 'string' ? nested.message : undefined,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
async function postBatch(args) {
|
|
132
|
+
const timeout = typeof args.timeoutMs === 'number' && Number.isFinite(args.timeoutMs) && args.timeoutMs > 0
|
|
133
|
+
? createTimeoutSignal(args.timeoutMs)
|
|
101
134
|
: null;
|
|
102
|
-
const requestSignal = mergeAbortSignals([options.signal, timeout?.signal]);
|
|
103
135
|
try {
|
|
104
|
-
const response = await fetch(
|
|
105
|
-
method:
|
|
136
|
+
const response = await fetch(args.url, {
|
|
137
|
+
method: 'POST',
|
|
106
138
|
headers: {
|
|
107
|
-
Authorization: toAuthorizationHeaderValue(
|
|
139
|
+
Authorization: toAuthorizationHeaderValue(args.authorization),
|
|
108
140
|
'Content-Type': 'application/json',
|
|
109
141
|
},
|
|
110
|
-
body: JSON.stringify(
|
|
111
|
-
signal:
|
|
142
|
+
body: JSON.stringify({ batch: args.batch }),
|
|
143
|
+
signal: mergeAbortSignals([timeout?.signal]),
|
|
112
144
|
});
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
145
|
+
const body = await readResponseBody(response);
|
|
146
|
+
if (!response.ok) {
|
|
147
|
+
const details = errorDetails(body.json);
|
|
148
|
+
throw createUploadError(`PowerSync upload failed (POST ${args.url}) [${response.status}]: ${details.message ?? body.text}`, {
|
|
149
|
+
code: details.code ?? `HTTP_${response.status}`,
|
|
150
|
+
status: response.status,
|
|
151
|
+
fatal: NON_RETRYABLE_UPLOAD_HTTP_STATUSES.has(response.status),
|
|
152
|
+
});
|
|
116
153
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
url: options.url,
|
|
122
|
-
code: body.code,
|
|
123
|
-
message: body.message,
|
|
124
|
-
text: body.text,
|
|
125
|
-
});
|
|
154
|
+
if (!isUploadResponse(body.json)) {
|
|
155
|
+
throw createUploadError(`Invalid PowerSync v6 response from ${args.url}`);
|
|
156
|
+
}
|
|
157
|
+
return body.json;
|
|
126
158
|
}
|
|
127
159
|
catch (error) {
|
|
128
|
-
if (timeout?.signal.aborted
|
|
129
|
-
|
|
130
|
-
err.name = 'PowerSyncUploadError';
|
|
131
|
-
err.status = 408;
|
|
132
|
-
throw err;
|
|
160
|
+
if (timeout?.signal.aborted) {
|
|
161
|
+
throw createUploadError(`PowerSync upload timed out (POST ${args.url}) after ${args.timeoutMs}ms`, { status: 408 });
|
|
133
162
|
}
|
|
134
163
|
throw error;
|
|
135
164
|
}
|
|
@@ -137,76 +166,41 @@ async function dispatchMutation(options) {
|
|
|
137
166
|
timeout?.cancel();
|
|
138
167
|
}
|
|
139
168
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const primaryUrl = `${baseUrl}${args.uploadV4Path}`;
|
|
143
|
-
const fallbackUrl = `${baseUrl}${args.uploadV2Path}`;
|
|
144
|
-
const primaryKey = legacyUploadTargetKey(baseUrl, args.uploadV4Path);
|
|
145
|
-
const dispatchPutOrPatch = async (method, payload) => {
|
|
146
|
-
if (legacyUploadTargets.has(primaryKey)) {
|
|
147
|
-
await dispatchMutation({ authorization: args.authorization, url: fallbackUrl, method, payload, timeoutMs: args.timeoutMs });
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
try {
|
|
151
|
-
await dispatchMutation({ authorization: args.authorization, url: primaryUrl, method, payload, timeoutMs: args.timeoutMs });
|
|
152
|
-
}
|
|
153
|
-
catch (error) {
|
|
154
|
-
if (shouldFallbackToLegacy(error?.status)) {
|
|
155
|
-
legacyUploadTargets.add(primaryKey);
|
|
156
|
-
await dispatchMutation({ authorization: args.authorization, url: fallbackUrl, method, payload, timeoutMs: args.timeoutMs });
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
throw error;
|
|
160
|
-
}
|
|
161
|
-
};
|
|
162
|
-
for (let index = 0; index < args.transaction.crud.length; index += 1) {
|
|
163
|
-
const op = args.transaction.crud[index];
|
|
164
|
-
const record = { table: op.table, data: { ...(op.opData ?? {}), id: op.id } };
|
|
165
|
-
try {
|
|
166
|
-
switch (op.op) {
|
|
167
|
-
case UpdateType.PUT:
|
|
168
|
-
await dispatchPutOrPatch('PUT', record);
|
|
169
|
-
break;
|
|
170
|
-
case UpdateType.PATCH:
|
|
171
|
-
await dispatchPutOrPatch('PATCH', record);
|
|
172
|
-
break;
|
|
173
|
-
case UpdateType.DELETE:
|
|
174
|
-
await dispatchMutation({ authorization: args.authorization, url: fallbackUrl, method: 'DELETE', payload: record, timeoutMs: args.timeoutMs });
|
|
175
|
-
break;
|
|
176
|
-
default:
|
|
177
|
-
throw createUnsupportedCrudOperationError(op);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
catch (error) {
|
|
181
|
-
attachFatalContext(error, args.transaction, index);
|
|
182
|
-
throw error;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
169
|
+
function describeOperation(operation) {
|
|
170
|
+
return operation ? `${operation.op}:${operation.type}:${operation.id}` : '(none)';
|
|
185
171
|
}
|
|
186
172
|
export function createPowerSyncUploadHandler(options) {
|
|
173
|
+
const url = `${normalizeBaseUrl(options.uploadBaseUrl)}${options.uploadPath}`;
|
|
187
174
|
return async (database) => {
|
|
188
175
|
const transaction = await database.getNextCrudTransaction();
|
|
189
176
|
if (!transaction) {
|
|
190
177
|
return;
|
|
191
178
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
authorization: options.authorization,
|
|
196
|
-
uploadBaseUrl: options.uploadBaseUrl,
|
|
197
|
-
uploadV4Path: options.uploadV4Path,
|
|
198
|
-
uploadV2Path: options.uploadV2Path,
|
|
199
|
-
timeoutMs: options.timeoutMs,
|
|
200
|
-
});
|
|
179
|
+
// Redundant local updates can collapse into empty PATCHes; they carry no state.
|
|
180
|
+
const batch = transaction.crud.map(toUploadOperation).filter(op => !isEmptyPatch(op));
|
|
181
|
+
if (batch.length === 0) {
|
|
201
182
|
await transaction.complete();
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
let result;
|
|
186
|
+
try {
|
|
187
|
+
result = await postBatch({ url, authorization: options.authorization, batch, timeoutMs: options.timeoutMs });
|
|
202
188
|
}
|
|
203
189
|
catch (error) {
|
|
204
|
-
if (
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
logFatalUploadFailure(error);
|
|
190
|
+
if (error.fatal) {
|
|
191
|
+
process.stderr.write(`${sanitizeForTerminal(`warning: PowerSync refused the ${batch.length}-operation upload (${error.code}); ` +
|
|
192
|
+
`the local changes were kept in the upload queue. First operation: ${describeOperation(batch[0])}`)}\n`);
|
|
208
193
|
}
|
|
209
194
|
throw error;
|
|
210
195
|
}
|
|
196
|
+
if (result.status === 'rejected') {
|
|
197
|
+
// The backend applied nothing. Keep the transaction so the command reports the
|
|
198
|
+
// failure (`--durability uploaded` must not succeed) instead of dropping edits.
|
|
199
|
+
const error = createUploadError(`PowerSync rejected the upload (${result.code ?? 'REJECTED'}): ${result.message ?? 'no message'}`, { code: result.code ?? 'REJECTED', fatal: true });
|
|
200
|
+
process.stderr.write(`${sanitizeForTerminal(`warning: ${error.message}; ${batch.length} operation(s) were kept in the upload queue. ` +
|
|
201
|
+
`First operation: ${describeOperation(batch[0])}`)}\n`);
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
await transaction.complete();
|
|
211
205
|
};
|
|
212
206
|
}
|
package/dist/skills/check.d.ts
CHANGED
|
@@ -4,3 +4,4 @@ export type SkillCheckIssue = {
|
|
|
4
4
|
};
|
|
5
5
|
export declare function checkSkillContent(skill: string, content: string, skillDir?: string, expectedVersion?: string): SkillCheckIssue[];
|
|
6
6
|
export declare function checkSkillsDirectory(skillsDir: string): SkillCheckIssue[];
|
|
7
|
+
export declare function checkSkillDirectory(skillDir: string, expectedVersion?: string): SkillCheckIssue[];
|