@dinoxx/dinox-cli 1.0.22 → 1.1.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.
- package/README.md +33 -10
- package/dist/auth/userInfo.d.ts +2 -0
- package/dist/auth/userInfo.js +55 -14
- package/dist/cli.js +36 -2
- package/dist/commands/auth/index.d.ts +35 -0
- package/dist/commands/auth/index.js +382 -83
- package/dist/commands/boxes/index.js +2 -6
- package/dist/commands/boxes/model.d.ts +3 -0
- package/dist/commands/boxes/model.js +16 -1
- package/dist/commands/boxes/operations.js +18 -8
- package/dist/commands/boxes/repo.js +13 -11
- package/dist/commands/boxes/store.js +1 -1
- package/dist/commands/config/index.js +7 -5
- package/dist/commands/descriptor/register.js +9 -0
- package/dist/commands/doctor.d.ts +123 -0
- package/dist/commands/doctor.js +539 -0
- package/dist/commands/graph/index.d.ts +1 -1
- package/dist/commands/graph/index.js +35 -21
- package/dist/commands/graph/repo.js +92 -17
- package/dist/commands/meta/index.js +4 -4
- package/dist/commands/meta/repo.js +4 -3
- package/dist/commands/notes/actions.d.ts +1 -1
- package/dist/commands/notes/actions.js +121 -78
- package/dist/commands/notes/bulk.d.ts +1 -0
- package/dist/commands/notes/bulk.js +23 -17
- package/dist/commands/notes/contentEngine.js +69 -11
- package/dist/commands/notes/contentPatch.d.ts +4 -1
- package/dist/commands/notes/contentPatch.js +105 -41
- package/dist/commands/notes/contentPatchCommand.js +28 -11
- package/dist/commands/notes/descriptors.js +50 -20
- package/dist/commands/notes/dryRun.d.ts +1 -0
- package/dist/commands/notes/dryRun.js +4 -7
- package/dist/commands/notes/export.d.ts +6 -0
- package/dist/commands/notes/export.js +26 -6
- package/dist/commands/notes/metadataMutation.d.ts +1 -0
- package/dist/commands/notes/metadataMutation.js +14 -3
- package/dist/commands/notes/repo.d.ts +46 -4
- package/dist/commands/notes/repo.js +113 -28
- package/dist/commands/notes/runtime.d.ts +2 -1
- package/dist/commands/notes/runtime.js +17 -1
- package/dist/commands/notes/searchSql.js +13 -6
- package/dist/commands/notes/writeReceipt.d.ts +20 -0
- package/dist/commands/notes/writeReceipt.js +82 -0
- package/dist/commands/prompt/index.js +3 -3
- package/dist/commands/prompt/repo.d.ts +4 -1
- package/dist/commands/prompt/repo.js +17 -7
- package/dist/commands/runtime.d.ts +14 -2
- package/dist/commands/runtime.js +89 -19
- package/dist/commands/schema/renderSkills.js +5 -2
- package/dist/commands/schema/spec.js +4 -0
- package/dist/commands/storage/index.js +8 -2
- package/dist/commands/storage/repo.d.ts +4 -0
- package/dist/commands/storage/repo.js +356 -92
- package/dist/commands/sync.d.ts +23 -1
- package/dist/commands/sync.js +105 -60
- package/dist/commands/tags/content.js +9 -0
- package/dist/commands/tags/index.js +2 -6
- package/dist/commands/tags/operations.js +15 -10
- package/dist/commands/tags/repo.d.ts +2 -0
- package/dist/commands/tags/repo.js +18 -5
- package/dist/commands/tags/store.js +1 -1
- package/dist/commands/todo/contentModel.d.ts +4 -3
- package/dist/commands/todo/contentModel.js +78 -28
- package/dist/commands/todo/dryRun.d.ts +1 -0
- package/dist/commands/todo/dryRun.js +17 -13
- package/dist/commands/todo/index.d.ts +6 -0
- package/dist/commands/todo/index.js +89 -123
- package/dist/commands/todo/repo.d.ts +36 -1
- package/dist/commands/todo/repo.js +146 -37
- package/dist/commands/todo/taskIdentity.d.ts +2 -0
- package/dist/commands/todo/taskIdentity.js +64 -0
- package/dist/commands/todo/taskModel.js +27 -17
- package/dist/commands/update.d.ts +13 -1
- package/dist/commands/update.js +135 -23
- package/dist/commands/writeDryRun.d.ts +3 -2
- package/dist/commands/writeDryRun.js +4 -3
- package/dist/commands/writeDurability.d.ts +5 -0
- package/dist/commands/writeDurability.js +22 -0
- package/dist/config/paths.js +6 -1
- package/dist/config/resolve.d.ts +2 -0
- package/dist/config/resolve.js +23 -9
- package/dist/config/store.d.ts +3 -0
- package/dist/config/store.js +61 -14
- package/dist/config/types.d.ts +4 -0
- package/dist/daemon/client.d.ts +26 -1
- package/dist/daemon/client.js +236 -12
- package/dist/daemon/ensure.js +74 -12
- package/dist/daemon/index.d.ts +42 -0
- package/dist/daemon/index.js +202 -52
- package/dist/daemon/lifecycle.d.ts +44 -0
- package/dist/daemon/lifecycle.js +118 -0
- package/dist/daemon/pid.d.ts +19 -6
- package/dist/daemon/pid.js +147 -13
- package/dist/daemon/route.d.ts +14 -0
- package/dist/daemon/route.js +40 -0
- package/dist/daemon/server.d.ts +11 -0
- package/dist/daemon/server.js +201 -50
- package/dist/daemon/types.d.ts +29 -1
- package/dist/daemon/types.js +1 -0
- package/dist/dinox.js +25 -1
- package/dist/powersync/connector.js +31 -16
- package/dist/powersync/exclusiveRuntime.d.ts +20 -0
- package/dist/powersync/exclusiveRuntime.js +48 -0
- package/dist/powersync/runtime.d.ts +25 -4
- package/dist/powersync/runtime.js +255 -35
- package/dist/powersync/syncGate.d.ts +4 -1
- package/dist/powersync/syncGate.js +11 -2
- package/dist/powersync/syncPass.d.ts +2 -0
- package/dist/powersync/syncPass.js +15 -4
- package/dist/powersync/tokenIndex.d.ts +13 -0
- package/dist/powersync/tokenIndex.js +216 -37
- package/dist/powersync/uploader.js +107 -52
- package/dist/utils/abort.js +0 -4
- package/dist/utils/asyncRelease.d.ts +2 -0
- package/dist/utils/asyncRelease.js +25 -0
- package/dist/utils/bidlinkCanonicalization.js +3 -0
- package/dist/utils/dryRun.d.ts +1 -1
- package/dist/utils/errors.d.ts +15 -1
- package/dist/utils/errors.js +223 -5
- package/dist/utils/output.d.ts +1 -0
- package/dist/utils/output.js +51 -15
- package/dist/utils/redact.js +1 -1
- package/dist/utils/sqliteLease.d.ts +8 -0
- package/dist/utils/sqliteLease.js +59 -0
- package/dist/utils/syncTimeout.d.ts +7 -1
- package/dist/utils/syncTimeout.js +8 -7
- package/dist/utils/terminalSanitization.js +6 -2
- package/dist/utils/tiptapMarkdown.js +49 -3
- package/dist/utils/tiptapMarkdownSyntax.d.ts +9 -0
- package/dist/utils/tiptapMarkdownSyntax.js +249 -0
- package/dist/utils/tokenize.js +24 -2
- package/dist/utils/updateNotice.js +60 -12
- package/package.json +14 -10
- package/dist/commands/mcp/auth.d.ts +0 -4
- package/dist/commands/mcp/auth.js +0 -75
- package/dist/commands/mcp/index.d.ts +0 -2
- package/dist/commands/mcp/index.js +0 -86
- package/dist/commands/mcp/noteContentTools.d.ts +0 -222
- package/dist/commands/mcp/noteContentTools.js +0 -390
- package/dist/commands/mcp/server.d.ts +0 -34
- package/dist/commands/mcp/server.js +0 -424
- package/dist/commands/mcp/tools.d.ts +0 -2
- package/dist/commands/mcp/tools.js +0 -32
- package/dist/commands/mcp/types.d.ts +0 -39
- package/dist/commands/mcp/types.js +0 -1
- package/dist/utils/noteContentEngine.d.ts +0 -333
- package/dist/utils/noteContentEngine.js +0 -740
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import fsp from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import {
|
|
5
|
-
import sharp from 'sharp';
|
|
4
|
+
import { withPowerSyncWriteTransaction } from '../../powersync/runtime.js';
|
|
6
5
|
import { generateId } from '../../utils/id.js';
|
|
7
6
|
import { DinoxError } from '../../utils/errors.js';
|
|
8
7
|
const ACTIVE_CUSTOM_STORAGE_CONFIG_ID_KEY = 'storage:active_custom_config_id';
|
|
@@ -64,14 +63,36 @@ function finishStorageHost(value) {
|
|
|
64
63
|
throw new DinoxError(`Invalid storage endpoint: ${trimmed}`, { cause: error });
|
|
65
64
|
}
|
|
66
65
|
}
|
|
66
|
+
function encodeStorageKeyForUrl(key) {
|
|
67
|
+
return key
|
|
68
|
+
.split('/')
|
|
69
|
+
.map((segment) => {
|
|
70
|
+
return encodeURIComponent(segment).replace(/[!'()*]/g, (character) => {
|
|
71
|
+
return `%${character.charCodeAt(0).toString(16).toUpperCase()}`;
|
|
72
|
+
});
|
|
73
|
+
})
|
|
74
|
+
.join('/');
|
|
75
|
+
}
|
|
76
|
+
function assertStorageKeyUrlSafe(key) {
|
|
77
|
+
const unsafeSegment = key.split('/').find((segment) => segment === '.' || segment === '..');
|
|
78
|
+
if (!unsafeSegment) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
throw new DinoxError('Storage object key must not contain "." or ".." path segments.', {
|
|
82
|
+
code: 'INVALID_STORAGE_KEY',
|
|
83
|
+
suggestion: 'Remove dot-only path segments from --key or the configured storage path prefix.',
|
|
84
|
+
details: { key },
|
|
85
|
+
});
|
|
86
|
+
}
|
|
67
87
|
function buildVirtualHostedStyleUrl(bucket, endpoint, key) {
|
|
68
88
|
const parsed = new URL(endpoint);
|
|
69
89
|
const protocol = parsed.protocol || 'https:';
|
|
70
90
|
const port = parsed.port ? `:${parsed.port}` : '';
|
|
71
91
|
const host = parsed.hostname || parsed.host;
|
|
72
92
|
const hostWithBucket = host.startsWith(`${bucket}.`) ? host : `${bucket}.${host}`;
|
|
93
|
+
const encodedKey = encodeStorageKeyForUrl(key);
|
|
73
94
|
return {
|
|
74
|
-
finalUrl: `${protocol}//${hostWithBucket}${port}/${
|
|
95
|
+
finalUrl: `${protocol}//${hostWithBucket}${port}/${encodedKey}`,
|
|
75
96
|
domain: `${protocol}//${hostWithBucket}${port}`,
|
|
76
97
|
};
|
|
77
98
|
}
|
|
@@ -80,8 +101,9 @@ function buildPathStyleUrl(bucket, endpoint, key) {
|
|
|
80
101
|
const protocol = parsed.protocol || 'https:';
|
|
81
102
|
const port = parsed.port ? `:${parsed.port}` : '';
|
|
82
103
|
const host = parsed.hostname || parsed.host;
|
|
104
|
+
const encodedKey = encodeStorageKeyForUrl(key);
|
|
83
105
|
return {
|
|
84
|
-
finalUrl: `${protocol}//${host}${port}/${bucket}/${
|
|
106
|
+
finalUrl: `${protocol}//${host}${port}/${bucket}/${encodedKey}`,
|
|
85
107
|
domain: `${protocol}//${host}${port}`,
|
|
86
108
|
};
|
|
87
109
|
}
|
|
@@ -149,11 +171,12 @@ function generateThumbnailKey(originalKey) {
|
|
|
149
171
|
return `${dirPath}/thumb/${filenameWithoutExt}_thumbnail.webp`;
|
|
150
172
|
}
|
|
151
173
|
function buildStorageTarget(config, key) {
|
|
174
|
+
assertStorageKeyUrlSafe(key);
|
|
152
175
|
const resolvedEndpoint = finishStorageHost(config.endpoint);
|
|
153
176
|
if (config.customDomain) {
|
|
154
177
|
const domain = finishStorageHost(config.customDomain);
|
|
155
178
|
return {
|
|
156
|
-
storageUrl: `${domain}/${key}`,
|
|
179
|
+
storageUrl: `${domain}/${encodeStorageKeyForUrl(key)}`,
|
|
157
180
|
domain,
|
|
158
181
|
};
|
|
159
182
|
}
|
|
@@ -364,6 +387,7 @@ function buildStorageTestPlan(args) {
|
|
|
364
387
|
};
|
|
365
388
|
}
|
|
366
389
|
async function defaultUploadObject(input) {
|
|
390
|
+
const { PutObjectCommand, S3Client } = await import('@aws-sdk/client-s3');
|
|
367
391
|
const client = new S3Client({
|
|
368
392
|
endpoint: finishStorageHost(input.storage.endpoint),
|
|
369
393
|
region: input.storage.region,
|
|
@@ -379,12 +403,56 @@ async function defaultUploadObject(input) {
|
|
|
379
403
|
Body: fs.createReadStream(input.localPath),
|
|
380
404
|
ContentType: input.contentType,
|
|
381
405
|
ContentLength: input.contentLength,
|
|
406
|
+
IfNoneMatch: input.overwrite ? undefined : '*',
|
|
382
407
|
}));
|
|
383
408
|
return {
|
|
384
409
|
etag: normalizeText(response.ETag) || null,
|
|
385
410
|
};
|
|
386
411
|
}
|
|
412
|
+
function isObjectCollisionError(error) {
|
|
413
|
+
if (!error || typeof error !== 'object') {
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
const record = error;
|
|
417
|
+
return record.$metadata?.httpStatusCode === 409
|
|
418
|
+
|| record.$metadata?.httpStatusCode === 412
|
|
419
|
+
|| record.name === 'ConditionalRequestConflict'
|
|
420
|
+
|| record.name === 'PreconditionFailed'
|
|
421
|
+
|| record.Code === 'PreconditionFailed'
|
|
422
|
+
|| record.code === 'PreconditionFailed';
|
|
423
|
+
}
|
|
424
|
+
function createStorageCollisionError(bucket, keys, failureDetails = {}) {
|
|
425
|
+
return new DinoxError(`Storage object already exists: ${keys.join(', ')}`, {
|
|
426
|
+
code: 'STORAGE_OBJECT_ALREADY_EXISTS',
|
|
427
|
+
suggestion: 'Choose a different --key, or pass --overwrite to replace the existing object.',
|
|
428
|
+
recoverable: true,
|
|
429
|
+
details: {
|
|
430
|
+
bucket,
|
|
431
|
+
keys,
|
|
432
|
+
...failureDetails,
|
|
433
|
+
},
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
function createStorageUploadOutcomeUnknownError(input) {
|
|
437
|
+
return new DinoxError(input.message, {
|
|
438
|
+
cause: input.cause,
|
|
439
|
+
code: 'STORAGE_UPLOAD_OUTCOME_UNKNOWN',
|
|
440
|
+
recoverable: true,
|
|
441
|
+
suggestion: 'Inspect the reported object key in remote storage before retrying; do not assume the failed request left the object unchanged.',
|
|
442
|
+
details: {
|
|
443
|
+
bucket: input.bucket,
|
|
444
|
+
storageKey: input.storageKey,
|
|
445
|
+
operation: input.operation,
|
|
446
|
+
overwrite: input.overwrite,
|
|
447
|
+
remoteOutcome: 'unknown',
|
|
448
|
+
remoteObjectChanged: null,
|
|
449
|
+
affectedKeys: [],
|
|
450
|
+
possiblyAffectedKeys: [...new Set(input.possiblyAffectedKeys)],
|
|
451
|
+
},
|
|
452
|
+
});
|
|
453
|
+
}
|
|
387
454
|
async function defaultDeleteObject(input) {
|
|
455
|
+
const { DeleteObjectCommand, S3Client } = await import('@aws-sdk/client-s3');
|
|
388
456
|
const client = new S3Client({
|
|
389
457
|
endpoint: finishStorageHost(input.storage.endpoint),
|
|
390
458
|
region: input.storage.region,
|
|
@@ -397,9 +465,11 @@ async function defaultDeleteObject(input) {
|
|
|
397
465
|
await client.send(new DeleteObjectCommand({
|
|
398
466
|
Bucket: input.bucket,
|
|
399
467
|
Key: input.key,
|
|
468
|
+
IfMatch: normalizeText(input.etag) || undefined,
|
|
400
469
|
}));
|
|
401
470
|
}
|
|
402
471
|
async function defaultUploadBytes(input) {
|
|
472
|
+
const { PutObjectCommand, S3Client } = await import('@aws-sdk/client-s3');
|
|
403
473
|
const client = new S3Client({
|
|
404
474
|
endpoint: finishStorageHost(input.storage.endpoint),
|
|
405
475
|
region: input.storage.region,
|
|
@@ -415,18 +485,64 @@ async function defaultUploadBytes(input) {
|
|
|
415
485
|
Body: input.body,
|
|
416
486
|
ContentType: input.contentType,
|
|
417
487
|
ContentLength: input.body.byteLength,
|
|
488
|
+
IfNoneMatch: input.overwrite ? undefined : '*',
|
|
418
489
|
}));
|
|
419
490
|
return {
|
|
420
491
|
etag: normalizeText(response.ETag) || null,
|
|
421
492
|
};
|
|
422
493
|
}
|
|
423
494
|
async function generateThumbnailBuffer(localPath) {
|
|
495
|
+
const { default: sharp } = await import('sharp');
|
|
424
496
|
return sharp(localPath)
|
|
425
497
|
.resize({ width: 400, withoutEnlargement: true })
|
|
426
498
|
.webp({ quality: 50 })
|
|
427
499
|
.toBuffer();
|
|
428
500
|
}
|
|
429
|
-
|
|
501
|
+
function buildUploadedResourceEntries(columns, args, id, now) {
|
|
502
|
+
const entries = [];
|
|
503
|
+
const push = (column, value) => {
|
|
504
|
+
if (columns.has(column)) {
|
|
505
|
+
entries.push([column, value]);
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
push('id', id);
|
|
509
|
+
push('user_id', args.userId);
|
|
510
|
+
push('name', args.plan.fileName);
|
|
511
|
+
push('mime_type', args.plan.contentType);
|
|
512
|
+
push('size_bytes', args.plan.sizeBytes);
|
|
513
|
+
push('storage_config_id', args.storage.id);
|
|
514
|
+
push('storage_key', args.plan.storageKey);
|
|
515
|
+
push('storage_url', args.plan.storageUrl);
|
|
516
|
+
push('storage_provider', args.storage.provider);
|
|
517
|
+
push('bucket', args.storage.bucket);
|
|
518
|
+
push('is_public', args.storage.isPrivate ? 0 : 1);
|
|
519
|
+
push('created_at', now);
|
|
520
|
+
push('updated_at', now);
|
|
521
|
+
push('is_del', 0);
|
|
522
|
+
push('status', 'uploaded');
|
|
523
|
+
push('uploaded_at', now);
|
|
524
|
+
push('etag', args.etag);
|
|
525
|
+
push('checksum', null);
|
|
526
|
+
push('category', args.plan.category);
|
|
527
|
+
push('endpoint', args.storage.endpoint);
|
|
528
|
+
push('domain', args.plan.domain);
|
|
529
|
+
push('extra_data', '{}');
|
|
530
|
+
push('deleted_at', null);
|
|
531
|
+
push('content_sha256', null);
|
|
532
|
+
push('content', null);
|
|
533
|
+
push('content_md', null);
|
|
534
|
+
push('summary', null);
|
|
535
|
+
return entries;
|
|
536
|
+
}
|
|
537
|
+
async function insertResourceRow(db, entries) {
|
|
538
|
+
const insertColumns = entries.map(([column]) => column);
|
|
539
|
+
const placeholders = insertColumns.map(() => '?').join(', ');
|
|
540
|
+
await db.execute(`
|
|
541
|
+
INSERT INTO c_resource (${insertColumns.join(', ')})
|
|
542
|
+
VALUES (${placeholders})
|
|
543
|
+
`, entries.map(([, value]) => value));
|
|
544
|
+
}
|
|
545
|
+
async function persistUploadedResource(db, args) {
|
|
430
546
|
const columns = await getTableColumns(db, 'c_resource');
|
|
431
547
|
const requiredColumns = [
|
|
432
548
|
'id',
|
|
@@ -448,47 +564,67 @@ async function insertUploadedResource(db, args) {
|
|
|
448
564
|
}
|
|
449
565
|
const id = generateId();
|
|
450
566
|
const now = new Date().toISOString();
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
567
|
+
const entries = buildUploadedResourceEntries(columns, args, id, now);
|
|
568
|
+
if (!args.overwrite) {
|
|
569
|
+
await insertResourceRow(db, entries);
|
|
570
|
+
return id;
|
|
571
|
+
}
|
|
572
|
+
return withPowerSyncWriteTransaction(db, async (tx) => {
|
|
573
|
+
const identityClauses = [
|
|
574
|
+
'user_id = ?',
|
|
575
|
+
'storage_key = ?',
|
|
576
|
+
'COALESCE(is_del, 0) = 0',
|
|
577
|
+
];
|
|
578
|
+
const identityParams = [args.userId, args.plan.storageKey];
|
|
579
|
+
if (columns.has('storage_config_id')) {
|
|
580
|
+
identityClauses.push('storage_config_id = ?');
|
|
581
|
+
identityParams.push(args.storage.id);
|
|
455
582
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
583
|
+
else {
|
|
584
|
+
identityClauses.push('storage_provider = ?', 'bucket = ?');
|
|
585
|
+
identityParams.push(args.storage.provider, args.storage.bucket);
|
|
586
|
+
if (columns.has('endpoint')) {
|
|
587
|
+
identityClauses.push('endpoint = ?');
|
|
588
|
+
identityParams.push(args.storage.endpoint);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
const existingRows = await tx.getAll(`
|
|
592
|
+
SELECT id
|
|
593
|
+
FROM c_resource
|
|
594
|
+
WHERE ${identityClauses.join('\n AND ')}
|
|
595
|
+
ORDER BY created_at ASC, id ASC
|
|
596
|
+
`, identityParams);
|
|
597
|
+
const existingIds = [...new Set(existingRows.map((row) => normalizeText(row.id)).filter(Boolean))];
|
|
598
|
+
const existingId = existingIds[0];
|
|
599
|
+
if (!existingId) {
|
|
600
|
+
await insertResourceRow(tx, entries);
|
|
601
|
+
return id;
|
|
602
|
+
}
|
|
603
|
+
const updateEntries = entries.filter(([column]) => (column !== 'id' && column !== 'user_id' && column !== 'created_at'));
|
|
604
|
+
await tx.execute(`
|
|
605
|
+
UPDATE c_resource
|
|
606
|
+
SET ${updateEntries.map(([column]) => `${column} = ?`).join(', ')}
|
|
607
|
+
WHERE id = ?
|
|
608
|
+
AND user_id = ?
|
|
609
|
+
AND COALESCE(is_del, 0) = 0
|
|
610
|
+
`, [...updateEntries.map(([, value]) => value), existingId, args.userId]);
|
|
611
|
+
const duplicateIds = existingIds.slice(1);
|
|
612
|
+
if (duplicateIds.length > 0) {
|
|
613
|
+
const duplicateUpdates = [
|
|
614
|
+
['updated_at', now],
|
|
615
|
+
['is_del', 1],
|
|
616
|
+
...(columns.has('deleted_at') ? [['deleted_at', now]] : []),
|
|
617
|
+
];
|
|
618
|
+
await tx.execute(`
|
|
619
|
+
UPDATE c_resource
|
|
620
|
+
SET ${duplicateUpdates.map(([column]) => `${column} = ?`).join(', ')}
|
|
621
|
+
WHERE user_id = ?
|
|
622
|
+
AND COALESCE(is_del, 0) = 0
|
|
623
|
+
AND id IN (${duplicateIds.map(() => '?').join(', ')})
|
|
624
|
+
`, [...duplicateUpdates.map(([, value]) => value), args.userId, ...duplicateIds]);
|
|
625
|
+
}
|
|
626
|
+
return existingId;
|
|
627
|
+
});
|
|
492
628
|
}
|
|
493
629
|
export function parseStorageUploadCategory(value) {
|
|
494
630
|
const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
@@ -642,17 +778,50 @@ export async function testStorageUpload(db, input, deps) {
|
|
|
642
778
|
const uploadBytes = deps?.uploadBytes ?? defaultUploadBytes;
|
|
643
779
|
const deleteObject = deps?.deleteObject ?? defaultDeleteObject;
|
|
644
780
|
const payload = Buffer.from(`dinox-cli storage test\nprovider=${storage.provider}\nuser=${input.userId}\ncreated_at=${new Date().toISOString()}\n`, 'utf8');
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
781
|
+
let uploaded;
|
|
782
|
+
try {
|
|
783
|
+
uploaded = await uploadBytes({
|
|
784
|
+
body: payload,
|
|
785
|
+
bucket: storage.bucket,
|
|
786
|
+
key: plan.storageKey,
|
|
787
|
+
contentType: plan.contentType,
|
|
788
|
+
storage,
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
catch (error) {
|
|
792
|
+
if (isObjectCollisionError(error)) {
|
|
793
|
+
throw error;
|
|
794
|
+
}
|
|
795
|
+
throw createStorageUploadOutcomeUnknownError({
|
|
796
|
+
message: 'Storage test upload outcome could not be confirmed.',
|
|
797
|
+
cause: error,
|
|
798
|
+
bucket: storage.bucket,
|
|
799
|
+
storageKey: plan.storageKey,
|
|
800
|
+
possiblyAffectedKeys: [plan.storageKey],
|
|
801
|
+
overwrite: false,
|
|
802
|
+
operation: 'storage_test',
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
const cleanupEtag = normalizeText(uploaded.etag);
|
|
806
|
+
if (!cleanupEtag) {
|
|
807
|
+
throw new DinoxError('Storage test uploaded the temporary object but could not clean it up safely.', {
|
|
808
|
+
code: 'STORAGE_TEST_CLEANUP_FAILED',
|
|
809
|
+
details: {
|
|
810
|
+
storageKey: plan.storageKey,
|
|
811
|
+
bucket: storage.bucket,
|
|
812
|
+
remoteObjectChanged: true,
|
|
813
|
+
affectedKeys: [plan.storageKey],
|
|
814
|
+
cleanupErrors: [
|
|
815
|
+
`${plan.storageKey} cleanup skipped: upload response did not include an ETag required for safe conditional deletion`,
|
|
816
|
+
],
|
|
817
|
+
},
|
|
818
|
+
});
|
|
819
|
+
}
|
|
652
820
|
try {
|
|
653
821
|
await deleteObject({
|
|
654
822
|
bucket: storage.bucket,
|
|
655
823
|
key: plan.storageKey,
|
|
824
|
+
etag: cleanupEtag,
|
|
656
825
|
storage,
|
|
657
826
|
});
|
|
658
827
|
}
|
|
@@ -663,6 +832,11 @@ export async function testStorageUpload(db, input, deps) {
|
|
|
663
832
|
details: {
|
|
664
833
|
storageKey: plan.storageKey,
|
|
665
834
|
bucket: storage.bucket,
|
|
835
|
+
remoteObjectChanged: true,
|
|
836
|
+
affectedKeys: [plan.storageKey],
|
|
837
|
+
cleanupErrors: [
|
|
838
|
+
`${plan.storageKey} cleanup failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
839
|
+
],
|
|
666
840
|
},
|
|
667
841
|
});
|
|
668
842
|
}
|
|
@@ -682,46 +856,152 @@ export async function testStorageUpload(db, input, deps) {
|
|
|
682
856
|
};
|
|
683
857
|
}
|
|
684
858
|
export async function uploadFileToStorage(db, input, deps) {
|
|
859
|
+
if (input.overwrite && !normalizeText(input.key)) {
|
|
860
|
+
throw new DinoxError('--overwrite requires an explicit --key.', {
|
|
861
|
+
code: 'USAGE_ERROR',
|
|
862
|
+
});
|
|
863
|
+
}
|
|
685
864
|
const plan = await inspectStorageUpload(db, input, { statFile: deps?.statFile });
|
|
686
865
|
const configs = await listStoredCustomStorageConfigs(db, input.userId);
|
|
687
866
|
const storage = configs.find((item) => item.id === plan.storage.id);
|
|
688
867
|
if (!storage) {
|
|
689
868
|
throw new DinoxError(`Storage config not found: ${plan.storage.id}`);
|
|
690
869
|
}
|
|
870
|
+
const overwrite = input.overwrite === true;
|
|
691
871
|
const uploadObject = deps?.uploadObject ?? defaultUploadObject;
|
|
692
872
|
const uploadBytes = deps?.uploadBytes ?? defaultUploadBytes;
|
|
693
873
|
const deleteObject = deps?.deleteObject ?? defaultDeleteObject;
|
|
694
|
-
const
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
contentType: plan.contentType,
|
|
699
|
-
contentLength: plan.sizeBytes,
|
|
700
|
-
storage,
|
|
701
|
-
});
|
|
702
|
-
let thumbnailUrl = null;
|
|
703
|
-
let uploadedThumbnailKey = null;
|
|
874
|
+
const thumbnailKey = plan.contentType.startsWith('image/')
|
|
875
|
+
? generateThumbnailKey(plan.storageKey)
|
|
876
|
+
: null;
|
|
877
|
+
let uploaded;
|
|
704
878
|
try {
|
|
705
|
-
|
|
706
|
-
|
|
879
|
+
uploaded = await uploadObject({
|
|
880
|
+
localPath: plan.filePath,
|
|
881
|
+
bucket: storage.bucket,
|
|
882
|
+
key: plan.storageKey,
|
|
883
|
+
contentType: plan.contentType,
|
|
884
|
+
contentLength: plan.sizeBytes,
|
|
885
|
+
overwrite,
|
|
886
|
+
storage,
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
catch (error) {
|
|
890
|
+
if (!overwrite && isObjectCollisionError(error)) {
|
|
891
|
+
throw createStorageCollisionError(storage.bucket, [plan.storageKey]);
|
|
892
|
+
}
|
|
893
|
+
throw createStorageUploadOutcomeUnknownError({
|
|
894
|
+
message: 'Storage upload outcome could not be confirmed.',
|
|
895
|
+
cause: error,
|
|
896
|
+
bucket: storage.bucket,
|
|
897
|
+
storageKey: plan.storageKey,
|
|
898
|
+
possiblyAffectedKeys: [plan.storageKey],
|
|
899
|
+
overwrite,
|
|
900
|
+
operation: 'upload',
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
let thumbnailUrl = null;
|
|
904
|
+
const possiblyAffectedKeys = new Set();
|
|
905
|
+
const remoteWrites = [{
|
|
906
|
+
key: plan.storageKey,
|
|
907
|
+
etag: uploaded.etag,
|
|
908
|
+
cleanupAllowed: !overwrite,
|
|
909
|
+
cleaned: false,
|
|
910
|
+
}];
|
|
911
|
+
const cleanupUploadedObjects = async () => {
|
|
912
|
+
const cleanupErrors = [];
|
|
913
|
+
for (const remoteWrite of [...remoteWrites].reverse()) {
|
|
914
|
+
if (!remoteWrite.cleanupAllowed || remoteWrite.cleaned) {
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
const cleanupEtag = normalizeText(remoteWrite.etag);
|
|
918
|
+
if (!cleanupEtag) {
|
|
919
|
+
cleanupErrors.push(`${remoteWrite.key} cleanup skipped: upload response did not include an ETag required for safe conditional deletion`);
|
|
920
|
+
continue;
|
|
921
|
+
}
|
|
922
|
+
await deleteObject({
|
|
923
|
+
bucket: storage.bucket,
|
|
924
|
+
key: remoteWrite.key,
|
|
925
|
+
etag: cleanupEtag,
|
|
926
|
+
storage,
|
|
927
|
+
}).then(() => {
|
|
928
|
+
remoteWrite.cleaned = true;
|
|
929
|
+
}, (cleanupError) => {
|
|
930
|
+
cleanupErrors.push(`${remoteWrite.key} cleanup failed: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
return cleanupErrors;
|
|
934
|
+
};
|
|
935
|
+
const buildRemoteFailureDetails = (cleanupErrors) => {
|
|
936
|
+
const affectedKeys = remoteWrites
|
|
937
|
+
.filter((remoteWrite) => !remoteWrite.cleaned)
|
|
938
|
+
.map((remoteWrite) => remoteWrite.key);
|
|
939
|
+
const possibleKeys = [...possiblyAffectedKeys];
|
|
940
|
+
return {
|
|
941
|
+
bucket: storage.bucket,
|
|
942
|
+
storageKey: plan.storageKey,
|
|
943
|
+
remoteObjectChanged: possibleKeys.length > 0
|
|
944
|
+
? (affectedKeys.length > 0 ? true : null)
|
|
945
|
+
: affectedKeys.length > 0,
|
|
946
|
+
affectedKeys,
|
|
947
|
+
...(possibleKeys.length > 0
|
|
948
|
+
? { remoteOutcome: 'unknown', possiblyAffectedKeys: possibleKeys }
|
|
949
|
+
: {}),
|
|
950
|
+
...(cleanupErrors.length > 0 ? { cleanupErrors } : {}),
|
|
951
|
+
};
|
|
952
|
+
};
|
|
953
|
+
if (plan.contentType.startsWith('image/')) {
|
|
954
|
+
try {
|
|
955
|
+
if (!thumbnailKey) {
|
|
956
|
+
throw new Error('Thumbnail key could not be generated.');
|
|
957
|
+
}
|
|
707
958
|
const thumbnailBuffer = await generateThumbnailBuffer(plan.filePath);
|
|
708
959
|
const thumbnailTarget = buildStorageTarget(storage, thumbnailKey);
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
960
|
+
let uploadedThumbnail;
|
|
961
|
+
try {
|
|
962
|
+
uploadedThumbnail = await uploadBytes({
|
|
963
|
+
body: thumbnailBuffer,
|
|
964
|
+
bucket: storage.bucket,
|
|
965
|
+
key: thumbnailKey,
|
|
966
|
+
contentType: 'image/webp',
|
|
967
|
+
overwrite,
|
|
968
|
+
storage,
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
catch (error) {
|
|
972
|
+
if (!isObjectCollisionError(error)) {
|
|
973
|
+
possiblyAffectedKeys.add(thumbnailKey);
|
|
974
|
+
}
|
|
975
|
+
throw error;
|
|
976
|
+
}
|
|
977
|
+
remoteWrites.push({
|
|
712
978
|
key: thumbnailKey,
|
|
713
|
-
|
|
714
|
-
|
|
979
|
+
etag: uploadedThumbnail.etag,
|
|
980
|
+
cleanupAllowed: !overwrite,
|
|
981
|
+
cleaned: false,
|
|
715
982
|
});
|
|
716
|
-
uploadedThumbnailKey = thumbnailKey;
|
|
717
983
|
thumbnailUrl = thumbnailTarget.storageUrl;
|
|
718
984
|
}
|
|
719
|
-
|
|
985
|
+
catch (error) {
|
|
986
|
+
const cleanupErrors = await cleanupUploadedObjects();
|
|
987
|
+
if (!overwrite && isObjectCollisionError(error) && thumbnailKey) {
|
|
988
|
+
throw createStorageCollisionError(storage.bucket, [thumbnailKey], buildRemoteFailureDetails(cleanupErrors));
|
|
989
|
+
}
|
|
990
|
+
throw new DinoxError('Storage upload failed while generating the required thumbnail.', {
|
|
991
|
+
cause: error,
|
|
992
|
+
code: 'STORAGE_THUMBNAIL_UPLOAD_FAILED',
|
|
993
|
+
details: buildRemoteFailureDetails(cleanupErrors),
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
try {
|
|
998
|
+
const resourceId = await persistUploadedResource(db, {
|
|
720
999
|
userId: input.userId,
|
|
721
1000
|
storage,
|
|
722
1001
|
plan,
|
|
723
1002
|
etag: uploaded.etag,
|
|
724
1003
|
thumbnailUrl,
|
|
1004
|
+
overwrite,
|
|
725
1005
|
});
|
|
726
1006
|
return {
|
|
727
1007
|
resourceId,
|
|
@@ -744,27 +1024,11 @@ export async function uploadFileToStorage(db, input, deps) {
|
|
|
744
1024
|
};
|
|
745
1025
|
}
|
|
746
1026
|
catch (error) {
|
|
747
|
-
const cleanupErrors =
|
|
748
|
-
|
|
749
|
-
await deleteObject({
|
|
750
|
-
bucket: storage.bucket,
|
|
751
|
-
key: uploadedThumbnailKey,
|
|
752
|
-
storage,
|
|
753
|
-
}).catch((cleanupError) => {
|
|
754
|
-
cleanupErrors.push(`thumbnail cleanup failed: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
|
|
755
|
-
});
|
|
756
|
-
}
|
|
757
|
-
await deleteObject({
|
|
758
|
-
bucket: storage.bucket,
|
|
759
|
-
key: plan.storageKey,
|
|
760
|
-
storage,
|
|
761
|
-
}).catch((cleanupError) => {
|
|
762
|
-
cleanupErrors.push(`main object cleanup failed: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
|
|
763
|
-
});
|
|
764
|
-
throw new DinoxError('Storage upload failed while generating the required thumbnail.', {
|
|
1027
|
+
const cleanupErrors = await cleanupUploadedObjects();
|
|
1028
|
+
throw new DinoxError('Storage upload failed while recording resource metadata.', {
|
|
765
1029
|
cause: error,
|
|
766
|
-
code: '
|
|
767
|
-
|
|
1030
|
+
code: 'STORAGE_RESOURCE_INSERT_FAILED',
|
|
1031
|
+
details: buildRemoteFailureDetails(cleanupErrors),
|
|
768
1032
|
});
|
|
769
1033
|
}
|
|
770
1034
|
}
|
package/dist/commands/sync.d.ts
CHANGED
|
@@ -1,4 +1,26 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import
|
|
2
|
+
import { loadConfig } from '../config/store.js';
|
|
3
|
+
import { resolveConfig } from '../config/resolve.js';
|
|
4
|
+
import { hydrateResolvedAuthIdentity } from '../auth/userInfo.js';
|
|
5
|
+
import { withExclusivePowerSync } from '../powersync/exclusiveRuntime.js';
|
|
6
|
+
import type { CommandDescriptor, CommandExecutionContext } from './descriptor/types.js';
|
|
7
|
+
import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess } from '../utils/output.js';
|
|
8
|
+
import { getStatusSnapshot, waitForLastSyncedAfter } from '../powersync/runtime.js';
|
|
9
|
+
import { runSingleSyncPass } from '../powersync/syncPass.js';
|
|
10
|
+
type SyncCommandDependencies = {
|
|
11
|
+
loadConfig: typeof loadConfig;
|
|
12
|
+
resolveConfig: typeof resolveConfig;
|
|
13
|
+
hydrateResolvedAuthIdentity: typeof hydrateResolvedAuthIdentity;
|
|
14
|
+
withExclusivePowerSync: typeof withExclusivePowerSync;
|
|
15
|
+
getRequestedStructuredOutputFormat: typeof getRequestedStructuredOutputFormat;
|
|
16
|
+
getStatusSnapshot: typeof getStatusSnapshot;
|
|
17
|
+
waitForLastSyncedAfter: typeof waitForLastSyncedAfter;
|
|
18
|
+
runSingleSyncPass: typeof runSingleSyncPass;
|
|
19
|
+
printHumanReadableLine: typeof printHumanReadableLine;
|
|
20
|
+
printSuccess: typeof printSuccess;
|
|
21
|
+
now: () => number;
|
|
22
|
+
};
|
|
23
|
+
export declare function executeSyncCommand(context: CommandExecutionContext, overrides?: Partial<SyncCommandDependencies>): Promise<void>;
|
|
3
24
|
export declare const syncCommandDescriptor: CommandDescriptor;
|
|
4
25
|
export declare function registerSyncCommand(program: Command): void;
|
|
26
|
+
export {};
|