create-qs 0.8.23 → 0.8.26
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/index.js +169 -2
- package/package.json +1 -1
- package/template/default/.quicksilver/manual/en-US/00-setup.md +1 -1
- package/template/default/.quicksilver/manual/zh-CN/00-setup.md +1 -1
- package/template/default/.quicksilver/manual/zh-TW/00-setup.md +1 -1
- package/template/default/AGENTS.md +35 -13
- package/template/default/modules/{{MODULE_DIRECTORY}}/api/data/demo/README.md +1 -1
- package/template/default/package.json +1 -0
- package/template/default/pnpm-workspace.yaml +6 -6
- package/template/default/quicksilver.yaml +8 -0
- package/template/default/run/api/config/README.md +19 -7
- package/template/default/run/api/config/application-dev.yaml +6 -5
- package/template/default/run/api/config/application-fresh.yaml +23 -0
- package/template/default/run/api/config/application-h2.yaml +2 -2
package/index.js
CHANGED
|
@@ -894,6 +894,133 @@ var MultiselectPrompt = class {
|
|
|
894
894
|
function multiselect(options) {
|
|
895
895
|
return new MultiselectPrompt(options).run();
|
|
896
896
|
}
|
|
897
|
+
var SelectPrompt = class {
|
|
898
|
+
rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
899
|
+
message;
|
|
900
|
+
items;
|
|
901
|
+
cursor;
|
|
902
|
+
state = "init";
|
|
903
|
+
constructor(options) {
|
|
904
|
+
if (options.items.length === 0) {
|
|
905
|
+
throw new Error("select: items must not be empty");
|
|
906
|
+
}
|
|
907
|
+
this.message = options.message;
|
|
908
|
+
this.items = options.items.map((c) => ({ ...c }));
|
|
909
|
+
const defaultIndex = options.default !== void 0 ? this.items.findIndex((it) => it.value === options.default) : -1;
|
|
910
|
+
this.cursor = defaultIndex >= 0 ? defaultIndex : 0;
|
|
911
|
+
readline.emitKeypressEvents(process.stdin, this.rl);
|
|
912
|
+
if (process.stdin.isTTY) {
|
|
913
|
+
process.stdin.setRawMode(true);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
async run() {
|
|
917
|
+
process.stdout.write(hideCursor());
|
|
918
|
+
this.render();
|
|
919
|
+
return new Promise((resolve3, reject) => {
|
|
920
|
+
let resolved = false;
|
|
921
|
+
const cleanup = () => {
|
|
922
|
+
process.stdin.removeListener("keypress", handleKeypress);
|
|
923
|
+
process.stdin.removeListener("data", handleData);
|
|
924
|
+
};
|
|
925
|
+
const safeResolve = (value) => {
|
|
926
|
+
if (!resolved) {
|
|
927
|
+
resolved = true;
|
|
928
|
+
cleanup();
|
|
929
|
+
resolve3(value);
|
|
930
|
+
}
|
|
931
|
+
};
|
|
932
|
+
const safeReject = (reason) => {
|
|
933
|
+
if (!resolved) {
|
|
934
|
+
resolved = true;
|
|
935
|
+
cleanup();
|
|
936
|
+
reject(reason);
|
|
937
|
+
}
|
|
938
|
+
};
|
|
939
|
+
const handleKeypress = (_str, key) => {
|
|
940
|
+
if (resolved) {
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
if (key.name === "up") {
|
|
944
|
+
this.moveCursor(-1);
|
|
945
|
+
} else if (key.name === "down") {
|
|
946
|
+
this.moveCursor(1);
|
|
947
|
+
} else if (key.name === "return") {
|
|
948
|
+
process.stdout.write(up());
|
|
949
|
+
safeResolve(this.finish());
|
|
950
|
+
} else if (key.ctrl && key.name === "c") {
|
|
951
|
+
this.state = "cancelled";
|
|
952
|
+
this.exit();
|
|
953
|
+
safeReject(new PromptCancelled());
|
|
954
|
+
}
|
|
955
|
+
this.render();
|
|
956
|
+
};
|
|
957
|
+
const handleData = (data) => {
|
|
958
|
+
if (resolved) {
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
const str = data.toString();
|
|
962
|
+
if (str === "\n" || str === "\r" || str === "\r\n") {
|
|
963
|
+
process.stdout.write(down());
|
|
964
|
+
safeResolve(this.finish());
|
|
965
|
+
}
|
|
966
|
+
};
|
|
967
|
+
process.stdin.on("keypress", handleKeypress);
|
|
968
|
+
process.stdin.on("data", handleData);
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
render() {
|
|
972
|
+
const hint = chalk4.dim(promptTexts().selectHelp);
|
|
973
|
+
if (this.state === "init") {
|
|
974
|
+
this.state = "select";
|
|
975
|
+
console.log(chalk4.cyan("? ") + chalk4.bold(this.message) + chalk4.dim(" \u203A"));
|
|
976
|
+
console.log(" " + hint);
|
|
977
|
+
this.writeItems();
|
|
978
|
+
} else if (this.state === "select") {
|
|
979
|
+
process.stdout.write(upStart(this.items.length + 1));
|
|
980
|
+
console.log(chalk4.cyan("? ") + chalk4.bold(this.message) + chalk4.dim(" \u203A"));
|
|
981
|
+
console.log(" " + hint + clearLineFromCursor());
|
|
982
|
+
this.writeItems();
|
|
983
|
+
} else {
|
|
984
|
+
process.stdout.write(down() + upClear(this.items.length + 2));
|
|
985
|
+
console.log(this.state === "cancelled" ? cancelledLine(this.message) : chalk4.green("\u2714 ") + chalk4.bold(this.message) + chalk4.dim(" \xB7 ") + chalk4.green(this.getSelectedItem().name));
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
writeItems() {
|
|
989
|
+
this.items.forEach((item, index) => this.writeItem(item, index));
|
|
990
|
+
process.stdout.write(up());
|
|
991
|
+
}
|
|
992
|
+
writeItem(item, index) {
|
|
993
|
+
const isSelected = index === this.cursor;
|
|
994
|
+
if (isSelected) {
|
|
995
|
+
console.log(` ${chalk4.cyan("\u276F")} ${item.name}`);
|
|
996
|
+
} else {
|
|
997
|
+
console.log(` ${item.name}`);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
moveCursor(direction) {
|
|
1001
|
+
const length = this.items.length;
|
|
1002
|
+
this.cursor = (this.cursor + direction + length) % length;
|
|
1003
|
+
}
|
|
1004
|
+
getSelectedItem() {
|
|
1005
|
+
return this.items[this.cursor];
|
|
1006
|
+
}
|
|
1007
|
+
finish() {
|
|
1008
|
+
this.state = "finished";
|
|
1009
|
+
this.exit();
|
|
1010
|
+
return this.getSelectedItem().value;
|
|
1011
|
+
}
|
|
1012
|
+
exit() {
|
|
1013
|
+
this.rl.close();
|
|
1014
|
+
if (process.stdin.isTTY) {
|
|
1015
|
+
process.stdin.setRawMode(false);
|
|
1016
|
+
}
|
|
1017
|
+
process.stdin.removeAllListeners("keypress");
|
|
1018
|
+
process.stdout.write(showCursor());
|
|
1019
|
+
}
|
|
1020
|
+
};
|
|
1021
|
+
function select(options) {
|
|
1022
|
+
return new SelectPrompt(options).run();
|
|
1023
|
+
}
|
|
897
1024
|
var INDENT = " ";
|
|
898
1025
|
var MAX_COLUMNS = 100;
|
|
899
1026
|
function renderCode(line, color = chalk4.cyan) {
|
|
@@ -998,6 +1125,11 @@ var HINTS = {
|
|
|
998
1125
|
"zh-TW": "\u672C\u7522\u54C1\u76F8\u4F9D\u7684\u6A21\u7D44\uFF0C\u5F8C\u7E8C\u53EF\u518D\u884C\u65B0\u589E\u3002",
|
|
999
1126
|
"en-US": "Modules this product depends on. More can be added later."
|
|
1000
1127
|
},
|
|
1128
|
+
jsonsLocale: {
|
|
1129
|
+
"zh-CN": "\u6570\u636E\u6587\u4EF6\u4E2D\u663E\u793A\u6587\u672C\u7684\u8BED\u8A00\uFF0C\u5373\u5355\u5143\u540D\u3001\u83DC\u5355\u3001\u6587\u6848\u7B49\u7684\u4E66\u5199\u8BED\u8A00\u3002\u5B89\u88C5\u65F6\u53EF\u4EE5\u53E6\u9009\u4EA7\u54C1\u8BED\u8A00\uFF0C\u7FFB\u8BD1\u8868\u63D0\u4F9B\u5176\u5B83\u8BED\u8A00\u3002\u26A0 \u66F4\u6539\u8981\u91CD\u5199\u5168\u90E8\u6570\u636E\u6587\u4EF6\u4E0E\u7FFB\u8BD1\u8868\u3002",
|
|
1130
|
+
"zh-TW": "\u8CC7\u6599\u6A94\u4E2D\u986F\u793A\u6587\u5B57\u7684\u8A9E\u8A00\uFF0C\u5373\u55AE\u5143\u540D\u3001\u9078\u55AE\u3001\u6587\u6848\u7B49\u7684\u66F8\u5BEB\u8A9E\u8A00\u3002\u5B89\u88DD\u6642\u53EF\u4EE5\u53E6\u9078\u7522\u54C1\u8A9E\u8A00\uFF0C\u7FFB\u8B6F\u8868\u63D0\u4F9B\u5176\u4ED6\u8A9E\u8A00\u3002\u26A0 \u66F4\u6539\u8981\u91CD\u5BEB\u5168\u90E8\u8CC7\u6599\u6A94\u8207\u7FFB\u8B6F\u8868\u3002",
|
|
1131
|
+
"en-US": "The language of the display texts in data files: unit names, menus, messages. Installations may pick another product language, and translation tables supply the others. \u26A0 Changing it means rewriting every data file and translation table."
|
|
1132
|
+
},
|
|
1001
1133
|
moduleMenu: {
|
|
1002
1134
|
"zh-CN": "\u672C\u6A21\u5757\u5728\u5DE6\u4FA7\u5BFC\u822A\u4E2D\u7684\u4E00\u7EA7\u5165\u53E3\uFF0C\u4E0E\u5E73\u53F0\u5185\u7F6E\u7684\u5165\u53E3\u5E76\u5217\u3002\u540E\u7EED\u521B\u5EFA\u7684\u9875\u9762\u53EF\u6302\u8F7D\u4E8E\u5176\u4E0B\uFF0C\u540D\u79F0\u4E0E\u5B58\u5E9F\u5747\u53EF\u968F\u65F6\u8C03\u6574\u3002",
|
|
1003
1135
|
"zh-TW": "\u672C\u6A21\u7D44\u5728\u5DE6\u5074\u5C0E\u89BD\u5217\u4E2D\u7684\u4E00\u7D1A\u5165\u53E3\uFF0C\u8207\u5E73\u53F0\u5167\u5EFA\u7684\u5165\u53E3\u4E26\u5217\u3002\u5F8C\u7E8C\u5EFA\u7ACB\u7684\u9801\u9762\u53EF\u639B\u8F09\u65BC\u5176\u4E0B\uFF0C\u540D\u7A31\u8207\u5B58\u5EE2\u5747\u53EF\u96A8\u6642\u8ABF\u6574\u3002",
|
|
@@ -1033,6 +1165,7 @@ var TEXTS = {
|
|
|
1033
1165
|
"zh-TW": "\u70BA\u672C\u6A21\u7D44\u5EFA\u7ACB\u9802\u5C64\u9078\u55AE",
|
|
1034
1166
|
"en-US": "Create a top-level menu for this module"
|
|
1035
1167
|
},
|
|
1168
|
+
"label.jsonsLocale": { "zh-CN": "\u6570\u636E\u6587\u4EF6\u7684\u7F16\u5199\u8BED\u8A00", "zh-TW": "\u8CC7\u6599\u6A94\u7684\u66F8\u5BEB\u8A9E\u8A00", "en-US": "Language of data files" },
|
|
1036
1169
|
"label.moduleMenuName": { "zh-CN": "\u9876\u5C42\u83DC\u5355\u540D\u79F0", "zh-TW": "\u9802\u5C64\u9078\u55AE\u540D\u7A31", "en-US": "Top-level menu name" },
|
|
1037
1170
|
// 校验
|
|
1038
1171
|
"validate.empty": { "zh-CN": "{0}\u4E0D\u80FD\u4E3A\u7A7A", "zh-TW": "{0}\u4E0D\u80FD\u70BA\u7A7A", "en-US": "{0} must be a non-empty string" },
|
|
@@ -1211,6 +1344,17 @@ function printHint2(step) {
|
|
|
1211
1344
|
printHint(HINTS[step][language]);
|
|
1212
1345
|
}
|
|
1213
1346
|
|
|
1347
|
+
// src/collect/jsons-locale.ts
|
|
1348
|
+
var JSONS_LOCALES = ["zh-CN", "zh-TW"];
|
|
1349
|
+
var JSONS_LOCALE_NAMES = {
|
|
1350
|
+
"zh-CN": "\u7B80\u4F53\u4E2D\u6587",
|
|
1351
|
+
"zh-TW": "\u7E41\u9AD4\u4E2D\u6587"
|
|
1352
|
+
};
|
|
1353
|
+
function orderJsonsLocales(uiLanguage) {
|
|
1354
|
+
const preferred = uiLanguage === "zh-TW" ? "zh-TW" : "zh-CN";
|
|
1355
|
+
return [preferred, ...JSONS_LOCALES.filter((locale) => locale !== preferred)];
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1214
1358
|
// src/collect/summary.ts
|
|
1215
1359
|
function row(step, value) {
|
|
1216
1360
|
return { label: t(`label.${step}`), value };
|
|
@@ -1229,6 +1373,7 @@ function summaryRows(options) {
|
|
|
1229
1373
|
row("apiArtifactId", options.apiArtifactId),
|
|
1230
1374
|
row("webModulePackage", options.webModulePackage),
|
|
1231
1375
|
row("platformModules", options.platformModules.join(", ")),
|
|
1376
|
+
row("jsonsLocale", JSONS_LOCALE_NAMES[options.jsonsLocale] ?? options.jsonsLocale),
|
|
1232
1377
|
{ label: t("label.moduleMenuName"), value: options.moduleMenu ?? t("summary.noMenu") }
|
|
1233
1378
|
];
|
|
1234
1379
|
}
|
|
@@ -1422,6 +1567,7 @@ var FLAGS = {
|
|
|
1422
1567
|
apiArtifactId: "--api-artifact-id",
|
|
1423
1568
|
webModulePackage: "--web-module-package",
|
|
1424
1569
|
platformModules: "--modules",
|
|
1570
|
+
jsonsLocale: "--jsons-locale",
|
|
1425
1571
|
moduleMenu: "--module-menu"
|
|
1426
1572
|
};
|
|
1427
1573
|
async function ask(args, step, options) {
|
|
@@ -1537,6 +1683,25 @@ async function collectPlatformModules(args) {
|
|
|
1537
1683
|
const codes = await multiselect({ message: t("label.platformModules"), items });
|
|
1538
1684
|
return resolvePlatformModules(codes).map((module) => module.code);
|
|
1539
1685
|
}
|
|
1686
|
+
async function collectJsonsLocale(args) {
|
|
1687
|
+
const ordered = orderJsonsLocales(language);
|
|
1688
|
+
if (args.jsonsLocale !== void 0) {
|
|
1689
|
+
const value = args.jsonsLocale.trim();
|
|
1690
|
+
if (!JSONS_LOCALES.includes(value)) {
|
|
1691
|
+
throw new UsageError(`--jsons-locale must be one of ${JSONS_LOCALES.join(", ")}, got '${args.jsonsLocale}'`);
|
|
1692
|
+
}
|
|
1693
|
+
return value;
|
|
1694
|
+
}
|
|
1695
|
+
if (args.yes) {
|
|
1696
|
+
return ordered[0];
|
|
1697
|
+
}
|
|
1698
|
+
printHint2("jsonsLocale");
|
|
1699
|
+
return select({
|
|
1700
|
+
message: t("label.jsonsLocale"),
|
|
1701
|
+
default: ordered[0],
|
|
1702
|
+
items: ordered.map((locale) => ({ name: JSONS_LOCALE_NAMES[locale], value: locale }))
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1540
1705
|
async function collectModuleMenu(args, options) {
|
|
1541
1706
|
if (args.moduleMenu === false) {
|
|
1542
1707
|
return void 0;
|
|
@@ -1584,6 +1749,7 @@ async function collectProjectOptions(args) {
|
|
|
1584
1749
|
options.apiArtifactId = await collectApiArtifactId(args, options);
|
|
1585
1750
|
options.webModulePackage = await collectWebModulePackage(args, options);
|
|
1586
1751
|
options.platformModules = await collectPlatformModules(args);
|
|
1752
|
+
options.jsonsLocale = await collectJsonsLocale(args);
|
|
1587
1753
|
options.moduleMenu = await collectModuleMenu(args, options);
|
|
1588
1754
|
options.template = "default";
|
|
1589
1755
|
printSummary(t("summary.title"), summaryRows(options));
|
|
@@ -1804,9 +1970,10 @@ async function processFiles(options) {
|
|
|
1804
1970
|
variables.set("WEB_PACKAGE_NAME", options.webModulePackage);
|
|
1805
1971
|
variables.set("API_PACKAGE", options.apiPackage);
|
|
1806
1972
|
variables.set("API_ARTIFACT_ID", options.apiArtifactId);
|
|
1807
|
-
variables.set("QUICKSILVER_VERSION", "0.8.
|
|
1973
|
+
variables.set("QUICKSILVER_VERSION", "0.8.26");
|
|
1808
1974
|
variables.set("MODULE_ID", randomUUID());
|
|
1809
1975
|
variables.set("DB_PREFIX", toDbPrefix(options.name));
|
|
1976
|
+
variables.set("JSONS_LOCALE", options.jsonsLocale);
|
|
1810
1977
|
const menu = renderModuleMenu(options.moduleMenu, options.platformModules);
|
|
1811
1978
|
variables.set("MODULE_MENU", menu.text);
|
|
1812
1979
|
variables.set("MODULE_DEFAULT_MENU", menu.menuId ? `default_menu_id: '${menu.menuId}', ` : "");
|
|
@@ -1866,7 +2033,7 @@ if (process.argv.some((arg) => arg === "--version" || arg === "-v")) {
|
|
|
1866
2033
|
console.log(getCliVersion());
|
|
1867
2034
|
process.exit(0);
|
|
1868
2035
|
}
|
|
1869
|
-
var program = new Command().name("create-qs").description("Create a new Quicksilver application").version("0.8.
|
|
2036
|
+
var program = new Command().name("create-qs").description("Create a new Quicksilver application").version("0.8.26", "-v, --version").argument("[project-name]", "Name of the project").option("-c, --module-code <code>", "Full module code (e.g., power_crm.sales)").option("-d, --module-directory <directory>", "Module directory name (e.g., sales)").option("-n, --module-namespace <code>", "Prefix of unit/table, defaults to the initials of the module code (e.g., pcs)").option("-p, --api-package <package>", "JVM package for Kotlin files (e.g., com.company.crm)").option("-a, --api-artifact-id <artifactId>", "Module artifact ID in repository (e.g., power-crm-module-sales)").option("-w, --web-module-package <package>", "Web module package name (e.g., @powercrm/module-sales-web)").option("-m, --modules <codes>", "Modules to depend on, comma separated (e.g., quicksilver.org)").option("--module-menu <name>", "Create the module's top-level menu in the left nav, with this label").option("--no-module-menu", "Do not create a top-level menu").option("--jsons-locale <locale>", "Language of the display texts in data files: zh-CN or zh-TW. Changing it later means rewriting every data file").option("-y, --yes", "Skip prompts and use defaults").action(async (projectName, options) => {
|
|
1870
2037
|
applyPromptTexts();
|
|
1871
2038
|
console.log(chalk4.cyan(`
|
|
1872
2039
|
${t("banner")}`));
|
package/package.json
CHANGED
|
@@ -117,7 +117,7 @@ power-crm/
|
|
|
117
117
|
├── modules/sales/ Business module, where most day-to-day development happens
|
|
118
118
|
│ ├── module.yaml Module identity: id, code, namespaces, dependencies, web package name
|
|
119
119
|
│ ├── api/ Backend (Kotlin + Spring Boot)
|
|
120
|
-
│ │ └── data/init/ ← initial data: tables.jsons (optional), units/ (one file per unit) and
|
|
120
|
+
│ │ └── data/init/ ← initial data: tables.jsons (optional), units/ (one file per unit), rows/ and views.jsons (optional)
|
|
121
121
|
│ └── web/ Frontend (Preact + TypeScript)
|
|
122
122
|
│ └── src/module.ts Registry of pages, plugins, components, styles and icons
|
|
123
123
|
├── run/
|
|
@@ -117,7 +117,7 @@ power-crm/
|
|
|
117
117
|
├── modules/sales/ 业务模块,日常开发主要在此进行
|
|
118
118
|
│ ├── module.yaml 模块标识,包括 id、code、namespaces、依赖、Web 包名
|
|
119
119
|
│ ├── api/ 后端(Kotlin + Spring Boot)
|
|
120
|
-
│ │ └── data/init/ ← 初始化数据:tables.jsons(可选)、units
|
|
120
|
+
│ │ └── data/init/ ← 初始化数据:tables.jsons(可选)、units/(每个单元一个文件)、rows/ 与 views.jsons(可选)
|
|
121
121
|
│ └── web/ 前端(Preact + TypeScript)
|
|
122
122
|
│ └── src/module.ts 页面、插件、组件、样式、图标的注册表
|
|
123
123
|
├── run/
|
|
@@ -117,7 +117,7 @@ power-crm/
|
|
|
117
117
|
├── modules/sales/ 業務模組,日常開發主要在此進行
|
|
118
118
|
│ ├── module.yaml 模組識別,包括 id、code、namespaces、相依、Web 套件名稱
|
|
119
119
|
│ ├── api/ 後端(Kotlin + Spring Boot)
|
|
120
|
-
│ │ └── data/init/ ← 初始化資料:tables.jsons(可選)、units
|
|
120
|
+
│ │ └── data/init/ ← 初始化資料:tables.jsons(可選)、units/(每個單元一個檔案)、rows/ 與 views.jsons(可選)
|
|
121
121
|
│ └── web/ 前端(Preact + TypeScript)
|
|
122
122
|
│ └── src/module.ts 頁面、外掛、元件、樣式、圖示的註冊表
|
|
123
123
|
├── run/
|
|
@@ -105,6 +105,7 @@ Project scripts (see `scripts` in `package.json`; the database dialect scripts l
|
|
|
105
105
|
```bash
|
|
106
106
|
pnpm dev # Full dev environment (API + web), ports 6286 / 6288
|
|
107
107
|
pnpm dev:h2 # A second, isolated instance (own database and Atomikos directory)
|
|
108
|
+
pnpm dev:fresh # A separate instance (ports 6280 / 6281) whose own database is rebuilt from init and demo data at every start
|
|
108
109
|
pnpm build # Full build (backend + frontend)
|
|
109
110
|
pnpm test:api # Backend tests
|
|
110
111
|
pnpm lint # ESLint (pnpm lint:fix to autofix)
|
|
@@ -175,16 +176,17 @@ you are unsure what a target would actually do.
|
|
|
175
176
|
**An AI agent starts `pnpm dev:h2`, never `pnpm dev`.**
|
|
176
177
|
|
|
177
178
|
`pnpm dev` (web 6286 / api 6288) belongs to the person you are working with. It may well be running
|
|
178
|
-
already, and it is backed by the H2 file database `run/api/local/data/h2/
|
|
179
|
+
already, and it is backed by the H2 file database `run/api/local/data/h2/dev`, which keeps
|
|
179
180
|
their data between starts: with `auto-init.draft-sync: true`, every start runs the upgrade drafts that
|
|
180
181
|
changed since the last one. Starting or restarting it yourself interrupts them and runs your
|
|
181
182
|
half-finished drafts against their data, so taking it over does not just steal a port.
|
|
182
183
|
|
|
183
|
-
`pnpm dev:h2` (6210 / 6211) is the isolated instance: its own database (`
|
|
184
|
+
`pnpm dev:h2` (6210 / 6211) is the isolated instance: its own database (`h2`), its own log
|
|
184
185
|
directory and its own Atomikos directory, so it can run side by side with theirs. The other dialect
|
|
185
186
|
scripts (`dev:postgresql` 6220/6221, `dev:mariadb` 6230/6231, `dev:mssql` 6240/6241, `dev:oracle`
|
|
186
187
|
6250/6251) are equally isolated, but they need a real server: fill in the placeholders in
|
|
187
|
-
`run/api/config/application-<dialect>.yaml` first.
|
|
188
|
+
`run/api/config/application-<dialect>.yaml` first. `pnpm dev:fresh` (6280 / 6281) is isolated in the
|
|
189
|
+
same way, on the H2 database `fresh`, which it drops and rebuilds from init and demo data at every start.
|
|
188
190
|
|
|
189
191
|
Two more rules:
|
|
190
192
|
|
|
@@ -194,6 +196,9 @@ Two more rules:
|
|
|
194
196
|
something in 9000–9999; if it is still taken, Vite fails outright — pick another.
|
|
195
197
|
- **Stop the server you started.** Especially after Playwright: leaving it running holds the port
|
|
196
198
|
and the database file.
|
|
199
|
+
- **Rebuild when the database no longer matches the branch.** After switching branches, or when
|
|
200
|
+
earlier experiments left the database in a state you cannot explain, start once with
|
|
201
|
+
`pnpm dev:h2 --fresh`. Draft sync never undoes a change that was taken out of a draft.
|
|
197
202
|
|
|
198
203
|
## Signing in locally
|
|
199
204
|
|
|
@@ -219,22 +224,39 @@ The profile file is the source of truth for where the data actually lives. Read
|
|
|
219
224
|
started) and take the datasource from there; note that paths in it are relative to `run/api`, which
|
|
220
225
|
is the working directory of the API process.
|
|
221
226
|
|
|
222
|
-
For the default H2 profiles that means a file — `run/api/local/data/h2/
|
|
223
|
-
`
|
|
227
|
+
For the default H2 profiles that means a file — `run/api/local/data/h2/dev` for `pnpm dev`,
|
|
228
|
+
`h2` for `pnpm dev:h2` — which you can open with any H2 client. For the other dialects,
|
|
224
229
|
connect with your usual client using the host, port, user and password from that same file.
|
|
225
230
|
|
|
226
231
|
The development database is kept between starts. With `auto-init.draft-sync: true`, every start runs
|
|
227
232
|
each module draft `data/upgrade/draft.jsons` that changed since the last start, and data entered by hand
|
|
228
|
-
or through the UI stays.
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
233
|
+
or through the UI stays. Data files follow four rules:
|
|
234
|
+
|
|
235
|
+
1. **`data/init/` holds the final state.** A fresh install runs only `data/init/`.
|
|
236
|
+
2. **The draft holds the rows you added or changed in `data/init/` this time.** `data/init/views.jsons`
|
|
237
|
+
is the exception: it never goes into the draft, since the platform converges the views to it on
|
|
238
|
+
every start. When a dropped or renamed column or table is used by one of those views, rewrite the
|
|
239
|
+
view's definition in `views.jsons` in the same change. A row nested in `$add` (the unit row, a field, a form item, a list field and so on)
|
|
240
|
+
goes into the draft as a standalone row carrying the id of its parent, for example a field row
|
|
241
|
+
with `unit_id`. A rename works the same way: write the new `table_name` or `column_name` in the
|
|
242
|
+
unit row or the field row, and the platform renames the table or the column.
|
|
243
|
+
3. **What `data/init/` cannot express goes into the draft only.** That is removals (take the rows out
|
|
244
|
+
of `data/init/` and write `$delete`, `@drop_column` or `@drop_table` into the draft), fixes to
|
|
245
|
+
existing data such as filling a new column, `@alter_column` to shorten a column, change a default
|
|
246
|
+
or make a column of a plain table NOT NULL, and `@rename_table`, `@rename_column` and
|
|
247
|
+
`@alter_primary_key` for plain tables.
|
|
248
|
+
4. **The platform does the rest.** That covers tables and columns, derived tables, indexes, i18n
|
|
249
|
+
views, dropping the views that depend on a changed table and rebuilding them afterwards, and the
|
|
250
|
+
references kept by name when something is renamed.
|
|
251
|
+
|
|
252
|
+
A change made only in `data/init/` reaches neither this database nor installed environments, and the
|
|
253
|
+
startup log lists such files. A mistake fails the precheck or the run, and the error says how to
|
|
254
|
+
write it instead. Changes to `data/demo/` stay out of the draft, since demo data runs on fresh
|
|
255
|
+
installs only. Imperative commands in a draft (`@sql`,
|
|
234
256
|
`$update`, `$delete` ...) run once per `$key`; to run a changed one again, give it a new `$key`.
|
|
235
257
|
`$place` is the exception: it runs again whenever its draft runs. To rebuild the database from the
|
|
236
|
-
data files under `data/init/` and `data/demo/`,
|
|
237
|
-
|
|
258
|
+
data files under `data/init/` and `data/demo/`, add `--fresh` to the start command for one start
|
|
259
|
+
(`pnpm dev:h2 --fresh`). It changes no config file, and the next start without it keeps the data again.
|
|
238
260
|
|
|
239
261
|
## Development skills
|
|
240
262
|
|
|
@@ -5,7 +5,7 @@ This folder holds demo data, rows that exist only to show and test the product,
|
|
|
5
5
|
- Put the rows in `.jsons` files in this folder or in subfolders of it. Other files, such as this README, are skipped.
|
|
6
6
|
- On a fresh install of the module, the files run after everything in `data/init/`, sorted by name level by level with each subfolder run where it sorts, and they can refer to rows written there.
|
|
7
7
|
- Development and test profiles load demo data, as set by `quicksilver.datasource.auto-init.demo-data` in `run/api/config/application-dev.yaml`. On a production install the installer asks first, and does not load it by default.
|
|
8
|
-
- A module that is already installed never loads demo data. Draft sync does not run these files either, so an existing database does not pick up files added or changed here. To load them,
|
|
8
|
+
- A module that is already installed never loads demo data. Draft sync does not run these files either, so an existing database does not pick up files added or changed here. To load them, start once with `pnpm dev --fresh`, or add `--fresh` to the dialect script you use. The rebuild clears the whole development database, including rows entered through the UI.
|
|
9
9
|
- Do not copy demo rows into `data/upgrade/draft.jsons`. Upgrades never load demo data.
|
|
10
10
|
|
|
11
11
|
The rules for data files are in section 10 of [`.agents/skills/qs-jsons/references/json-command.md`](../../../../../.agents/skills/qs-jsons/references/json-command.md), and in the developer manual at [`.quicksilver/manual/en-US/06-data.md`](../../../../../.quicksilver/manual/en-US/06-data.md). Both are written by `pnpm install`.
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"scripts": {
|
|
11
11
|
"// --- start dev servers -------------------------------------------": "",
|
|
12
12
|
"dev": "quicksilver dev",
|
|
13
|
+
"dev:fresh": "PORT=6280 API_PORT=6281 SPRING_PROFILES_ACTIVE=dev,fresh quicksilver dev --fresh",
|
|
13
14
|
"dev:h2": "PORT=6210 API_PORT=6211 SPRING_PROFILES_ACTIVE=dev,h2 quicksilver dev",
|
|
14
15
|
"dev:postgresql": "PORT=6220 API_PORT=6221 SPRING_PROFILES_ACTIVE=dev,postgresql quicksilver dev",
|
|
15
16
|
"dev:mariadb": "PORT=6230 API_PORT=6231 SPRING_PROFILES_ACTIVE=dev,mariadb quicksilver dev",
|
|
@@ -25,13 +25,13 @@ catalogs:
|
|
|
25
25
|
client:
|
|
26
26
|
"@qs-charts/adapter-preact": "0.2.11"
|
|
27
27
|
"@qs-charts/core": "0.2.11"
|
|
28
|
-
"@qs-elements/web": "1.0.
|
|
29
|
-
"@qs-elements/web-preact": "1.0.
|
|
28
|
+
"@qs-elements/web": "1.0.163"
|
|
29
|
+
"@qs-elements/web-preact": "1.0.163"
|
|
30
30
|
quicksilver:
|
|
31
|
-
"@qs-platform/cli": "0.8.
|
|
32
|
-
"@qs-platform/preset-vite-web": "0.8.
|
|
33
|
-
"@qs-platform/web-module-core": "0.8.
|
|
34
|
-
"@qs-platform/web-module-org": "0.8.
|
|
31
|
+
"@qs-platform/cli": "0.8.26"
|
|
32
|
+
"@qs-platform/preset-vite-web": "0.8.26"
|
|
33
|
+
"@qs-platform/web-module-core": "0.8.26"
|
|
34
|
+
"@qs-platform/web-module-org": "0.8.26"
|
|
35
35
|
tooling:
|
|
36
36
|
"@types/node": "^26.4.0"
|
|
37
37
|
"sass": "^1.103.1"
|
|
@@ -7,6 +7,14 @@ names:
|
|
|
7
7
|
maven-group: {{API_PACKAGE}}
|
|
8
8
|
maven-prefix: {{PROJECT_CODE}}
|
|
9
9
|
|
|
10
|
+
i18n:
|
|
11
|
+
# Language of the display texts in data files, the same for every module of the project.
|
|
12
|
+
# Changing it later means rewriting every data file and translation table.
|
|
13
|
+
jsons-locale: {{JSONS_LOCALE}}
|
|
14
|
+
# Languages the product supports. Installations pick the product language from them.
|
|
15
|
+
# Add zh-CN, zh-TW or en-US once data/i18n.tsv of every module carries its translations.
|
|
16
|
+
locales: [{{JSONS_LOCALE}}]
|
|
17
|
+
|
|
10
18
|
packaging:
|
|
11
19
|
archive-name: {{PROJECT_CODE}}
|
|
12
20
|
fixed-modules: [{{PLATFORM_MODULE_CODES_QUOTED}}, '{{MODULE_CODE}}']
|
|
@@ -33,7 +33,7 @@ database after this project — a project called `power-crm` gets `power_crm_dev
|
|
|
33
33
|
Quicksilver products can share one database server without colliding. Two files sit outside that
|
|
34
34
|
rule: Oracle, whose "database" is a PDB with a name fixed by the container image (`FREEPDB1`), and
|
|
35
35
|
the H2 profiles, whose databases are plain files under this project's own `local/data/h2/`
|
|
36
|
-
(`
|
|
36
|
+
(`dev` for `pnpm dev`, `h2` for `pnpm dev:h2`) and so have nothing to collide with. The
|
|
37
37
|
datasource list is replaced as a whole, so when you change one entry, keep the other one listed
|
|
38
38
|
as well.
|
|
39
39
|
|
|
@@ -60,16 +60,28 @@ dialects. Every other dialect listed above ships with its driver.
|
|
|
60
60
|
|
|
61
61
|
The `dev` profile turns on `draft-sync` and turns off `drop-first`. Every start runs the upgrade
|
|
62
62
|
drafts that changed since the last start, and keeps the data already in the dev database. To
|
|
63
|
-
rebuild the database from init and demo data,
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
rebuild the database from init and demo data, start once with `pnpm dev --fresh`, or add `--fresh` to
|
|
64
|
+
a dialect script, as in `pnpm dev:h2 --fresh`. The option turns `drop-first` on for that start only
|
|
65
|
+
and changes no config file. Switching `draft-sync` on or off also needs one such rebuild: the two
|
|
66
|
+
modes record drafts differently, and startup refuses a database built in the other mode.
|
|
67
|
+
`drop-first` is refused outside development mode, so a production configuration that turns it on
|
|
68
|
+
fails at startup.
|
|
69
|
+
|
|
70
|
+
## Views
|
|
71
|
+
|
|
72
|
+
The views a module declares in `data/init/views.jsons` are synced on every start: missing views are
|
|
73
|
+
created, views whose definition changed are replaced, and views taken out of the file are dropped.
|
|
74
|
+
When a view fails to build, for example because its definition uses a column that no longer exists,
|
|
75
|
+
startup fails by default, and the error names the module, the view, the definition and the reason
|
|
76
|
+
given by the database. Set `quicksilver.datasource.fail-on-view-sync-error: false` to log the error
|
|
77
|
+
and start anyway. The view and the views that depend on it then stay missing until a later start
|
|
78
|
+
builds them.
|
|
66
79
|
|
|
67
80
|
## Local edits
|
|
68
81
|
|
|
69
82
|
`application-dev.yaml` is committed, but **by convention your local edits to it are not** — a
|
|
70
|
-
different database host, `
|
|
71
|
-
|
|
72
|
-
default, and say so in the commit message.
|
|
83
|
+
different database host, a different `fail-on-unregistered-data-source`. Commit it only when you
|
|
84
|
+
are deliberately changing the team default, and say so in the commit message.
|
|
73
85
|
|
|
74
86
|
For an override that never enters version control, point `QUICKSILVER_CONFIG_LOCATION` at a config
|
|
75
87
|
directory of your own: it is appended to the tail of `spring.config.location`, so it wins over
|
|
@@ -10,11 +10,11 @@ quicksilver:
|
|
|
10
10
|
# Keep the quotes around passwords: special characters (# : @ ...) then paste in as is.
|
|
11
11
|
items:
|
|
12
12
|
- code: metadata
|
|
13
|
-
url: "jdbc:h2:./local/data/h2/
|
|
13
|
+
url: "jdbc:h2:./local/data/h2/dev;DB_CLOSE_ON_EXIT=FALSE;DATABASE_TO_UPPER=false;MODE=MySQL"
|
|
14
14
|
user: sa
|
|
15
15
|
password: "sa"
|
|
16
16
|
- code: business
|
|
17
|
-
url: "jdbc:h2:./local/data/h2/
|
|
17
|
+
url: "jdbc:h2:./local/data/h2/dev;DB_CLOSE_ON_EXIT=FALSE;DATABASE_TO_UPPER=false;MODE=MySQL"
|
|
18
18
|
user: sa
|
|
19
19
|
password: "sa"
|
|
20
20
|
# Each ac_table row records the datasource its table lives in. When a row names a code that is
|
|
@@ -27,15 +27,16 @@ quicksilver:
|
|
|
27
27
|
# only when quicksilver.application.mode is development. In production the installer does it.
|
|
28
28
|
enabled: true
|
|
29
29
|
# Drop the tables and views that Quicksilver manages before provisioning, so the database is
|
|
30
|
-
# rebuilt from init and demo data.
|
|
30
|
+
# rebuilt from init and demo data. Keep it false here. To rebuild, start once with
|
|
31
|
+
# `pnpm dev --fresh`, or add --fresh to a dialect script, which turns it on for that start only.
|
|
31
32
|
drop-first: false
|
|
32
33
|
# Draft sync. Every start runs the upgrade drafts that changed since the last start. Data in
|
|
33
34
|
# the dev database is kept, including rows entered through the UI. Switching draft-sync on or
|
|
34
|
-
# off needs one rebuild with
|
|
35
|
+
# off needs one rebuild with --fresh.
|
|
35
36
|
draft-sync: true
|
|
36
37
|
# Demo data: on a fresh install of a module, the .jsons files in its data/demo/ run after its
|
|
37
38
|
# init. A database that already exists does not pick up demo files added later, rebuild it with
|
|
38
|
-
#
|
|
39
|
+
# --fresh as described above.
|
|
39
40
|
demo-data: true
|
|
40
41
|
webhook:
|
|
41
42
|
# Webhook targets that resolve to a private or loopback address (10.0.0.0/8, 127.0.0.0/8, ...)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Overlay for the `fresh` profile used by `pnpm dev:fresh`: only the keys that differ from
|
|
3
|
+
# application-dev.yaml, everything else is inherited key by key. It has a database of its own, which
|
|
4
|
+
# the script drops and rebuilds from init and demo data at every start, so the `pnpm dev` database
|
|
5
|
+
# is never touched.
|
|
6
|
+
#
|
|
7
|
+
logging:
|
|
8
|
+
file.path: local/log/application/fresh
|
|
9
|
+
|
|
10
|
+
quicksilver:
|
|
11
|
+
datasource:
|
|
12
|
+
# Keep the quotes around passwords: special characters (# : @ ...) then paste in as is.
|
|
13
|
+
items:
|
|
14
|
+
- code: metadata
|
|
15
|
+
url: "jdbc:h2:./local/data/h2/fresh;DB_CLOSE_ON_EXIT=FALSE;DATABASE_TO_UPPER=false;MODE=MySQL"
|
|
16
|
+
user: sa
|
|
17
|
+
password: "sa"
|
|
18
|
+
- code: business
|
|
19
|
+
url: "jdbc:h2:./local/data/h2/fresh;DB_CLOSE_ON_EXIT=FALSE;DATABASE_TO_UPPER=false;MODE=MySQL"
|
|
20
|
+
user: sa
|
|
21
|
+
password: "sa"
|
|
22
|
+
atomikos:
|
|
23
|
+
log_base_dir: local/log/atomikos/fresh
|
|
@@ -11,11 +11,11 @@ quicksilver:
|
|
|
11
11
|
# Keep the quotes around passwords: special characters (# : @ ...) then paste in as is.
|
|
12
12
|
items:
|
|
13
13
|
- code: metadata
|
|
14
|
-
url: "jdbc:h2:./local/data/h2/
|
|
14
|
+
url: "jdbc:h2:./local/data/h2/h2;DB_CLOSE_ON_EXIT=FALSE;DATABASE_TO_UPPER=false;MODE=MySQL"
|
|
15
15
|
user: sa
|
|
16
16
|
password: "sa"
|
|
17
17
|
- code: business
|
|
18
|
-
url: "jdbc:h2:./local/data/h2/
|
|
18
|
+
url: "jdbc:h2:./local/data/h2/h2;DB_CLOSE_ON_EXIT=FALSE;DATABASE_TO_UPPER=false;MODE=MySQL"
|
|
19
19
|
user: sa
|
|
20
20
|
password: "sa"
|
|
21
21
|
atomikos:
|