@tailor-platform/sdk 1.56.1 → 1.57.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 +12 -0
- package/README.md +0 -23
- package/dist/{application-DuT_ae02.mjs → application-CdkoGX27.mjs} +7 -4
- package/dist/application-CdkoGX27.mjs.map +1 -0
- package/dist/application-x_mURdR0.mjs +4 -0
- package/dist/cli/erd-viewer-assets/app.js +1181 -0
- package/dist/cli/erd-viewer-assets/index.html +73 -0
- package/dist/cli/erd-viewer-assets/serve.json +13 -0
- package/dist/cli/erd-viewer-assets/styles.css +789 -0
- package/dist/cli/index.mjs +685 -344
- package/dist/cli/index.mjs.map +1 -1
- package/dist/cli/lib.d.mts +6 -1
- package/dist/cli/lib.mjs +2 -2
- package/dist/client-DLPEPJ_s.mjs.map +1 -1
- package/dist/{runtime-745lvg7i.mjs → runtime-1YuaoNr8.mjs} +36 -63
- package/dist/runtime-1YuaoNr8.mjs.map +1 -0
- package/docs/cli/tailordb.md +31 -26
- package/docs/cli-reference.md +2 -2
- package/package.json +1 -3
- package/dist/application-CC3oaSay.mjs +0 -4
- package/dist/application-DuT_ae02.mjs.map +0 -1
- package/dist/runtime-745lvg7i.mjs.map +0 -1
package/dist/cli/index.mjs
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import { G as PATScope, R as AuthInvokerSchema, c as fetchUserInfo, d as initOperatorClient, h as userAgent, i as fetchAll, j as FunctionExecution_Type, n as closeConnectionPool, o as fetchPaged, s as fetchPlatformMachineUserToken, u as initOAuth2Client } from "../client-DLPEPJ_s.mjs";
|
|
4
4
|
import { n as logger, r as styles } from "../logger-DpJyJvNz.mjs";
|
|
5
|
-
import { $ as listCommand$10, An as
|
|
6
|
-
import { A as
|
|
5
|
+
import { $ as listCommand$10, An as toPageDirection, At as startCommand, B as logBetaWarning, C as listCommand$13, Cn as commonArgs, Dn as isVerbose, Dt as jobsCommand, E as resumeCommand, En as deploymentArgs, F as writeDbTypesFile, Gt as parseMigrationLabelNumber, H as removeCommand$1, Ht as executeScript, I as getConfiguredEditorCommand, K as treeCommand, L as openInConfiguredEditor, Lt as functionExecutionStatusToString, Mt as getCommand$6, N as generateCommand$1, O as listCommand$12, On as pagedLogArgs, P as generateMigrationScript, Pt as executionsCommand, Rt as formatKeyValueTable, Sn as defineAppCommand, St as triggerCommand, T as healthCommand, Tn as confirmationArgs, U as updateCommand$3, Vt as deploy, Y as getCommand$5, Yt as INITIAL_SCHEMA_NUMBER, Z as updateCommand$2, _n as generateUserTypes, at as createCommand$3, b as createCommand$4, c as listCommand$14, cn as reconstructSnapshotFromMigrations, f as restoreCommand, ft as tokenCommand, g as getCommand$7, gn as PluginManager, gt as listCommand$7, hn as trnPrefix, ht as generate, i as updateCommand$4, in as getMigrationFiles, j as truncateCommand, jn as workspaceArgs, kn as paginationArgs, ln as formatMigrationNumber, lt as getCommand$3, m as listCommand$15, mn as sdkNameLabelKey, o as removeCommand, on as isValidMigrationNumber, pn as getNamespacesWithMigrations, pt as listCommand$8, q as listCommand$11, r as queryCommand, rn as getMigrationFilePath, rt as deleteCommand$3, sn as loadDiff, st as listCommand$9, t as isNativeTypeScriptRuntime, tt as getCommand$4, u as inviteCommand, v as deleteCommand$4, vn as prompt, vt as getCommand$2, wn as configArg, wt as listCommand$6, xn as assertWritable, xt as webhookCommand, yn as apiCommand, z as showCommand, zt as getCommand$1 } from "../runtime-1YuaoNr8.mjs";
|
|
6
|
+
import { A as resolveTokens, C as loadConfig, E as loadAccessToken, M as writePlatformConfig, O as loadWorkspaceId, T as fetchLatestToken, _ as createLogLevelTreeshakeOptions, a as WorkflowJobSchema, b as getDistDir, g as composeFunctionTreeshakeOptions, i as resolveInlineSourcemap, j as saveUserTokens, k as readPlatformConfig, l as ExecutorSchema, o as ResolverSchema, t as defineApplication, u as INVOKER_EXPR, v as resolveBundleLogLevel, w as deleteUserTokens, x as hashContent } from "../application-CdkoGX27.mjs";
|
|
7
7
|
import { t as multiline } from "../multiline-Cf9ODpr1.mjs";
|
|
8
|
+
import { r as isPluginGeneratedType } from "../seed-C0fE2sJB.mjs";
|
|
8
9
|
import { t as readPackageJson } from "../package-json-DcQApfPQ.mjs";
|
|
9
10
|
import { n as isCLIError } from "../errors-EsY4XO6O.mjs";
|
|
10
11
|
import { a as JSON_FOOTER_MARKER, i as CRASH_LOG_EXTENSION, o as parseCrashReportConfig, r as sendCrashReport, t as initCrashReporting } from "../crashreport-Bm2mN5tg.mjs";
|
|
11
|
-
import { createRequire } from "node:module";
|
|
12
12
|
import { arg, defineCommand, runCommand, runMain } from "politty";
|
|
13
13
|
import { withCompletionCommand } from "politty/completion";
|
|
14
14
|
import { z } from "zod";
|
|
@@ -17,7 +17,7 @@ import * as fs$1 from "node:fs";
|
|
|
17
17
|
import { timestampDate } from "@bufbuild/protobuf/wkt";
|
|
18
18
|
import * as path from "pathe";
|
|
19
19
|
import { dirname, resolve } from "pathe";
|
|
20
|
-
import { pathToFileURL } from "node:url";
|
|
20
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
21
21
|
import { generateCodeVerifier } from "@badgateway/oauth2-client";
|
|
22
22
|
import { Code, ConnectError } from "@connectrpc/connect";
|
|
23
23
|
import { resolvePackageJSON, resolveTSConfig } from "pkg-types";
|
|
@@ -26,9 +26,11 @@ import * as http from "node:http";
|
|
|
26
26
|
import open from "open";
|
|
27
27
|
import * as rolldown from "rolldown";
|
|
28
28
|
import * as fsPromises from "node:fs/promises";
|
|
29
|
+
import { glob } from "node:fs/promises";
|
|
29
30
|
import pLimit from "p-limit";
|
|
30
31
|
import { TraceMap, generatedPositionFor, originalPositionFor } from "@jridgewell/trace-mapping";
|
|
31
32
|
import { spawn, spawnSync } from "node:child_process";
|
|
33
|
+
import { watch } from "chokidar";
|
|
32
34
|
import * as fs from "fs";
|
|
33
35
|
import { lookup } from "mime-types";
|
|
34
36
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
@@ -3105,378 +3107,430 @@ const staticwebsiteCommand = defineCommand({
|
|
|
3105
3107
|
});
|
|
3106
3108
|
|
|
3107
3109
|
//#endregion
|
|
3108
|
-
//#region src/cli/
|
|
3110
|
+
//#region src/cli/commands/tailordb/erd/local-schema.ts
|
|
3109
3111
|
/**
|
|
3110
|
-
* Resolve
|
|
3111
|
-
* @param
|
|
3112
|
-
* @
|
|
3112
|
+
* Resolve TailorDB namespaces that need local type loading for ERD generation.
|
|
3113
|
+
* @param config - Loaded Tailor config.
|
|
3114
|
+
* @param options - Namespace selection options.
|
|
3115
|
+
* @returns Namespace names to load, or undefined to load all owned namespaces.
|
|
3113
3116
|
*/
|
|
3114
|
-
function
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3117
|
+
function resolveLocalErdSchemaNamespaces(config, options) {
|
|
3118
|
+
if (options.namespaces) return options.namespaces;
|
|
3119
|
+
if (!options.requireErdSite) return;
|
|
3120
|
+
return Object.entries(config.db ?? {}).flatMap(([namespace, dbConfig]) => "external" in dbConfig || !dbConfig.erdSite ? [] : [namespace]);
|
|
3121
|
+
}
|
|
3122
|
+
/**
|
|
3123
|
+
* Load local TailorDB namespaces exactly as SDK generation/deploy sees them.
|
|
3124
|
+
* @param options - Local schema loading options.
|
|
3125
|
+
* @returns Loaded TailorDB namespace data.
|
|
3126
|
+
*/
|
|
3127
|
+
async function loadLocalErdSchema(options) {
|
|
3128
|
+
const { config, plugins } = await loadConfig(options.configPath);
|
|
3129
|
+
await generateUserTypes({
|
|
3130
|
+
config,
|
|
3131
|
+
configPath: config.path
|
|
3132
|
+
});
|
|
3133
|
+
const application = defineApplication({
|
|
3134
|
+
config,
|
|
3135
|
+
pluginManager: plugins.length > 0 ? new PluginManager(plugins) : void 0
|
|
3136
|
+
});
|
|
3137
|
+
const namespaceNames = resolveLocalErdSchemaNamespaces(config, {
|
|
3138
|
+
namespaces: options.namespaces,
|
|
3139
|
+
requireErdSite: options.requireErdSite
|
|
3140
|
+
});
|
|
3141
|
+
const namespaceFilter = namespaceNames ? new Set(namespaceNames) : void 0;
|
|
3142
|
+
const services = namespaceFilter ? application.tailorDBServices.filter((db) => namespaceFilter.has(db.namespace)) : application.tailorDBServices;
|
|
3143
|
+
if (namespaceFilter && services.length !== namespaceFilter.size) {
|
|
3144
|
+
const available = application.tailorDBServices.map((db) => db.namespace).join(", ");
|
|
3145
|
+
const requested = [...namespaceFilter].join(", ");
|
|
3146
|
+
throw new Error(`TailorDB namespace "${requested}" not found in local config.db.` + (available ? ` Available owned namespaces: ${available}` : ""));
|
|
3122
3147
|
}
|
|
3123
|
-
const
|
|
3124
|
-
|
|
3125
|
-
|
|
3148
|
+
const namespaces = [];
|
|
3149
|
+
for (const db of services) {
|
|
3150
|
+
await db.loadTypes();
|
|
3151
|
+
await db.processNamespacePlugins();
|
|
3152
|
+
namespaces.push({
|
|
3153
|
+
namespace: db.namespace,
|
|
3154
|
+
types: { ...db.types },
|
|
3155
|
+
sourceInfo: new Map(Object.entries(db.typeSourceInfo)),
|
|
3156
|
+
pluginAttachments: db.pluginAttachments
|
|
3157
|
+
});
|
|
3158
|
+
}
|
|
3159
|
+
return {
|
|
3160
|
+
config,
|
|
3161
|
+
namespaces
|
|
3162
|
+
};
|
|
3126
3163
|
}
|
|
3127
3164
|
|
|
3128
3165
|
//#endregion
|
|
3129
3166
|
//#region src/cli/commands/tailordb/erd/schema.ts
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3167
|
+
const CLEAN_ROOM_NOTES = [
|
|
3168
|
+
"Generated by a TailorDB-specific viewer implementation.",
|
|
3169
|
+
"The implementation is based on TailorDB schema contracts, public Liam documentation, Liam CLI help, and black-box generated-output observation.",
|
|
3170
|
+
"It does not copy Liam source code, generated JavaScript/CSS, parser internals, or layout internals."
|
|
3171
|
+
];
|
|
3172
|
+
function buildRevision(schema) {
|
|
3173
|
+
return hashContent(JSON.stringify(schema)).slice(0, 16);
|
|
3174
|
+
}
|
|
3175
|
+
function toTypeSource(source) {
|
|
3176
|
+
if (!source) return void 0;
|
|
3177
|
+
if (isPluginGeneratedType(source)) return {
|
|
3178
|
+
kind: "plugin",
|
|
3179
|
+
exportName: source.exportName,
|
|
3180
|
+
pluginId: source.pluginId,
|
|
3181
|
+
pluginImportPath: source.pluginImportPath,
|
|
3182
|
+
originalExportName: source.originalExportName,
|
|
3183
|
+
generatedTypeKind: source.generatedTypeKind,
|
|
3184
|
+
namespace: source.namespace
|
|
3185
|
+
};
|
|
3186
|
+
return {
|
|
3187
|
+
kind: "user",
|
|
3188
|
+
exportName: source.exportName
|
|
3189
|
+
};
|
|
3190
|
+
}
|
|
3191
|
+
function toRelationships(relationships) {
|
|
3192
|
+
return Object.entries(relationships).sort(([a], [b]) => a.localeCompare(b)).map(([name, relationship]) => ({
|
|
3193
|
+
name,
|
|
3194
|
+
targetType: relationship.targetType,
|
|
3195
|
+
targetField: relationship.targetField,
|
|
3196
|
+
sourceField: relationship.sourceField,
|
|
3197
|
+
isArray: relationship.isArray,
|
|
3198
|
+
...relationship.description && { description: relationship.description }
|
|
3199
|
+
}));
|
|
3200
|
+
}
|
|
3201
|
+
function toIndexes(type) {
|
|
3202
|
+
return Object.entries(type.indexes ?? {}).sort(([a], [b]) => a.localeCompare(b)).map(([name, index]) => ({
|
|
3203
|
+
name,
|
|
3204
|
+
fields: [...index.fields],
|
|
3205
|
+
unique: index.unique === true
|
|
3206
|
+
}));
|
|
3207
|
+
}
|
|
3208
|
+
function toColumnRelation(options) {
|
|
3209
|
+
const { fieldConfig, parsedField } = options;
|
|
3210
|
+
if (!fieldConfig.foreignKey || !fieldConfig.foreignKeyType) return;
|
|
3211
|
+
const required = fieldConfig.required !== false;
|
|
3212
|
+
const kind = parsedField?.relation || fieldConfig.rawRelation ? "relation" : "foreignKey";
|
|
3213
|
+
return {
|
|
3214
|
+
targetTable: fieldConfig.foreignKeyType,
|
|
3215
|
+
targetColumn: fieldConfig.foreignKeyField || "id",
|
|
3216
|
+
kind,
|
|
3217
|
+
required,
|
|
3218
|
+
...fieldConfig.rawRelation?.type && { relationType: fieldConfig.rawRelation.type },
|
|
3219
|
+
...parsedField?.relation?.forwardName && { forwardName: parsedField.relation.forwardName },
|
|
3220
|
+
...parsedField?.relation?.backwardName && { backwardName: parsedField.relation.backwardName }
|
|
3221
|
+
};
|
|
3222
|
+
}
|
|
3223
|
+
function toColumn(options) {
|
|
3224
|
+
const { fieldName, fieldConfig } = options;
|
|
3225
|
+
const indexNames = options.indexEntries.filter(([, index]) => index.fields.includes(fieldName)).map(([name]) => name);
|
|
3226
|
+
const uniqueIndexNames = options.indexEntries.filter(([, index]) => index.unique && index.fields.includes(fieldName)).map(([name]) => name);
|
|
3227
|
+
const enumValues = fieldConfig.allowedValues?.map((value) => value.value) ?? [];
|
|
3228
|
+
const enumValueDescriptions = Object.fromEntries((fieldConfig.allowedValues ?? []).flatMap((value) => value.description ? [[value.value, value.description]] : []));
|
|
3229
|
+
const nestedFields = Object.entries(fieldConfig.fields ?? {}).map(([nestedName, nestedConfig]) => toColumn({
|
|
3230
|
+
fieldName: nestedName,
|
|
3231
|
+
fieldConfig: nestedConfig,
|
|
3232
|
+
indexEntries: []
|
|
3233
|
+
}));
|
|
3234
|
+
const relation = toColumnRelation(options);
|
|
3138
3235
|
return {
|
|
3139
3236
|
name: fieldName,
|
|
3140
|
-
type: fieldConfig.
|
|
3141
|
-
|
|
3142
|
-
|
|
3237
|
+
type: fieldConfig.type || "string",
|
|
3238
|
+
required: fieldConfig.required !== false,
|
|
3239
|
+
array: fieldConfig.array === true,
|
|
3240
|
+
...fieldConfig.description && { description: fieldConfig.description },
|
|
3241
|
+
...fieldConfig.unique && { unique: true },
|
|
3242
|
+
...(fieldConfig.index || indexNames.length > 0) && { index: true },
|
|
3243
|
+
...indexNames.length > 0 && { indexNames },
|
|
3244
|
+
...uniqueIndexNames.length > 0 && { uniqueIndexNames },
|
|
3245
|
+
...enumValues.length > 0 && { enumValues },
|
|
3246
|
+
...Object.keys(enumValueDescriptions).length > 0 && { enumValueDescriptions },
|
|
3247
|
+
...fieldConfig.vector && { vector: true },
|
|
3248
|
+
...fieldConfig.serial && { serial: { ...fieldConfig.serial } },
|
|
3249
|
+
...fieldConfig.scale !== void 0 && { scale: fieldConfig.scale },
|
|
3250
|
+
...fieldConfig.validate?.length && { validations: fieldConfig.validate.length },
|
|
3251
|
+
...fieldConfig.hooks && { hooks: {
|
|
3252
|
+
...fieldConfig.hooks.create && { create: true },
|
|
3253
|
+
...fieldConfig.hooks.update && { update: true }
|
|
3254
|
+
} },
|
|
3255
|
+
...nestedFields.length > 0 && { fields: nestedFields },
|
|
3256
|
+
...relation && { relation }
|
|
3143
3257
|
};
|
|
3144
3258
|
}
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
const
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
const tableConstraints = [];
|
|
3162
|
-
columns.push({
|
|
3259
|
+
function toTable(type, source) {
|
|
3260
|
+
const indexes = toIndexes(type);
|
|
3261
|
+
const indexEntries = indexes.map((index) => [index.name, index]);
|
|
3262
|
+
const fieldColumns = Object.entries(type.fields).filter(([fieldName]) => fieldName !== "id").map(([fieldName, field]) => toColumn({
|
|
3263
|
+
fieldName,
|
|
3264
|
+
fieldConfig: field.config,
|
|
3265
|
+
parsedField: field,
|
|
3266
|
+
indexEntries
|
|
3267
|
+
}));
|
|
3268
|
+
const typeSource = toTypeSource(source);
|
|
3269
|
+
return {
|
|
3270
|
+
name: type.name,
|
|
3271
|
+
pluralForm: type.pluralForm,
|
|
3272
|
+
...type.description && { description: type.description },
|
|
3273
|
+
...typeSource && { source: typeSource },
|
|
3274
|
+
columns: [{
|
|
3163
3275
|
name: "id",
|
|
3164
3276
|
type: "uuid",
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
values = /* @__PURE__ */ new Set();
|
|
3182
|
-
enumsMap.set(enumName, values);
|
|
3183
|
-
}
|
|
3184
|
-
for (const value of fieldConfig.allowedValues) values.add(value.value);
|
|
3185
|
-
}
|
|
3186
|
-
if (fieldConfig.foreignKey && fieldConfig.foreignKeyType) {
|
|
3187
|
-
const foreignTable = fieldConfig.foreignKeyType;
|
|
3188
|
-
const foreignColumn = fieldConfig.foreignKeyField || "id";
|
|
3189
|
-
const childCardinality = fieldConfig.required ? "exactly_one" : "zero_or_one";
|
|
3190
|
-
relations.push({
|
|
3191
|
-
table: tableName,
|
|
3192
|
-
columns: [fieldName],
|
|
3193
|
-
parent_table: foreignTable,
|
|
3194
|
-
parent_columns: [foreignColumn],
|
|
3195
|
-
cardinality: childCardinality,
|
|
3196
|
-
parent_cardinality: "zero_or_more",
|
|
3197
|
-
def: ""
|
|
3198
|
-
});
|
|
3199
|
-
tableConstraints.push({
|
|
3200
|
-
name: `fk_${tableName}_${fieldName}`,
|
|
3201
|
-
type: "FOREIGN KEY",
|
|
3202
|
-
def: "",
|
|
3203
|
-
table: tableName,
|
|
3204
|
-
columns: [fieldName],
|
|
3205
|
-
referenced_table: foreignTable,
|
|
3206
|
-
referenced_columns: [foreignColumn]
|
|
3207
|
-
});
|
|
3208
|
-
if (!referencedByTable[tableName]) referencedByTable[tableName] = /* @__PURE__ */ new Set();
|
|
3209
|
-
referencedByTable[tableName].add(foreignTable);
|
|
3210
|
-
}
|
|
3211
|
-
}
|
|
3212
|
-
constraintsByTable[tableName] = tableConstraints;
|
|
3213
|
-
tables.push({
|
|
3214
|
-
name: tableName,
|
|
3215
|
-
type: "table",
|
|
3216
|
-
comment: schema?.description ?? "",
|
|
3217
|
-
columns,
|
|
3218
|
-
indexes: [],
|
|
3219
|
-
constraints: constraintsByTable[tableName] ?? [],
|
|
3220
|
-
triggers: [],
|
|
3221
|
-
def: "",
|
|
3222
|
-
referenced_tables: []
|
|
3223
|
-
});
|
|
3224
|
-
}
|
|
3225
|
-
for (const table of tables) {
|
|
3226
|
-
const referenced = referencedByTable[table.name];
|
|
3227
|
-
table.referenced_tables = referenced ? Array.from(referenced) : [];
|
|
3228
|
-
}
|
|
3229
|
-
const enums = [];
|
|
3230
|
-
for (const [name, values] of enumsMap.entries()) enums.push({
|
|
3231
|
-
name,
|
|
3232
|
-
values: Array.from(values)
|
|
3277
|
+
required: true,
|
|
3278
|
+
array: false,
|
|
3279
|
+
primaryKey: true,
|
|
3280
|
+
unique: true
|
|
3281
|
+
}, ...fieldColumns],
|
|
3282
|
+
indexes,
|
|
3283
|
+
forwardRelationships: toRelationships(type.forwardRelationships),
|
|
3284
|
+
backwardRelationships: toRelationships(type.backwardRelationships)
|
|
3285
|
+
};
|
|
3286
|
+
}
|
|
3287
|
+
function toRelation(sourceTable, field) {
|
|
3288
|
+
const relation = toColumnRelation({
|
|
3289
|
+
fieldName: field.name,
|
|
3290
|
+
fieldConfig: field.config,
|
|
3291
|
+
parsedField: field,
|
|
3292
|
+
indexEntries: []
|
|
3233
3293
|
});
|
|
3294
|
+
if (!relation) return void 0;
|
|
3234
3295
|
return {
|
|
3235
|
-
name:
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3296
|
+
name: `${sourceTable}.${field.name}->${relation.targetTable}.${relation.targetColumn}`,
|
|
3297
|
+
sourceTable,
|
|
3298
|
+
sourceColumns: [field.name],
|
|
3299
|
+
targetTable: relation.targetTable,
|
|
3300
|
+
targetColumns: [relation.targetColumn],
|
|
3301
|
+
required: relation.required,
|
|
3302
|
+
unique: field.config.unique === true || field.relation?.unique === true,
|
|
3303
|
+
kind: relation.kind,
|
|
3304
|
+
...relation.relationType && { relationType: relation.relationType },
|
|
3305
|
+
...relation.forwardName && { forwardName: relation.forwardName },
|
|
3306
|
+
...relation.backwardName && { backwardName: relation.backwardName }
|
|
3239
3307
|
};
|
|
3240
3308
|
}
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
const types = await fetchAll(async (pageToken, maxPageSize) => {
|
|
3249
|
-
try {
|
|
3250
|
-
const { tailordbTypes, nextPageToken } = await client.listTailorDBTypes({
|
|
3251
|
-
workspaceId,
|
|
3252
|
-
namespaceName: namespace,
|
|
3253
|
-
pageToken,
|
|
3254
|
-
pageSize: maxPageSize
|
|
3255
|
-
});
|
|
3256
|
-
return [tailordbTypes, nextPageToken];
|
|
3257
|
-
} catch (error) {
|
|
3258
|
-
if (error instanceof ConnectError && error.code === Code.NotFound) return [[], ""];
|
|
3259
|
-
throw error;
|
|
3260
|
-
}
|
|
3261
|
-
});
|
|
3262
|
-
if (types.length === 0) logger.warn(`No TailorDB types found in namespace "${namespace}". Returning empty schema.`);
|
|
3263
|
-
return buildTblsSchema(types, namespace);
|
|
3309
|
+
function buildRelations(types) {
|
|
3310
|
+
const relations = [];
|
|
3311
|
+
for (const type of Object.values(types)) for (const field of Object.values(type.fields)) {
|
|
3312
|
+
const relation = toRelation(type.name, field);
|
|
3313
|
+
if (relation) relations.push(relation);
|
|
3314
|
+
}
|
|
3315
|
+
return relations.sort((a, b) => a.name.localeCompare(b.name));
|
|
3264
3316
|
}
|
|
3265
3317
|
/**
|
|
3266
|
-
*
|
|
3267
|
-
* @param options -
|
|
3318
|
+
* Build the TailorDB ERD viewer schema for one namespace.
|
|
3319
|
+
* @param options - Schema build options.
|
|
3320
|
+
* @returns TailorDB ERD viewer schema.
|
|
3268
3321
|
*/
|
|
3269
|
-
|
|
3270
|
-
const
|
|
3271
|
-
const
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3322
|
+
function buildTailorDbErdSchema(options) {
|
|
3323
|
+
const { namespaceData } = options;
|
|
3324
|
+
const tables = Object.values(namespaceData.types).sort((a, b) => a.name.localeCompare(b.name)).map((type) => toTable(type, namespaceData.sourceInfo.get(type.name)));
|
|
3325
|
+
const schemaWithoutRevision = {
|
|
3326
|
+
version: 1,
|
|
3327
|
+
namespace: namespaceData.namespace,
|
|
3328
|
+
source: options.source ?? "local",
|
|
3329
|
+
cleanRoom: {
|
|
3330
|
+
implementation: "tailor-sdk",
|
|
3331
|
+
notes: CLEAN_ROOM_NOTES
|
|
3332
|
+
},
|
|
3333
|
+
tables,
|
|
3334
|
+
relations: buildRelations(namespaceData.types)
|
|
3335
|
+
};
|
|
3336
|
+
return {
|
|
3337
|
+
...schemaWithoutRevision,
|
|
3338
|
+
generatedAt: options.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
3339
|
+
revision: buildRevision(schemaWithoutRevision)
|
|
3340
|
+
};
|
|
3276
3341
|
}
|
|
3277
3342
|
|
|
3278
3343
|
//#endregion
|
|
3279
3344
|
//#region src/cli/commands/tailordb/erd/utils.ts
|
|
3280
3345
|
/**
|
|
3281
|
-
* Initialize shared ERD command
|
|
3282
|
-
* @param args - CLI arguments.
|
|
3283
|
-
* @returns Initialized context.
|
|
3346
|
+
* Initialize shared ERD command behavior.
|
|
3284
3347
|
*/
|
|
3285
|
-
|
|
3348
|
+
function initErdCommand() {
|
|
3286
3349
|
logBetaWarning("tailordb erd");
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
const { config } = await loadConfig(args.config);
|
|
3350
|
+
}
|
|
3351
|
+
/**
|
|
3352
|
+
* Initialize platform context for ERD deployment.
|
|
3353
|
+
* @param args - CLI arguments.
|
|
3354
|
+
* @returns Initialized deploy context.
|
|
3355
|
+
*/
|
|
3356
|
+
async function initErdDeployContext(args) {
|
|
3357
|
+
initErdCommand();
|
|
3296
3358
|
return {
|
|
3297
|
-
client
|
|
3298
|
-
|
|
3299
|
-
|
|
3359
|
+
client: await initOperatorClient(await loadAccessToken({
|
|
3360
|
+
useProfile: true,
|
|
3361
|
+
profile: args.profile
|
|
3362
|
+
})),
|
|
3363
|
+
workspaceId: await loadWorkspaceId({
|
|
3364
|
+
workspaceId: args.workspaceId,
|
|
3365
|
+
profile: args.profile
|
|
3366
|
+
})
|
|
3300
3367
|
};
|
|
3301
3368
|
}
|
|
3302
3369
|
|
|
3303
3370
|
//#endregion
|
|
3304
|
-
//#region src/cli/commands/tailordb/erd/
|
|
3305
|
-
const
|
|
3371
|
+
//#region src/cli/commands/tailordb/erd/viewer.ts
|
|
3372
|
+
const VIEWER_ASSETS_DIR = "erd-viewer-assets";
|
|
3373
|
+
const STYLES_LINK = "<link rel=\"stylesheet\" href=\"./styles.css\" />";
|
|
3374
|
+
const APP_SCRIPT = "<script src=\"./app.js\" type=\"module\"><\/script>";
|
|
3375
|
+
function assetDirCandidates() {
|
|
3376
|
+
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
3377
|
+
return [
|
|
3378
|
+
path.join(currentDir, "viewer-assets"),
|
|
3379
|
+
path.join(currentDir, VIEWER_ASSETS_DIR),
|
|
3380
|
+
path.join(currentDir, "commands", "tailordb", "erd", VIEWER_ASSETS_DIR),
|
|
3381
|
+
path.resolve(process.cwd(), "packages/sdk/src/cli/commands/tailordb/erd/viewer-assets")
|
|
3382
|
+
];
|
|
3383
|
+
}
|
|
3306
3384
|
/**
|
|
3307
|
-
* Resolve
|
|
3308
|
-
* @
|
|
3309
|
-
* @param explicitNamespace - Namespace override.
|
|
3310
|
-
* @returns Resolved namespace and erdSite.
|
|
3385
|
+
* Resolve the packaged ERD viewer asset directory.
|
|
3386
|
+
* @returns Absolute path to the viewer asset directory.
|
|
3311
3387
|
*/
|
|
3312
|
-
function
|
|
3313
|
-
const
|
|
3314
|
-
|
|
3315
|
-
const dbConfig = config.db?.[namespace];
|
|
3316
|
-
if (!dbConfig || typeof dbConfig !== "object" || "external" in dbConfig) throw new Error(`TailorDB namespace "${namespace}" not found in config.db.`);
|
|
3317
|
-
return {
|
|
3318
|
-
namespace,
|
|
3319
|
-
erdSite: dbConfig.erdSite
|
|
3320
|
-
};
|
|
3388
|
+
function resolveViewerAssetsDir() {
|
|
3389
|
+
for (const candidate of assetDirCandidates()) if (fs$1.existsSync(path.join(candidate, "index.html"))) return candidate;
|
|
3390
|
+
throw new Error(`ERD viewer assets not found. Checked: ${assetDirCandidates().join(", ")}`);
|
|
3321
3391
|
}
|
|
3322
3392
|
/**
|
|
3323
|
-
*
|
|
3324
|
-
*
|
|
3325
|
-
*
|
|
3326
|
-
*
|
|
3393
|
+
* Build the self-contained ERD viewer HTML document. CSS, JS, and the schema
|
|
3394
|
+
* are inlined as separately extractable blocks: a `<style>` element, a
|
|
3395
|
+
* `<script type="module">`, and a `<script type="application/json"
|
|
3396
|
+
* id="erd-schema">` data block. This renders without any sibling asset files
|
|
3397
|
+
* and lets external tooling (e.g. a future ERD diff) pull out the schema via
|
|
3398
|
+
* `JSON.parse`.
|
|
3399
|
+
* @param options - Viewer build options.
|
|
3400
|
+
* @returns The self-contained HTML document.
|
|
3327
3401
|
*/
|
|
3328
|
-
function
|
|
3329
|
-
const
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3402
|
+
function buildViewerHtml(options) {
|
|
3403
|
+
const assetsDir = resolveViewerAssetsDir();
|
|
3404
|
+
const html = fs$1.readFileSync(path.join(assetsDir, "index.html"), "utf8");
|
|
3405
|
+
const css = fs$1.readFileSync(path.join(assetsDir, "styles.css"), "utf8");
|
|
3406
|
+
const appJs = fs$1.readFileSync(path.join(assetsDir, "app.js"), "utf8");
|
|
3407
|
+
if (!html.includes(STYLES_LINK) || !html.includes(APP_SCRIPT)) throw new Error("ERD viewer index.html is missing expected asset references for inlining.");
|
|
3408
|
+
const embedScript = `<script type="application/json" id="erd-schema">${JSON.stringify(options.schema).replaceAll("<", "\\u003c")}<\/script>`;
|
|
3409
|
+
const inlineScript = `<script type="module">\n${appJs.replace(/<\/script/gi, "<\\/script")}\n<\/script>`;
|
|
3410
|
+
return html.replace(STYLES_LINK, `<style>\n${css}\n</style>`).replace(APP_SCRIPT, `${embedScript}\n ${inlineScript}`);
|
|
3335
3411
|
}
|
|
3336
3412
|
/**
|
|
3337
|
-
*
|
|
3338
|
-
* @param
|
|
3339
|
-
* @param cwd - Working directory where liam will run (dist is created here)
|
|
3340
|
-
* @returns Resolves when the build completes successfully
|
|
3413
|
+
* Write the self-contained TailorDB ERD viewer to `<distDir>/index.html`.
|
|
3414
|
+
* @param options - Viewer dist write options.
|
|
3341
3415
|
*/
|
|
3342
|
-
|
|
3343
|
-
fs$1.
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
try {
|
|
3347
|
-
liamBinPath = resolveCliBinPath({
|
|
3348
|
-
packageName: "@liam-hq/cli",
|
|
3349
|
-
binName: "liam"
|
|
3350
|
-
});
|
|
3351
|
-
} catch (error) {
|
|
3352
|
-
logger.error(String(error));
|
|
3353
|
-
reject(error);
|
|
3354
|
-
return;
|
|
3355
|
-
}
|
|
3356
|
-
const child = spawn(process.execPath, [
|
|
3357
|
-
liamBinPath,
|
|
3358
|
-
"erd",
|
|
3359
|
-
"build",
|
|
3360
|
-
"--format",
|
|
3361
|
-
"tbls",
|
|
3362
|
-
"--input",
|
|
3363
|
-
schemaPath
|
|
3364
|
-
], {
|
|
3365
|
-
stdio: [
|
|
3366
|
-
"pipe",
|
|
3367
|
-
"ignore",
|
|
3368
|
-
"pipe"
|
|
3369
|
-
],
|
|
3370
|
-
cwd
|
|
3371
|
-
});
|
|
3372
|
-
let stderrOutput = "";
|
|
3373
|
-
child.stderr?.on("data", (data) => {
|
|
3374
|
-
stderrOutput += data.toString();
|
|
3375
|
-
});
|
|
3376
|
-
child.on("error", (error) => {
|
|
3377
|
-
logger.error("Failed to run `@liam-hq/cli`.");
|
|
3378
|
-
reject(error);
|
|
3379
|
-
});
|
|
3380
|
-
child.on("close", (code) => {
|
|
3381
|
-
if (code === 0) resolve();
|
|
3382
|
-
else {
|
|
3383
|
-
if (stderrOutput) logger.error(stderrOutput);
|
|
3384
|
-
logger.error("liam CLI exited with a non-zero code. Ensure `@liam-hq/cli erd build --format tbls --input schema.json` works in your project.");
|
|
3385
|
-
reject(/* @__PURE__ */ new Error(`liam CLI exited with code ${code ?? 1}`));
|
|
3386
|
-
}
|
|
3387
|
-
});
|
|
3416
|
+
function writeViewerDist(options) {
|
|
3417
|
+
fs$1.rmSync(options.distDir, {
|
|
3418
|
+
recursive: true,
|
|
3419
|
+
force: true
|
|
3388
3420
|
});
|
|
3421
|
+
fs$1.mkdirSync(options.distDir, { recursive: true });
|
|
3422
|
+
fs$1.writeFileSync(path.join(options.distDir, "index.html"), buildViewerHtml({ schema: options.schema }), "utf8");
|
|
3423
|
+
}
|
|
3424
|
+
|
|
3425
|
+
//#endregion
|
|
3426
|
+
//#region src/cli/commands/tailordb/erd/export.ts
|
|
3427
|
+
const DEFAULT_ERD_BASE_DIR$1 = ".tailor-sdk/erd";
|
|
3428
|
+
function getErdSite(context, namespace) {
|
|
3429
|
+
const dbConfig = context.config.db?.[namespace];
|
|
3430
|
+
if (!dbConfig || "external" in dbConfig) return;
|
|
3431
|
+
return dbConfig.erdSite;
|
|
3432
|
+
}
|
|
3433
|
+
function resolveExplicitTarget(options) {
|
|
3434
|
+
const namespaceData = options.context.namespaces.find((candidate) => candidate.namespace === options.namespace);
|
|
3435
|
+
if (!namespaceData) {
|
|
3436
|
+
const available = options.context.namespaces.map((candidate) => candidate.namespace).join(", ");
|
|
3437
|
+
throw new Error(`TailorDB namespace "${options.namespace}" not found in local config.db.` + (available ? ` Available owned namespaces: ${available}` : ""));
|
|
3438
|
+
}
|
|
3439
|
+
const erdSite = getErdSite(options.context, namespaceData.namespace);
|
|
3440
|
+
if (options.requireErdSite && !erdSite) throw new Error(`No erdSite configured for namespace "${namespaceData.namespace}". Add erdSite: "<static-website-name>" to db.${namespaceData.namespace} in tailor.config.ts.`);
|
|
3441
|
+
return toTarget(options.outputDir, namespaceData, erdSite);
|
|
3442
|
+
}
|
|
3443
|
+
function resolveAllTargets(options) {
|
|
3444
|
+
const namespaces = options.context.namespaces.filter((namespaceData) => !options.requireErdSite || getErdSite(options.context, namespaceData.namespace));
|
|
3445
|
+
if (namespaces.length === 0) throw new Error(options.requireErdSite ? "No namespaces with erdSite configured found. Add erdSite: \"<static-website-name>\" to db.<namespace> in tailor.config.ts." : "No TailorDB namespaces found in config. Please define db services in tailor.config.ts.");
|
|
3446
|
+
logger.info(`Found ${namespaces.length} namespace(s)${options.requireErdSite ? " with erdSite configured" : ""}.`);
|
|
3447
|
+
return namespaces.map((namespaceData) => toTarget(options.outputDir, namespaceData, getErdSite(options.context, namespaceData.namespace)));
|
|
3448
|
+
}
|
|
3449
|
+
function toTarget(outputDir, namespaceData, erdSite) {
|
|
3450
|
+
return {
|
|
3451
|
+
namespaceData,
|
|
3452
|
+
erdSite,
|
|
3453
|
+
distDir: path.join(outputDir, namespaceData.namespace, "dist")
|
|
3454
|
+
};
|
|
3455
|
+
}
|
|
3456
|
+
function resolveTargets(options) {
|
|
3457
|
+
if (options.namespace) return [resolveExplicitTarget(options)];
|
|
3458
|
+
return resolveAllTargets(options);
|
|
3459
|
+
}
|
|
3460
|
+
function prepareErdBuild(target) {
|
|
3461
|
+
writeViewerDist({
|
|
3462
|
+
schema: buildTailorDbErdSchema({ namespaceData: target.namespaceData }),
|
|
3463
|
+
distDir: target.distDir
|
|
3464
|
+
});
|
|
3465
|
+
const relativePath = path.relative(process.cwd(), target.distDir);
|
|
3466
|
+
logger.success(`Built ERD to ${relativePath}`);
|
|
3467
|
+
return {
|
|
3468
|
+
namespace: target.namespaceData.namespace,
|
|
3469
|
+
erdSite: target.erdSite,
|
|
3470
|
+
distDir: target.distDir
|
|
3471
|
+
};
|
|
3389
3472
|
}
|
|
3390
3473
|
/**
|
|
3391
|
-
*
|
|
3474
|
+
* Prepare TailorDB ERD static viewer builds for one or more namespaces.
|
|
3392
3475
|
* @param options - Build options.
|
|
3476
|
+
* @returns Build results by namespace.
|
|
3393
3477
|
*/
|
|
3394
|
-
async function
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3478
|
+
async function prepareErdBuilds(options) {
|
|
3479
|
+
return prepareErdBuildsFromContext({
|
|
3480
|
+
context: await loadLocalErdSchema({
|
|
3481
|
+
configPath: options.configPath,
|
|
3482
|
+
namespaces: options.namespace ? [options.namespace] : void 0,
|
|
3483
|
+
requireErdSite: options.requireErdSite
|
|
3484
|
+
}),
|
|
3485
|
+
namespace: options.namespace,
|
|
3486
|
+
outputDir: options.outputDir,
|
|
3487
|
+
requireErdSite: options.requireErdSite
|
|
3488
|
+
});
|
|
3400
3489
|
}
|
|
3401
3490
|
/**
|
|
3402
|
-
* Prepare ERD builds
|
|
3491
|
+
* Prepare TailorDB ERD static viewer builds from an already loaded schema context.
|
|
3403
3492
|
* @param options - Build options.
|
|
3404
3493
|
* @returns Build results by namespace.
|
|
3405
3494
|
*/
|
|
3406
|
-
|
|
3407
|
-
const
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
targets = [{
|
|
3415
|
-
namespace,
|
|
3416
|
-
erdSite,
|
|
3417
|
-
schemaOutputPath: path.join(erdDir, "schema.json"),
|
|
3418
|
-
distDir: path.join(erdDir, "dist"),
|
|
3419
|
-
erdDir
|
|
3420
|
-
}];
|
|
3421
|
-
} else {
|
|
3422
|
-
const namespaces = resolveAllNamespaces(config, { requireErdSite: options.requireErdSite });
|
|
3423
|
-
if (namespaces.length === 0) throw new Error(options.requireErdSite ? "No namespaces with erdSite configured found. Add erdSite: \"<static-website-name>\" to db.<namespace> in tailor.config.ts." : "No TailorDB namespaces found in config. Please define db services in tailor.config.ts.");
|
|
3424
|
-
logger.info(`Found ${namespaces.length} namespace(s)${options.requireErdSite ? " with erdSite configured" : ""}.`);
|
|
3425
|
-
targets = namespaces.map(({ namespace, erdSite }) => {
|
|
3426
|
-
const erdDir = path.join(baseDir, namespace);
|
|
3427
|
-
return {
|
|
3428
|
-
namespace,
|
|
3429
|
-
erdSite,
|
|
3430
|
-
schemaOutputPath: path.join(erdDir, "schema.json"),
|
|
3431
|
-
distDir: path.join(erdDir, "dist"),
|
|
3432
|
-
erdDir
|
|
3433
|
-
};
|
|
3434
|
-
});
|
|
3435
|
-
}
|
|
3436
|
-
await Promise.all(targets.map((target) => prepareErdBuild({
|
|
3437
|
-
namespace: target.namespace,
|
|
3438
|
-
client,
|
|
3439
|
-
workspaceId,
|
|
3440
|
-
outputPath: target.schemaOutputPath,
|
|
3441
|
-
erdDir: target.erdDir
|
|
3442
|
-
})));
|
|
3443
|
-
return targets;
|
|
3495
|
+
function prepareErdBuildsFromContext(options) {
|
|
3496
|
+
const outputDir = path.resolve(process.cwd(), options.outputDir ?? DEFAULT_ERD_BASE_DIR$1);
|
|
3497
|
+
return resolveTargets({
|
|
3498
|
+
context: options.context,
|
|
3499
|
+
namespace: options.namespace,
|
|
3500
|
+
outputDir,
|
|
3501
|
+
requireErdSite: options.requireErdSite
|
|
3502
|
+
}).map((target) => prepareErdBuild(target));
|
|
3444
3503
|
}
|
|
3445
3504
|
const erdExportCommand = defineAppCommand({
|
|
3446
3505
|
name: "export",
|
|
3447
|
-
description: "Export
|
|
3506
|
+
description: "Export TailorDB ERD static viewer from local TailorDB schema.",
|
|
3448
3507
|
args: z.object({
|
|
3449
|
-
...
|
|
3508
|
+
...configArg,
|
|
3450
3509
|
namespace: arg(z.string().optional(), {
|
|
3451
3510
|
alias: "n",
|
|
3452
3511
|
description: "TailorDB namespace name (optional if only one namespace is defined in config)"
|
|
3453
3512
|
}),
|
|
3454
|
-
output: arg(z.string().default(DEFAULT_ERD_BASE_DIR), {
|
|
3513
|
+
output: arg(z.string().default(DEFAULT_ERD_BASE_DIR$1), {
|
|
3455
3514
|
alias: "o",
|
|
3456
|
-
description: "Output directory path for
|
|
3515
|
+
description: "Output directory path for TailorDB ERD viewer files (writes to `<outputDir>/<namespace>/dist`)",
|
|
3457
3516
|
completion: { type: "directory" }
|
|
3458
3517
|
})
|
|
3459
3518
|
}).strict(),
|
|
3460
3519
|
run: async (args) => {
|
|
3461
|
-
|
|
3462
|
-
const outputDir = path.resolve(process.cwd(), String(args.output));
|
|
3520
|
+
initErdCommand();
|
|
3463
3521
|
const results = await prepareErdBuilds({
|
|
3464
|
-
|
|
3465
|
-
workspaceId,
|
|
3466
|
-
config,
|
|
3522
|
+
configPath: args.config,
|
|
3467
3523
|
namespace: args.namespace,
|
|
3468
|
-
outputDir
|
|
3524
|
+
outputDir: args.output
|
|
3469
3525
|
});
|
|
3470
3526
|
logger.newline();
|
|
3471
3527
|
if (args.json) logger.out(results.map((result) => ({
|
|
3472
3528
|
namespace: result.namespace,
|
|
3473
|
-
distDir: result.distDir
|
|
3474
|
-
schemaOutputPath: result.schemaOutputPath
|
|
3529
|
+
distDir: result.distDir
|
|
3475
3530
|
})));
|
|
3476
3531
|
else for (const result of results) {
|
|
3477
3532
|
logger.out(`Exported ERD for namespace "${result.namespace}"`);
|
|
3478
|
-
logger.out(` -
|
|
3479
|
-
logger.out(` - tbls schema.json: ${result.schemaOutputPath}`);
|
|
3533
|
+
logger.out(` - ERD viewer: ${path.join(result.distDir, "index.html")}`);
|
|
3480
3534
|
}
|
|
3481
3535
|
}
|
|
3482
3536
|
});
|
|
@@ -3495,11 +3549,9 @@ const erdDeployCommand = defineAppCommand({
|
|
|
3495
3549
|
}).strict(),
|
|
3496
3550
|
run: async (args) => {
|
|
3497
3551
|
await assertWritable({ profile: args.profile });
|
|
3498
|
-
const { client, workspaceId
|
|
3552
|
+
const { client, workspaceId } = await initErdDeployContext(args);
|
|
3499
3553
|
const buildResults = await prepareErdBuilds({
|
|
3500
|
-
|
|
3501
|
-
workspaceId,
|
|
3502
|
-
config,
|
|
3554
|
+
configPath: args.config,
|
|
3503
3555
|
namespace: args.namespace,
|
|
3504
3556
|
requireErdSite: true
|
|
3505
3557
|
});
|
|
@@ -3527,65 +3579,354 @@ const erdDeployCommand = defineAppCommand({
|
|
|
3527
3579
|
|
|
3528
3580
|
//#endregion
|
|
3529
3581
|
//#region src/cli/commands/tailordb/erd/serve.ts
|
|
3582
|
+
const DEFAULT_ERD_BASE_DIR = ".tailor-sdk/erd";
|
|
3583
|
+
const LOCAL_HOST = "127.0.0.1";
|
|
3584
|
+
const GLOB_CHARS = /[*?[\]{}()!+@]/;
|
|
3530
3585
|
function formatServeCommand(namespace) {
|
|
3531
3586
|
return `tailor-sdk tailordb erd serve --namespace ${namespace}`;
|
|
3532
3587
|
}
|
|
3533
|
-
|
|
3534
|
-
|
|
3588
|
+
function getCacheControl(filePath) {
|
|
3589
|
+
return filePath.endsWith(".html") || filePath.endsWith(".json") ? "no-cache" : "public, max-age=3600";
|
|
3590
|
+
}
|
|
3591
|
+
function resolveRequestPath(distDir, requestUrl) {
|
|
3592
|
+
const url = new URL(requestUrl ?? "/", "http://localhost");
|
|
3593
|
+
let pathname;
|
|
3594
|
+
try {
|
|
3595
|
+
pathname = decodeURIComponent(url.pathname);
|
|
3596
|
+
} catch {
|
|
3597
|
+
return;
|
|
3598
|
+
}
|
|
3599
|
+
if (pathname === "/" || pathname.endsWith("/")) pathname = path.join(pathname, "index.html");
|
|
3600
|
+
const root = path.resolve(distDir);
|
|
3601
|
+
const filePath = path.resolve(root, `.${pathname}`);
|
|
3602
|
+
if (filePath !== root && !filePath.startsWith(`${root}${path.sep}`)) return;
|
|
3603
|
+
return filePath;
|
|
3604
|
+
}
|
|
3605
|
+
function openStaticFile(filePath) {
|
|
3606
|
+
let fd;
|
|
3607
|
+
try {
|
|
3608
|
+
fd = fs$1.openSync(filePath, "r");
|
|
3609
|
+
if (!fs$1.fstatSync(fd).isFile()) {
|
|
3610
|
+
fs$1.closeSync(fd);
|
|
3611
|
+
return;
|
|
3612
|
+
}
|
|
3613
|
+
return {
|
|
3614
|
+
filePath,
|
|
3615
|
+
fd
|
|
3616
|
+
};
|
|
3617
|
+
} catch {
|
|
3618
|
+
if (fd !== void 0) fs$1.closeSync(fd);
|
|
3619
|
+
return;
|
|
3620
|
+
}
|
|
3621
|
+
}
|
|
3622
|
+
function serveFile(distDir, req, res) {
|
|
3623
|
+
const filePath = resolveRequestPath(distDir, req.url);
|
|
3624
|
+
if (!filePath) {
|
|
3625
|
+
res.writeHead(403);
|
|
3626
|
+
res.end("Forbidden");
|
|
3627
|
+
return;
|
|
3628
|
+
}
|
|
3629
|
+
const fallbackPath = path.join(distDir, "index.html");
|
|
3630
|
+
const target = openStaticFile(filePath) ?? openStaticFile(fallbackPath);
|
|
3631
|
+
if (!target) {
|
|
3632
|
+
res.writeHead(503, {
|
|
3633
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
3634
|
+
"Cache-Control": "no-cache",
|
|
3635
|
+
"Retry-After": "1"
|
|
3636
|
+
});
|
|
3637
|
+
res.end("ERD build is refreshing. Please retry.");
|
|
3638
|
+
return;
|
|
3639
|
+
}
|
|
3640
|
+
const mimeType = lookup(target.filePath) || "application/octet-stream";
|
|
3641
|
+
const stream = fs$1.createReadStream(target.filePath, {
|
|
3642
|
+
fd: target.fd,
|
|
3643
|
+
autoClose: true
|
|
3644
|
+
});
|
|
3645
|
+
stream.on("error", () => {
|
|
3646
|
+
if (!res.headersSent) {
|
|
3647
|
+
res.writeHead(503, {
|
|
3648
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
3649
|
+
"Cache-Control": "no-cache",
|
|
3650
|
+
"Retry-After": "1"
|
|
3651
|
+
});
|
|
3652
|
+
res.end("ERD build is refreshing. Please retry.");
|
|
3653
|
+
return;
|
|
3654
|
+
}
|
|
3655
|
+
res.destroy();
|
|
3656
|
+
});
|
|
3657
|
+
res.writeHead(200, {
|
|
3658
|
+
"Content-Type": mimeType,
|
|
3659
|
+
"Cache-Control": getCacheControl(target.filePath)
|
|
3660
|
+
});
|
|
3661
|
+
stream.pipe(res);
|
|
3662
|
+
}
|
|
3663
|
+
async function startStaticServer(options) {
|
|
3664
|
+
const server = http.createServer((req, res) => {
|
|
3665
|
+
serveFile(options.distDir, req, res);
|
|
3666
|
+
});
|
|
3667
|
+
return await new Promise((resolve, reject) => {
|
|
3668
|
+
server.once("error", reject);
|
|
3669
|
+
server.listen(options.port, LOCAL_HOST, () => {
|
|
3670
|
+
server.off("error", reject);
|
|
3671
|
+
const address = server.address();
|
|
3672
|
+
if (!address || typeof address === "string") {
|
|
3673
|
+
reject(/* @__PURE__ */ new Error("Failed to determine ERD server address."));
|
|
3674
|
+
return;
|
|
3675
|
+
}
|
|
3676
|
+
resolve({
|
|
3677
|
+
server,
|
|
3678
|
+
url: `http://${LOCAL_HOST}:${address.port}`
|
|
3679
|
+
});
|
|
3680
|
+
});
|
|
3681
|
+
});
|
|
3682
|
+
}
|
|
3683
|
+
function getWatchPatterns(config, results) {
|
|
3684
|
+
const namespaces = new Set(results.map((result) => result.namespace));
|
|
3685
|
+
const patterns = [config.path];
|
|
3686
|
+
for (const namespace of namespaces) {
|
|
3687
|
+
const dbConfig = config.db?.[namespace];
|
|
3688
|
+
if (dbConfig && !("external" in dbConfig)) patterns.push(...dbConfig.files);
|
|
3689
|
+
}
|
|
3690
|
+
return [...new Set(patterns)];
|
|
3691
|
+
}
|
|
3692
|
+
function hasGlobPattern(pattern) {
|
|
3693
|
+
return GLOB_CHARS.test(pattern);
|
|
3694
|
+
}
|
|
3695
|
+
function globBaseDir(pattern) {
|
|
3696
|
+
const absolutePattern = path.resolve(pattern);
|
|
3697
|
+
const parsed = path.parse(absolutePattern);
|
|
3698
|
+
const relativePattern = absolutePattern.slice(parsed.root.length);
|
|
3699
|
+
const literalParts = [];
|
|
3700
|
+
for (const part of relativePattern.split(path.sep)) {
|
|
3701
|
+
if (!part || GLOB_CHARS.test(part)) break;
|
|
3702
|
+
literalParts.push(part);
|
|
3703
|
+
}
|
|
3704
|
+
const literalPath = literalParts.length > 0 ? path.join(parsed.root, ...literalParts) : parsed.root;
|
|
3705
|
+
if (!literalPath || literalPath === parsed.root) return parsed.root || process.cwd();
|
|
3706
|
+
if (!fs$1.existsSync(literalPath)) return path.dirname(literalPath);
|
|
3707
|
+
return fs$1.statSync(literalPath).isDirectory() ? literalPath : path.dirname(literalPath);
|
|
3708
|
+
}
|
|
3709
|
+
async function expandWatchPattern(pattern) {
|
|
3710
|
+
if (!hasGlobPattern(pattern)) return [path.resolve(pattern)];
|
|
3711
|
+
const paths = /* @__PURE__ */ new Set();
|
|
3712
|
+
for await (const file of glob(pattern)) paths.add(path.resolve(file));
|
|
3713
|
+
const baseDir = globBaseDir(pattern);
|
|
3714
|
+
if (fs$1.existsSync(baseDir) && fs$1.statSync(baseDir).isDirectory()) paths.add(baseDir);
|
|
3715
|
+
return [...paths];
|
|
3716
|
+
}
|
|
3717
|
+
async function resolveWatchPathsFromConfig(config, results) {
|
|
3718
|
+
const paths = /* @__PURE__ */ new Set();
|
|
3719
|
+
for (const pattern of getWatchPatterns(config, results)) for (const watchPath of await expandWatchPattern(pattern)) paths.add(watchPath);
|
|
3720
|
+
return [...paths];
|
|
3721
|
+
}
|
|
3722
|
+
async function resolveWatchPaths(context, results) {
|
|
3723
|
+
return await resolveWatchPathsFromConfig(context.config, results);
|
|
3724
|
+
}
|
|
3725
|
+
function parseFreshErdExportResults(stdout) {
|
|
3726
|
+
const lines = stdout.trim().split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
3727
|
+
for (const line of lines.toReversed()) try {
|
|
3728
|
+
const parsed = JSON.parse(line);
|
|
3729
|
+
if (!Array.isArray(parsed)) continue;
|
|
3730
|
+
return parsed.map((entry) => {
|
|
3731
|
+
const result = entry;
|
|
3732
|
+
if (typeof result.namespace !== "string" || typeof result.distDir !== "string") throw new Error("Invalid ERD export JSON output.");
|
|
3733
|
+
return {
|
|
3734
|
+
namespace: result.namespace,
|
|
3735
|
+
distDir: result.distDir
|
|
3736
|
+
};
|
|
3737
|
+
});
|
|
3738
|
+
} catch {
|
|
3739
|
+
continue;
|
|
3740
|
+
}
|
|
3741
|
+
throw new Error("Failed to parse ERD export JSON output.");
|
|
3742
|
+
}
|
|
3743
|
+
function freshErdExportArgs(options) {
|
|
3744
|
+
const cliEntry = process.argv[1];
|
|
3745
|
+
if (!cliEntry) throw new Error("Cannot rebuild ERD schema in a fresh process: CLI entrypoint is unavailable.");
|
|
3746
|
+
const args = [
|
|
3747
|
+
cliEntry,
|
|
3748
|
+
"tailordb",
|
|
3749
|
+
"erd",
|
|
3750
|
+
"export",
|
|
3751
|
+
"--output",
|
|
3752
|
+
options.outputDir,
|
|
3753
|
+
"--json"
|
|
3754
|
+
];
|
|
3755
|
+
if (options.configPath) args.push("--config", options.configPath);
|
|
3756
|
+
if (options.namespace) args.push("--namespace", options.namespace);
|
|
3757
|
+
return args;
|
|
3758
|
+
}
|
|
3759
|
+
async function runFreshErdExport(options) {
|
|
3760
|
+
return await new Promise((resolve, reject) => {
|
|
3761
|
+
const child = spawn(process.execPath, freshErdExportArgs(options), {
|
|
3762
|
+
cwd: process.cwd(),
|
|
3763
|
+
env: process.env,
|
|
3764
|
+
stdio: [
|
|
3765
|
+
"ignore",
|
|
3766
|
+
"pipe",
|
|
3767
|
+
"pipe"
|
|
3768
|
+
]
|
|
3769
|
+
});
|
|
3770
|
+
let stdout = "";
|
|
3771
|
+
let stderr = "";
|
|
3772
|
+
child.stdout.setEncoding("utf8");
|
|
3773
|
+
child.stderr.setEncoding("utf8");
|
|
3774
|
+
child.stdout.on("data", (chunk) => {
|
|
3775
|
+
stdout += chunk;
|
|
3776
|
+
});
|
|
3777
|
+
child.stderr.on("data", (chunk) => {
|
|
3778
|
+
stderr += chunk;
|
|
3779
|
+
});
|
|
3780
|
+
child.on("error", reject);
|
|
3781
|
+
child.on("close", (code, signal) => {
|
|
3782
|
+
if (code !== 0) {
|
|
3783
|
+
const detail = stderr.trim() || signal || `exit code ${code}`;
|
|
3784
|
+
reject(/* @__PURE__ */ new Error(`Fresh ERD export failed: ${detail}`));
|
|
3785
|
+
return;
|
|
3786
|
+
}
|
|
3787
|
+
try {
|
|
3788
|
+
resolve(parseFreshErdExportResults(stdout));
|
|
3789
|
+
} catch (error) {
|
|
3790
|
+
reject(error);
|
|
3791
|
+
}
|
|
3792
|
+
});
|
|
3793
|
+
});
|
|
3794
|
+
}
|
|
3795
|
+
function selectPrimaryResult(results) {
|
|
3535
3796
|
const [primary, ...rest] = results;
|
|
3797
|
+
if (!primary) throw new Error("No ERD build results found.");
|
|
3536
3798
|
logger.info(`Serving ERD for namespace "${primary.namespace}".`);
|
|
3537
3799
|
if (rest.length > 0) {
|
|
3538
3800
|
const commands = rest.map((result) => ` - ${formatServeCommand(result.namespace)}`).join("\n");
|
|
3539
3801
|
logger.warn(`Multiple namespaces found. To serve another namespace, run:\n${commands}`);
|
|
3540
3802
|
}
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3803
|
+
return primary;
|
|
3804
|
+
}
|
|
3805
|
+
async function createErdWatcher(options) {
|
|
3806
|
+
let rebuilding = false;
|
|
3807
|
+
let pending = false;
|
|
3808
|
+
let watchPaths = await resolveWatchPaths(options.initialContext, options.initialResults);
|
|
3809
|
+
let importNonce = 0;
|
|
3810
|
+
const watcher = watch(watchPaths, {
|
|
3811
|
+
ignored: /node_modules/,
|
|
3812
|
+
ignoreInitial: true,
|
|
3813
|
+
awaitWriteFinish: {
|
|
3814
|
+
stabilityThreshold: 100,
|
|
3815
|
+
pollInterval: 100
|
|
3816
|
+
}
|
|
3817
|
+
});
|
|
3818
|
+
async function rebuild() {
|
|
3819
|
+
if (rebuilding) {
|
|
3820
|
+
pending = true;
|
|
3821
|
+
return;
|
|
3822
|
+
}
|
|
3823
|
+
rebuilding = true;
|
|
3544
3824
|
try {
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3825
|
+
const results = await runFreshErdExport({
|
|
3826
|
+
configPath: options.configPath,
|
|
3827
|
+
namespace: options.namespace,
|
|
3828
|
+
outputDir: options.outputDir
|
|
3548
3829
|
});
|
|
3830
|
+
const { config } = await loadConfig(options.configPath, { importNonce: String(importNonce += 1) });
|
|
3831
|
+
const nextWatchPaths = await resolveWatchPathsFromConfig(config, results);
|
|
3832
|
+
watcher.unwatch(watchPaths);
|
|
3833
|
+
watcher.add(nextWatchPaths);
|
|
3834
|
+
watchPaths = nextWatchPaths;
|
|
3835
|
+
logger.success(`Rebuilt ERD schema (${results.map((result) => result.namespace).join(", ")})`, { mode: "stream" });
|
|
3549
3836
|
} catch (error) {
|
|
3837
|
+
logger.error("Failed to rebuild ERD schema. Serving the last successful build.", { mode: "stream" });
|
|
3550
3838
|
logger.error(String(error));
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
cwd: primary.erdDir
|
|
3557
|
-
});
|
|
3558
|
-
child.on("error", (error) => {
|
|
3559
|
-
logger.error("Failed to run `serve dist`.");
|
|
3560
|
-
reject(error);
|
|
3561
|
-
});
|
|
3562
|
-
child.on("exit", (code) => {
|
|
3563
|
-
if (code === 0) resolve();
|
|
3564
|
-
else {
|
|
3565
|
-
logger.error("serve CLI exited with a non-zero code. Ensure `serve dist` works in your project.");
|
|
3566
|
-
reject(/* @__PURE__ */ new Error(`serve CLI exited with code ${code ?? 1}`));
|
|
3839
|
+
} finally {
|
|
3840
|
+
rebuilding = false;
|
|
3841
|
+
if (pending) {
|
|
3842
|
+
pending = false;
|
|
3843
|
+
await rebuild();
|
|
3567
3844
|
}
|
|
3568
|
-
}
|
|
3845
|
+
}
|
|
3846
|
+
}
|
|
3847
|
+
let debounceTimer;
|
|
3848
|
+
const scheduleRebuild = (changedPath) => {
|
|
3849
|
+
logger.info(`Schema source changed: ${path.relative(process.cwd(), changedPath)}`, { mode: "stream" });
|
|
3850
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
3851
|
+
debounceTimer = setTimeout(() => {
|
|
3852
|
+
rebuild();
|
|
3853
|
+
}, 150);
|
|
3854
|
+
};
|
|
3855
|
+
watcher.on("add", scheduleRebuild);
|
|
3856
|
+
watcher.on("change", scheduleRebuild);
|
|
3857
|
+
watcher.on("unlink", scheduleRebuild);
|
|
3858
|
+
watcher.on("error", (error) => {
|
|
3859
|
+
logger.error(`ERD watcher error: ${String(error)}`, { mode: "stream" });
|
|
3860
|
+
});
|
|
3861
|
+
return watcher;
|
|
3862
|
+
}
|
|
3863
|
+
async function waitForShutdown(server, watcher) {
|
|
3864
|
+
return await new Promise((resolve) => {
|
|
3865
|
+
const shutdown = () => {
|
|
3866
|
+
watcher.close().finally(() => {
|
|
3867
|
+
server.close(() => {
|
|
3868
|
+
logger.info("ERD server stopped.");
|
|
3869
|
+
resolve();
|
|
3870
|
+
});
|
|
3871
|
+
});
|
|
3872
|
+
};
|
|
3873
|
+
process.once("SIGINT", shutdown);
|
|
3874
|
+
process.once("SIGTERM", shutdown);
|
|
3569
3875
|
});
|
|
3570
3876
|
}
|
|
3571
3877
|
const erdServeCommand = defineAppCommand({
|
|
3572
3878
|
name: "serve",
|
|
3573
|
-
description: "Generate and serve ERD locally
|
|
3879
|
+
description: "Generate and serve TailorDB ERD locally with watch reload. (beta)",
|
|
3574
3880
|
args: z.object({
|
|
3575
|
-
...
|
|
3881
|
+
...configArg,
|
|
3576
3882
|
namespace: arg(z.string().optional(), {
|
|
3577
3883
|
alias: "n",
|
|
3578
3884
|
description: "TailorDB namespace name (uses first namespace in config if not specified)"
|
|
3579
|
-
})
|
|
3885
|
+
}),
|
|
3886
|
+
port: arg(z.coerce.number().int().min(0).max(65535).default(0), { description: "Local server port (0 selects a free port)" }),
|
|
3887
|
+
open: arg(z.boolean().default(false), { description: "Open the ERD viewer in the default browser" })
|
|
3580
3888
|
}).strict(),
|
|
3581
3889
|
run: async (args) => {
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3890
|
+
initErdCommand();
|
|
3891
|
+
const outputDir = path.resolve(process.cwd(), DEFAULT_ERD_BASE_DIR);
|
|
3892
|
+
const context = await loadLocalErdSchema({
|
|
3893
|
+
configPath: args.config,
|
|
3894
|
+
namespaces: args.namespace ? [args.namespace] : void 0
|
|
3895
|
+
});
|
|
3896
|
+
const results = prepareErdBuildsFromContext({
|
|
3897
|
+
context,
|
|
3898
|
+
namespace: args.namespace,
|
|
3899
|
+
outputDir
|
|
3900
|
+
});
|
|
3901
|
+
const primary = selectPrimaryResult(results);
|
|
3902
|
+
const { server, url } = await startStaticServer({
|
|
3903
|
+
distDir: primary.distDir,
|
|
3904
|
+
port: args.port
|
|
3905
|
+
});
|
|
3906
|
+
const watchUrl = `${url}/?watch=1`;
|
|
3907
|
+
const watcher = await createErdWatcher({
|
|
3908
|
+
configPath: args.config,
|
|
3909
|
+
namespace: args.namespace,
|
|
3910
|
+
outputDir,
|
|
3911
|
+
initialContext: context,
|
|
3912
|
+
initialResults: results
|
|
3913
|
+
});
|
|
3914
|
+
logger.newline();
|
|
3915
|
+
if (args.json) logger.out({
|
|
3916
|
+
namespace: primary.namespace,
|
|
3917
|
+
url: watchUrl,
|
|
3918
|
+
distDir: primary.distDir
|
|
3919
|
+
});
|
|
3920
|
+
else {
|
|
3921
|
+
logger.success("ERD server started.");
|
|
3922
|
+
logger.out(watchUrl);
|
|
3923
|
+
}
|
|
3924
|
+
if (args.open) try {
|
|
3925
|
+
await open(watchUrl);
|
|
3926
|
+
} catch {
|
|
3927
|
+
logger.warn("Failed to open browser automatically. Please open the URL above manually.");
|
|
3928
|
+
}
|
|
3929
|
+
await waitForShutdown(server, watcher);
|
|
3589
3930
|
}
|
|
3590
3931
|
});
|
|
3591
3932
|
|
|
@@ -3593,7 +3934,7 @@ const erdServeCommand = defineAppCommand({
|
|
|
3593
3934
|
//#region src/cli/commands/tailordb/erd/index.ts
|
|
3594
3935
|
const erdCommand = defineCommand({
|
|
3595
3936
|
name: "erd",
|
|
3596
|
-
description: "Generate ERD artifacts
|
|
3937
|
+
description: "Generate TailorDB ERD viewer artifacts from local TailorDB schema. (beta)",
|
|
3597
3938
|
subCommands: {
|
|
3598
3939
|
export: erdExportCommand,
|
|
3599
3940
|
serve: erdServeCommand,
|