@lark-apaas/fullstack-cli 1.1.19 → 1.1.20
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/dist/gen-dbschema-template/types.ts +54 -0
- package/dist/index.js +177 -7
- package/package.json +1 -1
|
@@ -38,6 +38,60 @@ export const fileAttachment = customType<{
|
|
|
38
38
|
},
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
+
/** Escape single quotes in SQL string literals */
|
|
42
|
+
function escapeLiteral(str: string): string {
|
|
43
|
+
return `'${str.replace(/'/g, "''")}'`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const userProfileArray = customType<{
|
|
47
|
+
data: string[];
|
|
48
|
+
driverData: string;
|
|
49
|
+
}>({
|
|
50
|
+
dataType() {
|
|
51
|
+
return 'user_profile[]';
|
|
52
|
+
},
|
|
53
|
+
toDriver(value: string[]) {
|
|
54
|
+
if (!value || value.length === 0) {
|
|
55
|
+
return sql`'{}'::user_profile[]`;
|
|
56
|
+
}
|
|
57
|
+
const elements = value.map(id => `ROW(${escapeLiteral(id)})::user_profile`).join(',');
|
|
58
|
+
return sql.raw(`ARRAY[${elements}]::user_profile[]`);
|
|
59
|
+
},
|
|
60
|
+
fromDriver(value: string): string[] {
|
|
61
|
+
if (!value || value === '{}') return [];
|
|
62
|
+
const inner = value.slice(1, -1);
|
|
63
|
+
const matches = inner.match(/\([^)]*\)/g) || [];
|
|
64
|
+
return matches.map(m => m.slice(1, -1).split(',')[0].trim());
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
export const fileAttachmentArray = customType<{
|
|
69
|
+
data: FileAttachment[];
|
|
70
|
+
driverData: string;
|
|
71
|
+
}>({
|
|
72
|
+
dataType() {
|
|
73
|
+
return 'file_attachment[]';
|
|
74
|
+
},
|
|
75
|
+
toDriver(value: FileAttachment[]) {
|
|
76
|
+
if (!value || value.length === 0) {
|
|
77
|
+
return sql`'{}'::file_attachment[]`;
|
|
78
|
+
}
|
|
79
|
+
const elements = value.map(f =>
|
|
80
|
+
`ROW(${escapeLiteral(f.bucket_id)},${escapeLiteral(f.file_path)})::file_attachment`
|
|
81
|
+
).join(',');
|
|
82
|
+
return sql.raw(`ARRAY[${elements}]::file_attachment[]`);
|
|
83
|
+
},
|
|
84
|
+
fromDriver(value: string): FileAttachment[] {
|
|
85
|
+
if (!value || value === '{}') return [];
|
|
86
|
+
const inner = value.slice(1, -1);
|
|
87
|
+
const matches = inner.match(/\([^)]*\)/g) || [];
|
|
88
|
+
return matches.map(m => {
|
|
89
|
+
const [bucketId, filePath] = m.slice(1, -1).split(',');
|
|
90
|
+
return { bucket_id: bucketId.trim(), file_path: filePath.trim() };
|
|
91
|
+
});
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
41
95
|
export const customTimestamptz = customType<{
|
|
42
96
|
data: Date;
|
|
43
97
|
driverData: string;
|
package/dist/index.js
CHANGED
|
@@ -472,6 +472,10 @@ var KNOWN_TYPE_FACTORIES = {
|
|
|
472
472
|
user_profile: "userProfile",
|
|
473
473
|
file_attachment: "fileAttachment"
|
|
474
474
|
};
|
|
475
|
+
var KNOWN_ARRAY_TYPE_FACTORIES = {
|
|
476
|
+
user_profile: "userProfileArray",
|
|
477
|
+
file_attachment: "fileAttachmentArray"
|
|
478
|
+
};
|
|
475
479
|
var replaceUnknownTransform = {
|
|
476
480
|
name: "replace-unknown",
|
|
477
481
|
transform(ctx) {
|
|
@@ -490,12 +494,17 @@ var replaceUnknownTransform = {
|
|
|
490
494
|
const lines = textBefore.split("\n");
|
|
491
495
|
let factoryName = "text";
|
|
492
496
|
let foundKnownType = false;
|
|
497
|
+
let isArrayType = false;
|
|
493
498
|
for (let i = lines.length - 1; i >= Math.max(0, lines.length - 5); i--) {
|
|
494
499
|
const line = lines[i];
|
|
495
500
|
const todoMatch = line.match(/\/\/ TODO: failed to parse database type '(?:\w+\.)?([\w_]+)(\[\])?'/);
|
|
496
501
|
if (todoMatch) {
|
|
497
502
|
const typeName = todoMatch[1];
|
|
498
|
-
|
|
503
|
+
isArrayType = todoMatch[2] === "[]";
|
|
504
|
+
if (isArrayType && KNOWN_ARRAY_TYPE_FACTORIES[typeName]) {
|
|
505
|
+
factoryName = KNOWN_ARRAY_TYPE_FACTORIES[typeName];
|
|
506
|
+
foundKnownType = true;
|
|
507
|
+
} else if (KNOWN_TYPE_FACTORIES[typeName]) {
|
|
499
508
|
factoryName = KNOWN_TYPE_FACTORIES[typeName];
|
|
500
509
|
foundKnownType = true;
|
|
501
510
|
}
|
|
@@ -503,6 +512,15 @@ var replaceUnknownTransform = {
|
|
|
503
512
|
}
|
|
504
513
|
}
|
|
505
514
|
expression.replaceWithText(factoryName);
|
|
515
|
+
if (isArrayType && foundKnownType) {
|
|
516
|
+
const parent = node.getParent();
|
|
517
|
+
if (Node5.isPropertyAccessExpression(parent) && parent.getName() === "array") {
|
|
518
|
+
const grandParent = parent.getParent();
|
|
519
|
+
if (Node5.isCallExpression(grandParent)) {
|
|
520
|
+
grandParent.replaceWithText(node.getText());
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
506
524
|
if (foundKnownType) {
|
|
507
525
|
stats.replacedUnknown++;
|
|
508
526
|
} else {
|
|
@@ -1005,6 +1023,139 @@ function resolveTemplateTypesPath() {
|
|
|
1005
1023
|
return void 0;
|
|
1006
1024
|
}
|
|
1007
1025
|
|
|
1026
|
+
// src/commands/db/gen-dbschema/utils/fetch-column-comments.ts
|
|
1027
|
+
import postgres from "postgres";
|
|
1028
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
1029
|
+
async function fetchColumnComments(connectionString, options = {}) {
|
|
1030
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
1031
|
+
const url = new URL(connectionString);
|
|
1032
|
+
const schemaName = url.searchParams.get("schema") ?? "public";
|
|
1033
|
+
const sql = postgres(connectionString, {
|
|
1034
|
+
connect_timeout: Math.ceil(timeoutMs / 1e3),
|
|
1035
|
+
idle_timeout: Math.ceil(timeoutMs / 1e3)
|
|
1036
|
+
});
|
|
1037
|
+
try {
|
|
1038
|
+
const queryPromise = sql`
|
|
1039
|
+
SELECT
|
|
1040
|
+
c.table_name AS "tableName",
|
|
1041
|
+
c.column_name AS "columnName",
|
|
1042
|
+
pgd.description AS "comment"
|
|
1043
|
+
FROM information_schema.columns c
|
|
1044
|
+
JOIN pg_catalog.pg_statio_all_tables st
|
|
1045
|
+
ON c.table_name = st.relname AND c.table_schema = st.schemaname
|
|
1046
|
+
JOIN pg_catalog.pg_description pgd
|
|
1047
|
+
ON pgd.objoid = st.relid AND pgd.objsubid = c.ordinal_position
|
|
1048
|
+
WHERE c.table_schema = ${schemaName}
|
|
1049
|
+
AND pgd.description IS NOT NULL
|
|
1050
|
+
AND pgd.description != ''
|
|
1051
|
+
`;
|
|
1052
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
1053
|
+
setTimeout(() => reject(new Error(`Timeout after ${timeoutMs}ms`)), timeoutMs);
|
|
1054
|
+
});
|
|
1055
|
+
const result = await Promise.race([queryPromise, timeoutPromise]);
|
|
1056
|
+
const commentMap = /* @__PURE__ */ new Map();
|
|
1057
|
+
for (const row of result) {
|
|
1058
|
+
const key = `${row.tableName}.${row.columnName}`;
|
|
1059
|
+
commentMap.set(key, row.comment);
|
|
1060
|
+
}
|
|
1061
|
+
return commentMap;
|
|
1062
|
+
} finally {
|
|
1063
|
+
await sql.end().catch(() => {
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
function extractTypeAnnotation(comment) {
|
|
1068
|
+
const typeStart = comment.indexOf("@type");
|
|
1069
|
+
if (typeStart === -1) return null;
|
|
1070
|
+
const afterType = comment.slice(typeStart + 5).trimStart();
|
|
1071
|
+
if (!afterType.startsWith("{")) return null;
|
|
1072
|
+
let depth = 0;
|
|
1073
|
+
let endIndex = 0;
|
|
1074
|
+
for (let i = 0; i < afterType.length; i++) {
|
|
1075
|
+
if (afterType[i] === "{") depth++;
|
|
1076
|
+
if (afterType[i] === "}") depth--;
|
|
1077
|
+
if (depth === 0) {
|
|
1078
|
+
endIndex = i + 1;
|
|
1079
|
+
break;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
if (endIndex === 0) return null;
|
|
1083
|
+
return afterType.slice(0, endIndex);
|
|
1084
|
+
}
|
|
1085
|
+
function parseColumnComment(comment) {
|
|
1086
|
+
const typeValue = extractTypeAnnotation(comment);
|
|
1087
|
+
if (typeValue) {
|
|
1088
|
+
const descMatch = comment.match(/@description\s+([^@]+)/);
|
|
1089
|
+
return {
|
|
1090
|
+
type: typeValue,
|
|
1091
|
+
description: descMatch?.[1]?.trim()
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
return { description: comment.trim() };
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// src/commands/db/gen-dbschema/transforms/text/jsonb-comments.ts
|
|
1098
|
+
function addJsonbTypeComments(source, columnComments) {
|
|
1099
|
+
if (!columnComments || columnComments.size === 0) {
|
|
1100
|
+
return { text: source, added: 0 };
|
|
1101
|
+
}
|
|
1102
|
+
const lines = source.split("\n");
|
|
1103
|
+
const result = [];
|
|
1104
|
+
let added = 0;
|
|
1105
|
+
let currentTableName = null;
|
|
1106
|
+
const tableDefRegex = /export const\s+\w+\s*=\s*pgTable\(\s*["'`]([^"'`]+)["'`]/;
|
|
1107
|
+
const jsonFieldWithNameRegex = /^\s*(\w+):\s*(?:json|jsonb)\(\s*["'`]([^"'`]+)["'`]\)/;
|
|
1108
|
+
const jsonFieldNoNameRegex = /^\s*(\w+):\s*(?:json|jsonb)\(\s*\)/;
|
|
1109
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1110
|
+
const line = lines[i];
|
|
1111
|
+
const tableMatch = line.match(tableDefRegex);
|
|
1112
|
+
if (tableMatch) {
|
|
1113
|
+
currentTableName = tableMatch[1];
|
|
1114
|
+
}
|
|
1115
|
+
let columnName = null;
|
|
1116
|
+
const jsonMatchWithName = line.match(jsonFieldWithNameRegex);
|
|
1117
|
+
const jsonMatchNoName = line.match(jsonFieldNoNameRegex);
|
|
1118
|
+
if (jsonMatchWithName) {
|
|
1119
|
+
columnName = jsonMatchWithName[2];
|
|
1120
|
+
} else if (jsonMatchNoName) {
|
|
1121
|
+
columnName = jsonMatchNoName[1];
|
|
1122
|
+
}
|
|
1123
|
+
if (columnName && currentTableName) {
|
|
1124
|
+
const commentKey = `${currentTableName}.${columnName}`;
|
|
1125
|
+
const comment = columnComments.get(commentKey);
|
|
1126
|
+
if (comment) {
|
|
1127
|
+
const parsed = parseColumnComment(comment);
|
|
1128
|
+
const indentMatch = line.match(/^\s*/);
|
|
1129
|
+
const indent = indentMatch ? indentMatch[0] : "";
|
|
1130
|
+
const prevLine = result[result.length - 1]?.trim() ?? "";
|
|
1131
|
+
if (!prevLine.startsWith("/**") && !prevLine.startsWith("*") && !prevLine.startsWith("//")) {
|
|
1132
|
+
const commentLines = [];
|
|
1133
|
+
commentLines.push(`${indent}/**`);
|
|
1134
|
+
if (parsed.description) {
|
|
1135
|
+
const safeDesc = parsed.description.replace(/[\r\n]+/g, " ").trim();
|
|
1136
|
+
commentLines.push(`${indent} * ${safeDesc}`);
|
|
1137
|
+
}
|
|
1138
|
+
if (parsed.type) {
|
|
1139
|
+
if (parsed.description) {
|
|
1140
|
+
commentLines.push(`${indent} *`);
|
|
1141
|
+
}
|
|
1142
|
+
const safeType = parsed.type.replace(/[\r\n]+/g, " ").trim();
|
|
1143
|
+
commentLines.push(`${indent} * @type ${safeType}`);
|
|
1144
|
+
}
|
|
1145
|
+
commentLines.push(`${indent} */`);
|
|
1146
|
+
result.push(...commentLines);
|
|
1147
|
+
added++;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
if (line.match(/^\}\);?\s*$/) || line.match(/^}\s*,\s*\{/)) {
|
|
1152
|
+
currentTableName = null;
|
|
1153
|
+
}
|
|
1154
|
+
result.push(line);
|
|
1155
|
+
}
|
|
1156
|
+
return { text: result.join("\n"), added };
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1008
1159
|
// src/commands/db/gen-dbschema/transforms/text/table-aliases.ts
|
|
1009
1160
|
var TABLE_ALIAS_MARKER = "// table aliases";
|
|
1010
1161
|
function generateTableAliases(source) {
|
|
@@ -1041,7 +1192,7 @@ function formatSource(source) {
|
|
|
1041
1192
|
}
|
|
1042
1193
|
|
|
1043
1194
|
// src/commands/db/gen-dbschema/postprocess.ts
|
|
1044
|
-
function postprocessSchema(rawSource) {
|
|
1195
|
+
function postprocessSchema(rawSource, options = {}) {
|
|
1045
1196
|
const patchResult = patchDefects(rawSource);
|
|
1046
1197
|
let source = patchResult.text;
|
|
1047
1198
|
const { sourceFile } = parseSource(source);
|
|
@@ -1051,12 +1202,15 @@ function postprocessSchema(rawSource) {
|
|
|
1051
1202
|
source = ensureHeader(source);
|
|
1052
1203
|
source = addSystemFieldComments(source);
|
|
1053
1204
|
source = inlineCustomTypes(source);
|
|
1205
|
+
const jsonbCommentsResult = addJsonbTypeComments(source, options.columnComments);
|
|
1206
|
+
source = jsonbCommentsResult.text;
|
|
1054
1207
|
source = generateTableAliases(source);
|
|
1055
1208
|
source = formatSource(source);
|
|
1056
1209
|
return {
|
|
1057
1210
|
source,
|
|
1058
1211
|
astStats,
|
|
1059
|
-
patchedDefects: patchResult.fixed
|
|
1212
|
+
patchedDefects: patchResult.fixed,
|
|
1213
|
+
addedJsonbComments: jsonbCommentsResult.added
|
|
1060
1214
|
};
|
|
1061
1215
|
}
|
|
1062
1216
|
function logStats(result, prefix = "[postprocess]") {
|
|
@@ -1103,17 +1257,20 @@ function logStats(result, prefix = "[postprocess]") {
|
|
|
1103
1257
|
if (astStats.removedImports.length > 0) {
|
|
1104
1258
|
console.info(`${prefix} Removed imports: ${astStats.removedImports.join(", ")}`);
|
|
1105
1259
|
}
|
|
1260
|
+
if (result.addedJsonbComments > 0) {
|
|
1261
|
+
console.info(`${prefix} Added ${result.addedJsonbComments} JSDoc comments for jsonb fields`);
|
|
1262
|
+
}
|
|
1106
1263
|
}
|
|
1107
1264
|
|
|
1108
1265
|
// src/commands/db/gen-dbschema/index.ts
|
|
1109
|
-
function postprocessDrizzleSchema(targetPath) {
|
|
1266
|
+
async function postprocessDrizzleSchema(targetPath, options = {}) {
|
|
1110
1267
|
const resolvedPath = path.resolve(targetPath);
|
|
1111
1268
|
if (!fs3.existsSync(resolvedPath)) {
|
|
1112
1269
|
console.warn(`[postprocess-drizzle-schema] File not found: ${resolvedPath}`);
|
|
1113
1270
|
return void 0;
|
|
1114
1271
|
}
|
|
1115
1272
|
const rawSource = fs3.readFileSync(resolvedPath, "utf8");
|
|
1116
|
-
const result = postprocessSchema(rawSource);
|
|
1273
|
+
const result = postprocessSchema(rawSource, { columnComments: options.columnComments });
|
|
1117
1274
|
fs3.writeFileSync(resolvedPath, result.source, "utf8");
|
|
1118
1275
|
logStats(result, "[postprocess-drizzle-schema]");
|
|
1119
1276
|
return {
|
|
@@ -1122,7 +1279,8 @@ function postprocessDrizzleSchema(targetPath) {
|
|
|
1122
1279
|
unmatchedUnknown: result.astStats.unmatchedUnknown,
|
|
1123
1280
|
patchedDefects: result.patchedDefects,
|
|
1124
1281
|
replacedTimestamps: result.astStats.replacedTimestamp,
|
|
1125
|
-
replacedDefaultNow: result.astStats.replacedDefaultNow
|
|
1282
|
+
replacedDefaultNow: result.astStats.replacedDefaultNow,
|
|
1283
|
+
addedJsonbComments: result.addedJsonbComments
|
|
1126
1284
|
};
|
|
1127
1285
|
}
|
|
1128
1286
|
|
|
@@ -1847,6 +2005,16 @@ async function run(options = {}) {
|
|
|
1847
2005
|
}
|
|
1848
2006
|
throw new Error("Unable to locate drizzle-kit package root");
|
|
1849
2007
|
};
|
|
2008
|
+
let columnComments;
|
|
2009
|
+
try {
|
|
2010
|
+
columnComments = await fetchColumnComments(databaseUrl, { timeoutMs: 1e4 });
|
|
2011
|
+
console.log(`[gen-db-schema] \u2713 Fetched ${columnComments.size} column comments`);
|
|
2012
|
+
} catch (err) {
|
|
2013
|
+
console.warn(
|
|
2014
|
+
"[gen-db-schema] \u26A0 Failed to fetch column comments (skipping):",
|
|
2015
|
+
err instanceof Error ? err.message : String(err)
|
|
2016
|
+
);
|
|
2017
|
+
}
|
|
1850
2018
|
try {
|
|
1851
2019
|
const env = {
|
|
1852
2020
|
...process.env,
|
|
@@ -1870,7 +2038,9 @@ async function run(options = {}) {
|
|
|
1870
2038
|
console.error("[gen-db-schema] schema.ts not generated");
|
|
1871
2039
|
throw new Error("drizzle-kit introspect failed to generate schema.ts");
|
|
1872
2040
|
}
|
|
1873
|
-
const stats = postprocessDrizzleSchema(generatedSchema
|
|
2041
|
+
const stats = await postprocessDrizzleSchema(generatedSchema, {
|
|
2042
|
+
columnComments
|
|
2043
|
+
});
|
|
1874
2044
|
if (stats?.unmatchedUnknown?.length) {
|
|
1875
2045
|
console.warn("[gen-db-schema] Unmatched custom types detected:", stats.unmatchedUnknown);
|
|
1876
2046
|
}
|