@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.
@@ -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
- if (KNOWN_TYPE_FACTORIES[typeName]) {
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/fullstack-cli",
3
- "version": "1.1.19",
3
+ "version": "1.1.20",
4
4
  "description": "CLI tool for fullstack template management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",