connectbase-client 1.7.0 → 1.8.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 +26 -0
- package/dist/cli.js +149 -36
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,32 @@
|
|
|
3
3
|
본 SDK 의 모든 주요 변경사항을 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/) 형식으로 기록합니다.
|
|
4
4
|
버전은 [Semantic Versioning](https://semver.org/lang/ko/) 을 따릅니다.
|
|
5
5
|
|
|
6
|
+
## [1.8.0] - 2026-04-19
|
|
7
|
+
|
|
8
|
+
웹 스토리지 CLI 배포(`connectbase deploy ./dist`) 를 manifest 기반 **증분 업로드** 로 재구성. 동일 dist 재배포는 거의 즉시 완료되고, 일부만 바뀐 경우 변경분만 전송한다. 서버 DB 쓰기도 단일 트랜잭션 + bulk insert 로 묶어 전체 배포 시간이 짧아짐.
|
|
9
|
+
|
|
10
|
+
### Added — 증분 배포
|
|
11
|
+
|
|
12
|
+
- **`GET /v1/public/storages/webs/:storageID/deploy/manifest`** — 현재 배포된 파일의 `path/size/hash(sha256)` 와 결정적 `revision` 반환.
|
|
13
|
+
- **`POST /v1/public/storages/webs/:storageID/deploy/incremental`** — `{ upsert, delete, base_revision? }` 변경분만 적용. 서버가 hash 를 재검증하고 단일 트랜잭션으로 upsert/delete 처리 후 기존 배포 파이프라인으로 Object Storage 에 전체 업로드.
|
|
14
|
+
- CLI 는 prod 배포 시 자동으로 manifest → diff → incremental 순서를 사용한다. 변경 없음이면 `✓ 변경사항 없음` 출력 후 종료.
|
|
15
|
+
- `base_revision` 은 옵션. 동시 배포 경합에서 `409 Conflict` 면 manifest 재조회 후 1회 자동 재시도.
|
|
16
|
+
|
|
17
|
+
### Changed — 서버 배포 속도 개선 (기존 `/deploy` 포함)
|
|
18
|
+
|
|
19
|
+
- `CLIDeploy` 가 파일/폴더별 개별 트랜잭션을 단일 트랜잭션 + depth-batch `CreateBulk` 로 교체. 수십~수백 개의 `BEGIN/COMMIT` 왕복이 1회로 축소.
|
|
20
|
+
- 실패 시 기존 파일도 함께 롤백되어 **실패한 배포가 스토리지를 비우는 회귀가 사라짐** (과거엔 DeleteAll 이 선행되고 이후 SaveFile 이 실패하면 스토리지가 빈 상태로 남았음).
|
|
21
|
+
|
|
22
|
+
### Compatibility
|
|
23
|
+
|
|
24
|
+
- **구버전 서버**: manifest 엔드포인트 404 → 기존 `/deploy` 전량 업로드로 자동 폴백.
|
|
25
|
+
- **구버전 SDK** (`1.7.x` 이하): 기존 `/deploy` 는 그대로 동작 (내부 로직만 최적화됨).
|
|
26
|
+
- **Dev 배포** (`connectbase deploy --dev`) 는 Object Storage 에 직접 업로드하는 구조상 증분 적용 불가 — 항상 전량 업로드 유지.
|
|
27
|
+
|
|
28
|
+
### Internal — DB 스키마
|
|
29
|
+
|
|
30
|
+
- `storage_web_file.content_hash` 컬럼 추가 (Optional, Default `""`). 기존 row 는 manifest 최초 조회 시 lazy backfill.
|
|
31
|
+
|
|
6
32
|
## [1.7.0] - 2026-04-19
|
|
7
33
|
|
|
8
34
|
콘솔(JWT) DB 관리 API 경로 정정 — 기존 메서드들이 실제로 존재하지 않는 라우트(`/v1/apps/:appID/tables/:tableID/...`, `/v1/apps/:appID/triggers`, `/v1/apps/:appID/security/rules`, `/v1/apps/:appID/tables/:tableID/relations`) 를 호출해 서버가 항상 404 를 반환하던 문제를 수정.
|
package/dist/cli.js
CHANGED
|
@@ -6,6 +6,10 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
6
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
7
|
var __getProtoOf = Object.getPrototypeOf;
|
|
8
8
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
9
13
|
var __copyProps = (to, from, except, desc) => {
|
|
10
14
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
15
|
for (let key of __getOwnPropNames(from))
|
|
@@ -22,8 +26,16 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
22
26
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
27
|
mod
|
|
24
28
|
));
|
|
29
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
25
30
|
|
|
26
31
|
// src/cli.ts
|
|
32
|
+
var cli_exports = {};
|
|
33
|
+
__export(cli_exports, {
|
|
34
|
+
computeDeployDiff: () => computeDeployDiff,
|
|
35
|
+
normalizeRelativePath: () => normalizeRelativePath,
|
|
36
|
+
sha256Hex: () => sha256Hex
|
|
37
|
+
});
|
|
38
|
+
module.exports = __toCommonJS(cli_exports);
|
|
27
39
|
var fs2 = __toESM(require("fs"));
|
|
28
40
|
var path2 = __toESM(require("path"));
|
|
29
41
|
var crypto = __toESM(require("crypto"));
|
|
@@ -200,6 +212,14 @@ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
200
212
|
".zip",
|
|
201
213
|
".wasm"
|
|
202
214
|
]);
|
|
215
|
+
function sha256Hex(value) {
|
|
216
|
+
return crypto.createHash("sha256").update(value, "utf8").digest("hex");
|
|
217
|
+
}
|
|
218
|
+
function normalizeRelativePath(relativePath) {
|
|
219
|
+
let p = relativePath.replace(/\\/g, "/");
|
|
220
|
+
if (!p.startsWith("/")) p = `/${p}`;
|
|
221
|
+
return p;
|
|
222
|
+
}
|
|
203
223
|
function loadConfig() {
|
|
204
224
|
const config = {
|
|
205
225
|
publicKey: process.env.CONNECTBASE_PUBLIC_KEY,
|
|
@@ -247,11 +267,12 @@ function collectFiles(dir, baseDir = dir) {
|
|
|
247
267
|
} else {
|
|
248
268
|
content = fs2.readFileSync(fullPath, "utf-8");
|
|
249
269
|
}
|
|
270
|
+
const normalized = normalizeRelativePath(relativePath);
|
|
250
271
|
files.push({
|
|
251
|
-
path:
|
|
252
|
-
// Windows 경로 변환
|
|
272
|
+
path: normalized,
|
|
253
273
|
content,
|
|
254
|
-
isBinary
|
|
274
|
+
isBinary,
|
|
275
|
+
hash: sha256Hex(content)
|
|
255
276
|
});
|
|
256
277
|
}
|
|
257
278
|
}
|
|
@@ -337,47 +358,133 @@ async function deploy(directory, config, isDev = false) {
|
|
|
337
358
|
const totalSize = files.reduce((sum, f) => sum + f.content.length, 0);
|
|
338
359
|
const sizeKB = (totalSize / 1024).toFixed(1);
|
|
339
360
|
log(`${colors.green}\u2713${colors.reset} ${files.length}\uAC1C \uD30C\uC77C \uBC1C\uACAC (${sizeKB} KB)`);
|
|
340
|
-
const deployPath = isDev ? "deploy/dev" : "deploy";
|
|
341
|
-
const url = `${config.baseUrl}/v1/public/storages/webs/${config.storageId}/${deployPath}`;
|
|
342
361
|
const envLabel = isDev ? "Dev" : "Prod";
|
|
343
|
-
|
|
362
|
+
const baseStorageUrl = `${config.baseUrl}/v1/public/storages/webs/${config.storageId}`;
|
|
363
|
+
const headers = { "X-Public-Key": config.publicKey };
|
|
364
|
+
if (isDev) {
|
|
365
|
+
await fullDeploy(baseStorageUrl, headers, files, envLabel, "deploy/dev");
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
344
368
|
try {
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
"
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
);
|
|
369
|
+
const manifest = await tryFetchManifest(baseStorageUrl, headers);
|
|
370
|
+
if (!manifest) {
|
|
371
|
+
await fullDeploy(baseStorageUrl, headers, files, envLabel, "deploy");
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const diff = computeDeployDiff(files, manifest);
|
|
375
|
+
if (diff.upsert.length === 0 && diff.delete.length === 0) {
|
|
376
|
+
success(`\uBCC0\uACBD\uC0AC\uD56D \uC5C6\uC74C (${files.length}\uAC1C \uD30C\uC77C \uC77C\uCE58)`);
|
|
377
|
+
log(`
|
|
378
|
+
${colors.cyan}URL: https://${config.storageId}.web.connectbase.world${colors.reset}
|
|
379
|
+
`);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
info(`\uBCC0\uACBD: ${colors.green}+${diff.upsert.length}${colors.reset} / ${colors.red}-${diff.delete.length}${colors.reset} (\uC804\uCCB4 ${files.length}\uAC1C \uC911)`);
|
|
383
|
+
const uploadSize = diff.upsert.reduce((s, f) => s + f.content.length, 0);
|
|
384
|
+
info(`\uC5C5\uB85C\uB4DC \uD06C\uAE30: ${(uploadSize / 1024).toFixed(1)} KB`);
|
|
385
|
+
await incrementalDeploy(baseStorageUrl, headers, diff, manifest.revision, envLabel);
|
|
386
|
+
} catch (err) {
|
|
359
387
|
process.stdout.write("\r \r");
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
388
|
+
error(`\uB124\uD2B8\uC6CC\uD06C \uC624\uB958: ${err instanceof Error ? err.message : err}`);
|
|
389
|
+
process.exit(1);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
async function tryFetchManifest(baseStorageUrl, headers) {
|
|
393
|
+
const res = await makeRequest(`${baseStorageUrl}/deploy/manifest`, "GET", headers);
|
|
394
|
+
if (res.status === 404) return null;
|
|
395
|
+
if (res.status < 200 || res.status >= 300) {
|
|
396
|
+
const data = res.data;
|
|
397
|
+
const msg = typeof data === "object" && data !== null ? data.message || data.error || JSON.stringify(data) : `HTTP ${res.status}`;
|
|
398
|
+
throw new Error(`manifest \uC870\uD68C \uC2E4\uD328: ${msg}`);
|
|
399
|
+
}
|
|
400
|
+
const parsed = res.data;
|
|
401
|
+
if (!parsed || !Array.isArray(parsed.files)) {
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
return parsed;
|
|
405
|
+
}
|
|
406
|
+
function computeDeployDiff(local, manifest) {
|
|
407
|
+
const localByPath = /* @__PURE__ */ new Map();
|
|
408
|
+
for (const f of local) localByPath.set(f.path, f);
|
|
409
|
+
const remoteByPath = /* @__PURE__ */ new Map();
|
|
410
|
+
for (const m of manifest.files) remoteByPath.set(m.path, m);
|
|
411
|
+
const upsert = [];
|
|
412
|
+
for (const f of local) {
|
|
413
|
+
const remote = remoteByPath.get(f.path);
|
|
414
|
+
if (!remote || remote.hash !== f.hash || !remote.hash) {
|
|
415
|
+
upsert.push({
|
|
416
|
+
path: f.path,
|
|
417
|
+
content: f.content,
|
|
418
|
+
is_binary: f.isBinary,
|
|
419
|
+
hash: f.hash
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
const toDelete = [];
|
|
424
|
+
for (const m of manifest.files) {
|
|
425
|
+
if (!localByPath.has(m.path)) toDelete.push(m.path);
|
|
426
|
+
}
|
|
427
|
+
return { upsert, delete: toDelete };
|
|
428
|
+
}
|
|
429
|
+
async function incrementalDeploy(baseStorageUrl, headers, diff, baseRevision, envLabel) {
|
|
430
|
+
process.stdout.write(`${colors.blue}\u27F3${colors.reset} ${envLabel} \uC99D\uBD84 \uBC30\uD3EC \uC911...`);
|
|
431
|
+
const body = JSON.stringify({
|
|
432
|
+
upsert: diff.upsert,
|
|
433
|
+
delete: diff.delete,
|
|
434
|
+
base_revision: baseRevision
|
|
435
|
+
});
|
|
436
|
+
const res = await makeRequest(`${baseStorageUrl}/deploy/incremental`, "POST", headers, body);
|
|
437
|
+
process.stdout.write("\r \r");
|
|
438
|
+
if (res.status === 409) {
|
|
439
|
+
warn("\uC11C\uBC84 revision \uC774 \uBCC0\uACBD\uB418\uC5C8\uC2B5\uB2C8\uB2E4. manifest \uC7AC\uC870\uD68C \uD6C4 \uC7AC\uC2DC\uB3C4\uD569\uB2C8\uB2E4.");
|
|
440
|
+
const manifest = await tryFetchManifest(baseStorageUrl, headers);
|
|
441
|
+
if (!manifest) {
|
|
442
|
+
error("\uC7AC\uC2DC\uB3C4 manifest \uC870\uD68C \uC2E4\uD328");
|
|
443
|
+
process.exit(1);
|
|
444
|
+
}
|
|
445
|
+
const body2 = JSON.stringify({
|
|
446
|
+
upsert: diff.upsert,
|
|
447
|
+
delete: diff.delete,
|
|
448
|
+
base_revision: manifest.revision
|
|
449
|
+
});
|
|
450
|
+
process.stdout.write(`${colors.blue}\u27F3${colors.reset} ${envLabel} \uC99D\uBD84 \uBC30\uD3EC \uC7AC\uC2DC\uB3C4...`);
|
|
451
|
+
const res2 = await makeRequest(`${baseStorageUrl}/deploy/incremental`, "POST", headers, body2);
|
|
452
|
+
process.stdout.write("\r \r");
|
|
453
|
+
handleDeployResponse(res2, envLabel);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
handleDeployResponse(res, envLabel);
|
|
457
|
+
}
|
|
458
|
+
async function fullDeploy(baseStorageUrl, headers, files, envLabel, endpoint) {
|
|
459
|
+
process.stdout.write(`${colors.blue}\u27F3${colors.reset} ${envLabel} \uBC30\uD3EC \uC911...`);
|
|
460
|
+
const body = JSON.stringify({
|
|
461
|
+
files: files.map((f) => ({
|
|
462
|
+
path: f.path,
|
|
463
|
+
content: f.content,
|
|
464
|
+
is_binary: f.isBinary
|
|
465
|
+
}))
|
|
466
|
+
});
|
|
467
|
+
const res = await makeRequest(`${baseStorageUrl}/${endpoint}`, "POST", headers, body);
|
|
468
|
+
process.stdout.write("\r \r");
|
|
469
|
+
handleDeployResponse(res, envLabel);
|
|
470
|
+
}
|
|
471
|
+
function handleDeployResponse(response, envLabel) {
|
|
472
|
+
if (response.status >= 200 && response.status < 300) {
|
|
473
|
+
success(`${envLabel} \uBC30\uD3EC \uC644\uB8CC!`);
|
|
474
|
+
const data = response.data;
|
|
475
|
+
if (data?.dev_url) {
|
|
476
|
+
log(`
|
|
365
477
|
${colors.yellow}Dev URL: ${data.dev_url}${colors.reset}
|
|
366
478
|
`);
|
|
367
|
-
|
|
368
|
-
|
|
479
|
+
} else if (data?.url) {
|
|
480
|
+
log(`
|
|
369
481
|
${colors.cyan}URL: ${data.url}${colors.reset}
|
|
370
482
|
`);
|
|
371
|
-
}
|
|
372
|
-
} else {
|
|
373
|
-
const data = response.data;
|
|
374
|
-
const errorMsg = typeof data === "object" && data !== null ? data.message || data.error || JSON.stringify(data) : typeof data === "string" ? data : `HTTP ${response.status}`;
|
|
375
|
-
error(`\uBC30\uD3EC \uC2E4\uD328: ${errorMsg}`);
|
|
376
|
-
process.exit(1);
|
|
377
483
|
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
error(
|
|
484
|
+
} else {
|
|
485
|
+
const data = response.data;
|
|
486
|
+
const errorMsg = typeof data === "object" && data !== null ? data.message || data.error || JSON.stringify(data) : typeof data === "string" ? data : `HTTP ${response.status}`;
|
|
487
|
+
error(`\uBC30\uD3EC \uC2E4\uD328: ${errorMsg}`);
|
|
381
488
|
process.exit(1);
|
|
382
489
|
}
|
|
383
490
|
}
|
|
@@ -2117,3 +2224,9 @@ main().catch((err) => {
|
|
|
2117
2224
|
error(err.message);
|
|
2118
2225
|
process.exit(1);
|
|
2119
2226
|
});
|
|
2227
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2228
|
+
0 && (module.exports = {
|
|
2229
|
+
computeDeployDiff,
|
|
2230
|
+
normalizeRelativePath,
|
|
2231
|
+
sha256Hex
|
|
2232
|
+
});
|