@svadmin/create 0.26.1 → 0.28.7
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/index.js +277 -160
- package/guidance/AGENTS.md +39 -0
- package/package.json +3 -3
- package/scaffold-manifest.json +6 -9
- package/template/src/app.css +1 -7
- package/template/src/pages/Dashboard.svelte +60 -47
- package/template/src/resource-contracts.ts +29 -0
- package/template/src/resources.ts +5 -0
- package/template/vite.config.ts +0 -2
package/dist/index.js
CHANGED
|
@@ -19,7 +19,7 @@ var __toESM = (mod, isNodeMode, target) => {
|
|
|
19
19
|
return cached;
|
|
20
20
|
}
|
|
21
21
|
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
22
|
-
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
22
|
+
const to = isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
23
23
|
if (mod && typeof mod === "object" || typeof mod === "function") {
|
|
24
24
|
for (let key of __getOwnPropNames(mod))
|
|
25
25
|
if (!__hasOwnProp.call(to, key))
|
|
@@ -259,7 +259,7 @@ var require_clear = __commonJS(function(exports, module) {
|
|
|
259
259
|
if (it)
|
|
260
260
|
o = it;
|
|
261
261
|
var i = 0;
|
|
262
|
-
var F = function
|
|
262
|
+
var F = function F() {};
|
|
263
263
|
return { s: F, n: function n() {
|
|
264
264
|
if (i >= o.length)
|
|
265
265
|
return { done: true };
|
|
@@ -876,7 +876,7 @@ var require_select = __commonJS(function(exports, module) {
|
|
|
876
876
|
else
|
|
877
877
|
this.out.write(clear(this.outputText, this.out.columns));
|
|
878
878
|
super.render();
|
|
879
|
-
let _entriesToDisplay = entriesToDisplay(this.cursor, this.choices.length, this.optionsPerPage),
|
|
879
|
+
let _entriesToDisplay = entriesToDisplay(this.cursor, this.choices.length, this.optionsPerPage), { startIndex, endIndex } = _entriesToDisplay;
|
|
880
880
|
this.outputText = [style.symbol(this.done, this.aborted), color.bold(this.msg), style.delimiter(false), this.done ? this.selection.title : this.selection.disabled ? color.yellow(this.warn) : color.gray(this.hint)].join(" ");
|
|
881
881
|
if (!this.done) {
|
|
882
882
|
this.outputText += `
|
|
@@ -1885,14 +1885,14 @@ Instructions:
|
|
|
1885
1885
|
}
|
|
1886
1886
|
return "";
|
|
1887
1887
|
}
|
|
1888
|
-
renderOption(
|
|
1888
|
+
renderOption(cursor, v, i, arrowIndicator) {
|
|
1889
1889
|
const prefix = (v.selected ? color.green(figures.radioOn) : figures.radioOff) + " " + arrowIndicator + " ";
|
|
1890
1890
|
let title, desc;
|
|
1891
1891
|
if (v.disabled) {
|
|
1892
|
-
title =
|
|
1892
|
+
title = cursor === i ? color.gray().underline(v.title) : color.strikethrough().gray(v.title);
|
|
1893
1893
|
} else {
|
|
1894
|
-
title =
|
|
1895
|
-
if (
|
|
1894
|
+
title = cursor === i ? color.cyan().underline(v.title) : v.title;
|
|
1895
|
+
if (cursor === i && v.description) {
|
|
1896
1896
|
desc = ` - ${v.description}`;
|
|
1897
1897
|
if (prefix.length + title.length + desc.length >= this.out.columns || v.description.split(/\r?\n/).length > 1) {
|
|
1898
1898
|
desc = `
|
|
@@ -1909,7 +1909,7 @@ Instructions:
|
|
|
1909
1909
|
if (options.length === 0) {
|
|
1910
1910
|
return color.red("No matches for this query.");
|
|
1911
1911
|
}
|
|
1912
|
-
let _entriesToDisplay = entriesToDisplay(this.cursor, options.length, this.optionsPerPage),
|
|
1912
|
+
let _entriesToDisplay = entriesToDisplay(this.cursor, options.length, this.optionsPerPage), { startIndex, endIndex } = _entriesToDisplay;
|
|
1913
1913
|
let prefix, styledOptions = [];
|
|
1914
1914
|
for (let i = startIndex;i < endIndex; i++) {
|
|
1915
1915
|
if (i === startIndex && startIndex > 0) {
|
|
@@ -2217,7 +2217,7 @@ var require_autocomplete = __commonJS(function(exports, module) {
|
|
|
2217
2217
|
else
|
|
2218
2218
|
this.out.write(clear(this.outputText, this.out.columns));
|
|
2219
2219
|
super.render();
|
|
2220
|
-
let _entriesToDisplay = entriesToDisplay(this.select, this.choices.length, this.limit),
|
|
2220
|
+
let _entriesToDisplay = entriesToDisplay(this.select, this.choices.length, this.limit), { startIndex, endIndex } = _entriesToDisplay;
|
|
2221
2221
|
this.outputText = [style.symbol(this.done, this.aborted, this.exited), color.bold(this.msg), style.delimiter(this.completing), this.done && this.suggestions[this.select] ? this.suggestions[this.select].title : this.rendered = this.transform.render(this.input)].join(" ");
|
|
2222
2222
|
if (!this.done) {
|
|
2223
2223
|
const suggestions = this.suggestions.slice(startIndex, endIndex).map((item, i) => this.renderOption(item, this.select === i + startIndex, i === 0 && startIndex > 0, i + startIndex === endIndex - 1 && endIndex < this.choices.length)).join(`
|
|
@@ -2356,12 +2356,12 @@ Instructions:
|
|
|
2356
2356
|
Filtered results for: ${this.inputValue ? this.inputValue : color.gray("Enter something to filter")}
|
|
2357
2357
|
`;
|
|
2358
2358
|
}
|
|
2359
|
-
renderOption(
|
|
2359
|
+
renderOption(cursor, v, i) {
|
|
2360
2360
|
let title;
|
|
2361
2361
|
if (v.disabled)
|
|
2362
|
-
title =
|
|
2362
|
+
title = cursor === i ? color.gray().underline(v.title) : color.strikethrough().gray(v.title);
|
|
2363
2363
|
else
|
|
2364
|
-
title =
|
|
2364
|
+
title = cursor === i ? color.cyan().underline(v.title) : v.title;
|
|
2365
2365
|
return (v.selected ? color.green(figures.radioOn) : figures.radioOff) + " " + title;
|
|
2366
2366
|
}
|
|
2367
2367
|
renderDoneOrInstructions() {
|
|
@@ -2591,7 +2591,7 @@ var require_dist = __commonJS(function(exports, module) {
|
|
|
2591
2591
|
if (it)
|
|
2592
2592
|
o = it;
|
|
2593
2593
|
var i = 0;
|
|
2594
|
-
var F = function
|
|
2594
|
+
var F = function F() {};
|
|
2595
2595
|
return { s: F, n: function n() {
|
|
2596
2596
|
if (i >= o.length)
|
|
2597
2597
|
return { done: true };
|
|
@@ -2684,17 +2684,17 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
|
|
|
2684
2684
|
onCancel = noop
|
|
2685
2685
|
} = {}) {
|
|
2686
2686
|
const answers = {};
|
|
2687
|
-
const
|
|
2687
|
+
const override = prompt._override || {};
|
|
2688
2688
|
questions = [].concat(questions);
|
|
2689
2689
|
let answer, question, quit, name, type, lastPrompt;
|
|
2690
2690
|
const getFormattedAnswer = /* @__PURE__ */ function() {
|
|
2691
|
-
var _ref = _asyncToGenerator(function* (
|
|
2692
|
-
if (!skipValidation &&
|
|
2691
|
+
var _ref = _asyncToGenerator(function* (question, answer, skipValidation = false) {
|
|
2692
|
+
if (!skipValidation && question.validate && question.validate(answer) !== true) {
|
|
2693
2693
|
return;
|
|
2694
2694
|
}
|
|
2695
|
-
return
|
|
2695
|
+
return question.format ? yield question.format(answer, answers) : answer;
|
|
2696
2696
|
});
|
|
2697
|
-
return function
|
|
2697
|
+
return function getFormattedAnswer(_x, _x2) {
|
|
2698
2698
|
return _ref.apply(this, arguments);
|
|
2699
2699
|
};
|
|
2700
2700
|
}();
|
|
@@ -2727,8 +2727,8 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
|
|
|
2727
2727
|
if (prompts[type] === undefined) {
|
|
2728
2728
|
throw new Error(`prompt type (${type}) is not defined`);
|
|
2729
2729
|
}
|
|
2730
|
-
if (
|
|
2731
|
-
answer = yield getFormattedAnswer(question,
|
|
2730
|
+
if (override[question.name] !== undefined) {
|
|
2731
|
+
answer = yield getFormattedAnswer(question, override[question.name]);
|
|
2732
2732
|
if (answer !== undefined) {
|
|
2733
2733
|
answers[name] = answer;
|
|
2734
2734
|
continue;
|
|
@@ -4870,14 +4870,14 @@ var require_lib = __commonJS(function(exports, module) {
|
|
|
4870
4870
|
var noop = () => {};
|
|
4871
4871
|
async function prompt(questions = [], { onSubmit = noop, onCancel = noop } = {}) {
|
|
4872
4872
|
const answers = {};
|
|
4873
|
-
const
|
|
4873
|
+
const override = prompt._override || {};
|
|
4874
4874
|
questions = [].concat(questions);
|
|
4875
4875
|
let answer, question, quit, name, type, lastPrompt;
|
|
4876
|
-
const getFormattedAnswer = async (
|
|
4877
|
-
if (!skipValidation &&
|
|
4876
|
+
const getFormattedAnswer = async (question, answer, skipValidation = false) => {
|
|
4877
|
+
if (!skipValidation && question.validate && question.validate(answer) !== true) {
|
|
4878
4878
|
return;
|
|
4879
4879
|
}
|
|
4880
|
-
return
|
|
4880
|
+
return question.format ? await question.format(answer, answers) : answer;
|
|
4881
4881
|
};
|
|
4882
4882
|
for (question of questions) {
|
|
4883
4883
|
({ name, type } = question);
|
|
@@ -4901,8 +4901,8 @@ var require_lib = __commonJS(function(exports, module) {
|
|
|
4901
4901
|
if (prompts[type] === undefined) {
|
|
4902
4902
|
throw new Error(`prompt type (${type}) is not defined`);
|
|
4903
4903
|
}
|
|
4904
|
-
if (
|
|
4905
|
-
answer = await getFormattedAnswer(question,
|
|
4904
|
+
if (override[question.name] !== undefined) {
|
|
4905
|
+
answer = await getFormattedAnswer(question, override[question.name]);
|
|
4906
4906
|
if (answer !== undefined) {
|
|
4907
4907
|
answers[name] = answer;
|
|
4908
4908
|
continue;
|
|
@@ -5075,22 +5075,22 @@ function assertReferencedPacksExist(dependencyPacks, selectedPacks) {
|
|
|
5075
5075
|
}
|
|
5076
5076
|
function assertScaffoldManifest(candidate) {
|
|
5077
5077
|
assertJsonObject(candidate, "scaffold manifest");
|
|
5078
|
-
assertNonEmptyString(candidate
|
|
5079
|
-
assertNonEmptyString(candidate
|
|
5080
|
-
if (candidate
|
|
5078
|
+
assertNonEmptyString(candidate["name"], "scaffold manifest.name");
|
|
5079
|
+
assertNonEmptyString(candidate["version"], "scaffold manifest.version");
|
|
5080
|
+
if (candidate["private"] !== true) {
|
|
5081
5081
|
throw new Error("scaffold manifest.private must be true");
|
|
5082
5082
|
}
|
|
5083
|
-
assertNonEmptyString(candidate
|
|
5084
|
-
assertStringRecord(candidate
|
|
5085
|
-
assertStringRecord(candidate
|
|
5086
|
-
assertStringRecord(candidate
|
|
5087
|
-
assertJsonObject(candidate
|
|
5088
|
-
assertDependencyPacks(candidate
|
|
5089
|
-
assertSelectionMap(candidate
|
|
5090
|
-
assertSelectionMap(candidate
|
|
5091
|
-
assertReferencedPacksExist(candidate
|
|
5092
|
-
...Object.values(candidate
|
|
5093
|
-
...Object.values(candidate
|
|
5083
|
+
assertNonEmptyString(candidate["type"], "scaffold manifest.type");
|
|
5084
|
+
assertStringRecord(candidate["scripts"], "scaffold manifest.scripts");
|
|
5085
|
+
assertStringRecord(candidate["dependencies"], "scaffold manifest.dependencies");
|
|
5086
|
+
assertStringRecord(candidate["devDependencies"], "scaffold manifest.devDependencies");
|
|
5087
|
+
assertJsonObject(candidate["svadmin"], "scaffold manifest.svadmin");
|
|
5088
|
+
assertDependencyPacks(candidate["svadmin"]["dependencyPacks"]);
|
|
5089
|
+
assertSelectionMap(candidate["svadmin"]["dataProviders"], DATA_PROVIDER_CHOICES, "scaffold manifest.svadmin.dataProviders");
|
|
5090
|
+
assertSelectionMap(candidate["svadmin"]["authProviders"], AUTH_PROVIDER_CHOICES, "scaffold manifest.svadmin.authProviders");
|
|
5091
|
+
assertReferencedPacksExist(candidate["svadmin"]["dependencyPacks"], [
|
|
5092
|
+
...Object.values(candidate["svadmin"]["dataProviders"]).flat(),
|
|
5093
|
+
...Object.values(candidate["svadmin"]["authProviders"]).flat()
|
|
5094
5094
|
]);
|
|
5095
5095
|
}
|
|
5096
5096
|
function loadScaffoldManifest(manifestPath) {
|
|
@@ -5114,7 +5114,10 @@ function createProjectPackageJson(scaffold, options) {
|
|
|
5114
5114
|
...scaffold.svadmin.authProviders[options.authProvider]
|
|
5115
5115
|
]);
|
|
5116
5116
|
for (const packName of selectedPacks) {
|
|
5117
|
-
|
|
5117
|
+
const pack = scaffold.svadmin.dependencyPacks[packName];
|
|
5118
|
+
if (pack === undefined)
|
|
5119
|
+
throw new Error(`Unknown dependency pack: ${packName}`);
|
|
5120
|
+
mergeDependencyPack(dependencies, pack, packName);
|
|
5118
5121
|
}
|
|
5119
5122
|
return {
|
|
5120
5123
|
name: options.projectName.trim(),
|
|
@@ -5431,8 +5434,10 @@ function parseGraphQLSDL(sdl) {
|
|
|
5431
5434
|
const enumRegex = /enum\s+([A-Za-z0-9_]+)\s*\{([^}]+)\}/g;
|
|
5432
5435
|
let match;
|
|
5433
5436
|
while ((match = enumRegex.exec(cleanSDL)) !== null) {
|
|
5434
|
-
const enumName = match
|
|
5435
|
-
|
|
5437
|
+
const [, enumName, enumBody] = match;
|
|
5438
|
+
if (enumName === undefined || enumBody === undefined)
|
|
5439
|
+
continue;
|
|
5440
|
+
const rawValues = enumBody.split(/\s+/).filter(Boolean);
|
|
5436
5441
|
types.push({
|
|
5437
5442
|
kind: "ENUM",
|
|
5438
5443
|
name: enumName,
|
|
@@ -5441,8 +5446,9 @@ function parseGraphQLSDL(sdl) {
|
|
|
5441
5446
|
}
|
|
5442
5447
|
const typeRegex = /type\s+([A-Za-z0-9_]+)\s*(?:implements\s+[A-Za-z0-9_&,\s]+)?\s*\{([^}]+)\}/g;
|
|
5443
5448
|
while ((match = typeRegex.exec(cleanSDL)) !== null) {
|
|
5444
|
-
const typeName = match
|
|
5445
|
-
|
|
5449
|
+
const [, typeName, fieldsBody] = match;
|
|
5450
|
+
if (typeName === undefined || fieldsBody === undefined)
|
|
5451
|
+
continue;
|
|
5446
5452
|
const fieldLines = fieldsBody.split(`
|
|
5447
5453
|
`).map((l) => l.trim()).filter(Boolean);
|
|
5448
5454
|
const fields = [];
|
|
@@ -5450,8 +5456,9 @@ function parseGraphQLSDL(sdl) {
|
|
|
5450
5456
|
const fieldMatch = /^([A-Za-z0-9_]+)(?:\([^)]*\))?\s*:\s*([[\]A-Za-z0-9_!]+)/.exec(line);
|
|
5451
5457
|
if (!fieldMatch)
|
|
5452
5458
|
continue;
|
|
5453
|
-
const fName = fieldMatch
|
|
5454
|
-
|
|
5459
|
+
const [, fName, rawType] = fieldMatch;
|
|
5460
|
+
if (fName === undefined || rawType === undefined)
|
|
5461
|
+
continue;
|
|
5455
5462
|
const isNonNullable = rawType.endsWith("!");
|
|
5456
5463
|
const cleanType = isNonNullable ? rawType.slice(0, -1) : rawType;
|
|
5457
5464
|
const isList = cleanType.startsWith("[") && cleanType.endsWith("]");
|
|
@@ -5966,74 +5973,99 @@ function parseInferArguments(args) {
|
|
|
5966
5973
|
method: "GET",
|
|
5967
5974
|
format: "all"
|
|
5968
5975
|
};
|
|
5969
|
-
|
|
5970
|
-
|
|
5976
|
+
const argumentsIterator = args.values();
|
|
5977
|
+
function requireArgument(flag) {
|
|
5978
|
+
const next = argumentsIterator.next();
|
|
5979
|
+
if (next.done || !next.value.trim() || next.value.startsWith("-")) {
|
|
5980
|
+
throw new Error(`Missing value for ${flag}`);
|
|
5981
|
+
}
|
|
5982
|
+
return next.value;
|
|
5983
|
+
}
|
|
5984
|
+
function sourceType(value) {
|
|
5985
|
+
switch (value) {
|
|
5986
|
+
case "rest":
|
|
5987
|
+
case "openapi":
|
|
5988
|
+
case "graphql":
|
|
5989
|
+
case "auto":
|
|
5990
|
+
return value;
|
|
5991
|
+
default:
|
|
5992
|
+
throw new Error(`Invalid source type: ${value}`);
|
|
5993
|
+
}
|
|
5994
|
+
}
|
|
5995
|
+
function outputFormat(value) {
|
|
5996
|
+
switch (value) {
|
|
5997
|
+
case "all":
|
|
5998
|
+
case "resource":
|
|
5999
|
+
case "typebox":
|
|
6000
|
+
case "components":
|
|
6001
|
+
return value;
|
|
6002
|
+
default:
|
|
6003
|
+
throw new Error(`Invalid output format: ${value}`);
|
|
6004
|
+
}
|
|
6005
|
+
}
|
|
6006
|
+
function addHeader(headerLine) {
|
|
6007
|
+
const colonIndex = headerLine.indexOf(":");
|
|
6008
|
+
const key = headerLine.slice(0, colonIndex).trim();
|
|
6009
|
+
if (colonIndex <= 0 || !key) {
|
|
6010
|
+
throw new Error(`Invalid header: ${headerLine}`);
|
|
6011
|
+
}
|
|
6012
|
+
options.headers ??= {};
|
|
6013
|
+
options.headers[key] = headerLine.slice(colonIndex + 1).trim();
|
|
6014
|
+
}
|
|
6015
|
+
for (const arg of argumentsIterator) {
|
|
6016
|
+
if (arg.startsWith("--") && arg.indexOf("=") === arg.length - 1) {
|
|
6017
|
+
throw new Error(`Missing value for ${arg.slice(0, -1)}`);
|
|
6018
|
+
}
|
|
5971
6019
|
if (arg === "--write" || arg === "-w") {
|
|
5972
6020
|
options.write = true;
|
|
5973
6021
|
} else if (arg === "--dry-run") {
|
|
5974
6022
|
options.write = false;
|
|
5975
6023
|
} else if (arg === "--url" || arg === "-u") {
|
|
5976
|
-
options.url =
|
|
6024
|
+
options.url = requireArgument(arg);
|
|
5977
6025
|
} else if (arg.startsWith("--url=")) {
|
|
5978
6026
|
options.url = arg.slice(6);
|
|
5979
6027
|
} else if (arg === "--file" || arg === "-f") {
|
|
5980
|
-
options.file =
|
|
6028
|
+
options.file = requireArgument(arg);
|
|
5981
6029
|
} else if (arg.startsWith("--file=")) {
|
|
5982
6030
|
options.file = arg.slice(7);
|
|
5983
6031
|
} else if (arg === "--type" || arg === "-t") {
|
|
5984
|
-
options.type =
|
|
6032
|
+
options.type = sourceType(requireArgument(arg));
|
|
5985
6033
|
} else if (arg.startsWith("--type=")) {
|
|
5986
|
-
options.type = arg.slice(7);
|
|
6034
|
+
options.type = sourceType(arg.slice(7));
|
|
5987
6035
|
} else if (arg === "--resource" || arg === "-r") {
|
|
5988
|
-
options.resource =
|
|
6036
|
+
options.resource = requireArgument(arg);
|
|
5989
6037
|
} else if (arg.startsWith("--resource=")) {
|
|
5990
6038
|
options.resource = arg.slice(11);
|
|
5991
6039
|
} else if (arg === "--out-dir" || arg === "-o" || arg === "--output") {
|
|
5992
|
-
options.outDir =
|
|
6040
|
+
options.outDir = requireArgument(arg);
|
|
5993
6041
|
} else if (arg.startsWith("--out-dir=")) {
|
|
5994
6042
|
options.outDir = arg.slice(10);
|
|
5995
6043
|
} else if (arg.startsWith("--output=")) {
|
|
5996
6044
|
options.outDir = arg.slice(9);
|
|
5997
6045
|
} else if (arg === "--primary-key" || arg === "-k") {
|
|
5998
|
-
options.primaryKey =
|
|
6046
|
+
options.primaryKey = requireArgument(arg);
|
|
5999
6047
|
} else if (arg.startsWith("--primary-key=")) {
|
|
6000
6048
|
options.primaryKey = arg.slice(14);
|
|
6001
6049
|
} else if (arg === "--fields") {
|
|
6002
|
-
options.fields =
|
|
6050
|
+
options.fields = requireArgument(arg);
|
|
6003
6051
|
} else if (arg.startsWith("--fields=")) {
|
|
6004
6052
|
options.fields = arg.slice(9);
|
|
6005
6053
|
} else if (arg === "--header" || arg === "-H") {
|
|
6006
|
-
|
|
6007
|
-
const colonIndex = headerLine.indexOf(":");
|
|
6008
|
-
if (colonIndex > 0) {
|
|
6009
|
-
const key = headerLine.slice(0, colonIndex).trim();
|
|
6010
|
-
const value = headerLine.slice(colonIndex + 1).trim();
|
|
6011
|
-
if (!options.headers)
|
|
6012
|
-
options.headers = {};
|
|
6013
|
-
options.headers[key] = value;
|
|
6014
|
-
}
|
|
6054
|
+
addHeader(requireArgument(arg));
|
|
6015
6055
|
} else if (arg.startsWith("--header=")) {
|
|
6016
|
-
|
|
6017
|
-
const colonIndex = headerLine.indexOf(":");
|
|
6018
|
-
if (colonIndex > 0) {
|
|
6019
|
-
const key = headerLine.slice(0, colonIndex).trim();
|
|
6020
|
-
const value = headerLine.slice(colonIndex + 1).trim();
|
|
6021
|
-
if (!options.headers)
|
|
6022
|
-
options.headers = {};
|
|
6023
|
-
options.headers[key] = value;
|
|
6024
|
-
}
|
|
6056
|
+
addHeader(arg.slice(9));
|
|
6025
6057
|
} else if (arg === "--method" || arg === "-m") {
|
|
6026
|
-
options.method = (
|
|
6058
|
+
options.method = requireArgument(arg).toUpperCase();
|
|
6027
6059
|
} else if (arg.startsWith("--method=")) {
|
|
6028
6060
|
options.method = arg.slice(9).toUpperCase();
|
|
6029
6061
|
} else if (arg === "--body" || arg === "-b") {
|
|
6030
|
-
options.body =
|
|
6062
|
+
options.body = requireArgument(arg);
|
|
6031
6063
|
} else if (arg.startsWith("--body=")) {
|
|
6032
6064
|
options.body = arg.slice(7);
|
|
6033
6065
|
} else if (arg === "--format") {
|
|
6034
|
-
options.format =
|
|
6066
|
+
options.format = outputFormat(requireArgument(arg));
|
|
6035
6067
|
} else if (arg.startsWith("--format=")) {
|
|
6036
|
-
options.format = arg.slice(9);
|
|
6068
|
+
options.format = outputFormat(arg.slice(9));
|
|
6037
6069
|
} else if (arg === "--help" || arg === "-h") {
|
|
6038
6070
|
printInferHelp();
|
|
6039
6071
|
process.exit(0);
|
|
@@ -6045,6 +6077,8 @@ function parseInferArguments(args) {
|
|
|
6045
6077
|
} else {
|
|
6046
6078
|
options.resource = arg;
|
|
6047
6079
|
}
|
|
6080
|
+
} else {
|
|
6081
|
+
throw new Error(`Unexpected argument: ${arg}`);
|
|
6048
6082
|
}
|
|
6049
6083
|
}
|
|
6050
6084
|
return options;
|
|
@@ -6080,6 +6114,22 @@ ${import_picocolors.default.bold("EXAMPLES:")}
|
|
|
6080
6114
|
svadmin infer --file sample-posts.json --resource posts --out-dir src/resources --write
|
|
6081
6115
|
`);
|
|
6082
6116
|
}
|
|
6117
|
+
function isRecord(value) {
|
|
6118
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6119
|
+
}
|
|
6120
|
+
function isOpenAPIDocument(value) {
|
|
6121
|
+
return isRecord(value) && (("openapi" in value) || ("swagger" in value) || ("paths" in value) && ("components" in value));
|
|
6122
|
+
}
|
|
6123
|
+
function isGraphQLDocument(value) {
|
|
6124
|
+
return isRecord(value) && (("__schema" in value) || isRecord(value["data"]) && ("__schema" in value["data"]) || Array.isArray(value["types"]));
|
|
6125
|
+
}
|
|
6126
|
+
function readSampleRecords(value) {
|
|
6127
|
+
const records = Array.isArray(value) ? value : isRecord(value) && Array.isArray(value["data"]) ? value["data"] : isRecord(value) && Array.isArray(value["items"]) ? value["items"] : [value];
|
|
6128
|
+
if (!records.every(isRecord)) {
|
|
6129
|
+
throw new Error("REST samples must be objects or arrays of objects");
|
|
6130
|
+
}
|
|
6131
|
+
return records;
|
|
6132
|
+
}
|
|
6083
6133
|
async function loadSourceData(options, customFetch = fetch) {
|
|
6084
6134
|
if (options.file) {
|
|
6085
6135
|
const filePath = path.resolve(process.cwd(), options.file);
|
|
@@ -6097,31 +6147,9 @@ async function loadSourceData(options, customFetch = fetch) {
|
|
|
6097
6147
|
sourceDescription: options.file
|
|
6098
6148
|
};
|
|
6099
6149
|
}
|
|
6150
|
+
let json;
|
|
6100
6151
|
try {
|
|
6101
|
-
|
|
6102
|
-
if (typeof json === "object" && json !== null && (("openapi" in json) || ("swagger" in json) || ("paths" in json) && ("components" in json))) {
|
|
6103
|
-
return {
|
|
6104
|
-
sourceType: "openapi",
|
|
6105
|
-
data: json,
|
|
6106
|
-
derivedResourceName: options.resource,
|
|
6107
|
-
sourceDescription: options.file
|
|
6108
|
-
};
|
|
6109
|
-
}
|
|
6110
|
-
if (typeof json === "object" && json !== null && (("__schema" in json) || ("data" in json) && json.data && ("__schema" in json.data) || ("types" in json) && Array.isArray(json.types))) {
|
|
6111
|
-
return {
|
|
6112
|
-
sourceType: "graphql",
|
|
6113
|
-
data: json,
|
|
6114
|
-
derivedResourceName: options.resource,
|
|
6115
|
-
sourceDescription: options.file
|
|
6116
|
-
};
|
|
6117
|
-
}
|
|
6118
|
-
const records = Array.isArray(json) ? json : Array.isArray(json.data) ? json.data : Array.isArray(json.items) ? json.items : [json];
|
|
6119
|
-
return {
|
|
6120
|
-
sourceType: "rest",
|
|
6121
|
-
data: records,
|
|
6122
|
-
derivedResourceName: options.resource || baseName,
|
|
6123
|
-
sourceDescription: options.file
|
|
6124
|
-
};
|
|
6152
|
+
json = JSON.parse(content);
|
|
6125
6153
|
} catch {
|
|
6126
6154
|
if (content.includes("type ") || content.includes("enum ") || content.includes("schema ")) {
|
|
6127
6155
|
return {
|
|
@@ -6133,12 +6161,35 @@ async function loadSourceData(options, customFetch = fetch) {
|
|
|
6133
6161
|
}
|
|
6134
6162
|
throw new Error(`Unable to parse file ${filePath}. Expected valid JSON or GraphQL SDL schema.`);
|
|
6135
6163
|
}
|
|
6164
|
+
if (isOpenAPIDocument(json)) {
|
|
6165
|
+
return {
|
|
6166
|
+
sourceType: "openapi",
|
|
6167
|
+
data: json,
|
|
6168
|
+
...options.resource === undefined ? {} : { derivedResourceName: options.resource },
|
|
6169
|
+
sourceDescription: options.file
|
|
6170
|
+
};
|
|
6171
|
+
}
|
|
6172
|
+
if (isGraphQLDocument(json)) {
|
|
6173
|
+
return {
|
|
6174
|
+
sourceType: "graphql",
|
|
6175
|
+
data: json,
|
|
6176
|
+
...options.resource === undefined ? {} : { derivedResourceName: options.resource },
|
|
6177
|
+
sourceDescription: options.file
|
|
6178
|
+
};
|
|
6179
|
+
}
|
|
6180
|
+
const records = readSampleRecords(json);
|
|
6181
|
+
return {
|
|
6182
|
+
sourceType: "rest",
|
|
6183
|
+
data: records,
|
|
6184
|
+
derivedResourceName: options.resource || baseName,
|
|
6185
|
+
sourceDescription: options.file
|
|
6186
|
+
};
|
|
6136
6187
|
}
|
|
6137
6188
|
if (options.url) {
|
|
6138
6189
|
const url = options.url;
|
|
6139
6190
|
const isGraphQL = options.type === "graphql" || url.endsWith("/graphql") || url.includes("/graphql?");
|
|
6140
6191
|
if (isGraphQL) {
|
|
6141
|
-
const
|
|
6192
|
+
const response = await customFetch(url, {
|
|
6142
6193
|
method: "POST",
|
|
6143
6194
|
headers: {
|
|
6144
6195
|
"Content-Type": "application/json",
|
|
@@ -6147,14 +6198,14 @@ async function loadSourceData(options, customFetch = fetch) {
|
|
|
6147
6198
|
},
|
|
6148
6199
|
body: JSON.stringify({ query: GRAPHQL_INTROSPECTION_QUERY })
|
|
6149
6200
|
});
|
|
6150
|
-
if (!
|
|
6151
|
-
throw new Error(`GraphQL introspection request failed: HTTP ${
|
|
6201
|
+
if (!response.ok) {
|
|
6202
|
+
throw new Error(`GraphQL introspection request failed: HTTP ${response.status} ${response.statusText}`);
|
|
6152
6203
|
}
|
|
6153
|
-
const
|
|
6204
|
+
const json = await response.json();
|
|
6154
6205
|
return {
|
|
6155
6206
|
sourceType: "graphql",
|
|
6156
|
-
data:
|
|
6157
|
-
derivedResourceName: options.resource,
|
|
6207
|
+
data: json,
|
|
6208
|
+
...options.resource === undefined ? {} : { derivedResourceName: options.resource },
|
|
6158
6209
|
sourceDescription: url
|
|
6159
6210
|
};
|
|
6160
6211
|
}
|
|
@@ -6164,25 +6215,25 @@ async function loadSourceData(options, customFetch = fetch) {
|
|
|
6164
6215
|
Accept: "application/json",
|
|
6165
6216
|
...options.headers ?? {}
|
|
6166
6217
|
},
|
|
6167
|
-
body: options.body
|
|
6218
|
+
...options.body === undefined ? {} : { body: options.body }
|
|
6168
6219
|
});
|
|
6169
6220
|
if (!response.ok) {
|
|
6170
6221
|
throw new Error(`HTTP request failed: HTTP ${response.status} ${response.statusText}`);
|
|
6171
6222
|
}
|
|
6172
6223
|
const json = await response.json();
|
|
6173
|
-
if (
|
|
6224
|
+
if (isOpenAPIDocument(json)) {
|
|
6174
6225
|
return {
|
|
6175
6226
|
sourceType: "openapi",
|
|
6176
6227
|
data: json,
|
|
6177
|
-
derivedResourceName: options.resource,
|
|
6228
|
+
...options.resource === undefined ? {} : { derivedResourceName: options.resource },
|
|
6178
6229
|
sourceDescription: url
|
|
6179
6230
|
};
|
|
6180
6231
|
}
|
|
6181
|
-
if (
|
|
6232
|
+
if (isGraphQLDocument(json)) {
|
|
6182
6233
|
return {
|
|
6183
6234
|
sourceType: "graphql",
|
|
6184
6235
|
data: json,
|
|
6185
|
-
derivedResourceName: options.resource,
|
|
6236
|
+
...options.resource === undefined ? {} : { derivedResourceName: options.resource },
|
|
6186
6237
|
sourceDescription: url
|
|
6187
6238
|
};
|
|
6188
6239
|
}
|
|
@@ -6196,7 +6247,7 @@ async function loadSourceData(options, customFetch = fetch) {
|
|
|
6196
6247
|
derivedName = "items";
|
|
6197
6248
|
}
|
|
6198
6249
|
}
|
|
6199
|
-
const records =
|
|
6250
|
+
const records = readSampleRecords(json);
|
|
6200
6251
|
return {
|
|
6201
6252
|
sourceType: "rest",
|
|
6202
6253
|
data: records,
|
|
@@ -6270,9 +6321,11 @@ async function executeInfer(options, customFetch = fetch) {
|
|
|
6270
6321
|
let resources;
|
|
6271
6322
|
const bundles = new Map;
|
|
6272
6323
|
if (loaded.sourceType === "openapi") {
|
|
6324
|
+
if (!isRecord(loaded.data))
|
|
6325
|
+
throw new Error("OpenAPI source must be an object");
|
|
6273
6326
|
resources = inferFromOpenAPI(loaded.data, {
|
|
6274
6327
|
primaryKey,
|
|
6275
|
-
|
|
6328
|
+
...options.resource ? { include: [options.resource] } : {}
|
|
6276
6329
|
});
|
|
6277
6330
|
for (const res of resources) {
|
|
6278
6331
|
bundles.set(res.name, generateResourceBundle(res));
|
|
@@ -6280,14 +6333,14 @@ async function executeInfer(options, customFetch = fetch) {
|
|
|
6280
6333
|
} else if (loaded.sourceType === "graphql") {
|
|
6281
6334
|
resources = inferFromGraphQL(loaded.data, {
|
|
6282
6335
|
primaryKey,
|
|
6283
|
-
|
|
6336
|
+
...options.resource ? { include: [options.resource] } : {}
|
|
6284
6337
|
});
|
|
6285
6338
|
for (const res of resources) {
|
|
6286
6339
|
bundles.set(res.name, generateResourceBundle(res));
|
|
6287
6340
|
}
|
|
6288
6341
|
} else {
|
|
6289
6342
|
const resName = options.resource || loaded.derivedResourceName || "items";
|
|
6290
|
-
const sampleArray =
|
|
6343
|
+
const sampleArray = readSampleRecords(loaded.data);
|
|
6291
6344
|
const inferRes = inferResource(resName, sampleArray, { primaryKey });
|
|
6292
6345
|
resources = [inferRes.resource];
|
|
6293
6346
|
bundles.set(resName, inferRes);
|
|
@@ -6312,7 +6365,7 @@ async function executeInfer(options, customFetch = fetch) {
|
|
|
6312
6365
|
files,
|
|
6313
6366
|
sourceDescription: loaded.sourceDescription,
|
|
6314
6367
|
wrote,
|
|
6315
|
-
outDir: options.outDir
|
|
6368
|
+
...options.outDir === undefined ? {} : { outDir: options.outDir }
|
|
6316
6369
|
};
|
|
6317
6370
|
}
|
|
6318
6371
|
function printInferResult(result) {
|
|
@@ -6409,14 +6462,33 @@ async function promptInferWizard() {
|
|
|
6409
6462
|
initial: true
|
|
6410
6463
|
}
|
|
6411
6464
|
]);
|
|
6412
|
-
|
|
6413
|
-
|
|
6465
|
+
return parseInferWizardAnswers(answers);
|
|
6466
|
+
}
|
|
6467
|
+
function parseInferWizardAnswers(answers) {
|
|
6468
|
+
if (!isRecord(answers))
|
|
6469
|
+
throw new Error("Invalid inference wizard answers");
|
|
6470
|
+
const sourceType = answers["sourceType"];
|
|
6471
|
+
if (sourceType !== "rest-file" && sourceType !== "rest-url" && sourceType !== "openapi" && sourceType !== "graphql") {
|
|
6472
|
+
throw new Error("Invalid inference wizard source type");
|
|
6473
|
+
}
|
|
6474
|
+
const targetPath = sourceType === "rest-file" || sourceType === "openapi" ? answers["pathOrUrl"] : answers["endpointUrl"];
|
|
6475
|
+
if (typeof targetPath !== "string" || !targetPath.trim()) {
|
|
6476
|
+
throw new Error("Inference wizard requires a URL or file path");
|
|
6477
|
+
}
|
|
6478
|
+
const outDir = answers["outDir"];
|
|
6479
|
+
const write = answers["write"];
|
|
6480
|
+
if (typeof outDir !== "string" || !outDir.trim() || typeof write !== "boolean") {
|
|
6481
|
+
throw new Error("Invalid inference wizard output options");
|
|
6482
|
+
}
|
|
6483
|
+
const isUrl = targetPath.startsWith("http://") || targetPath.startsWith("https://");
|
|
6484
|
+
if ((sourceType === "rest-url" || sourceType === "graphql") && !isUrl) {
|
|
6485
|
+
throw new Error("Inference wizard requires an HTTP(S) endpoint");
|
|
6486
|
+
}
|
|
6414
6487
|
return {
|
|
6415
|
-
|
|
6416
|
-
|
|
6417
|
-
|
|
6418
|
-
|
|
6419
|
-
write: answers.write
|
|
6488
|
+
...isUrl ? { url: targetPath } : { file: targetPath },
|
|
6489
|
+
type: sourceType === "graphql" ? "graphql" : sourceType === "openapi" ? "openapi" : "rest",
|
|
6490
|
+
outDir,
|
|
6491
|
+
write
|
|
6420
6492
|
};
|
|
6421
6493
|
}
|
|
6422
6494
|
async function inferCommand(args) {
|
|
@@ -6442,34 +6514,77 @@ import path2 from "node:path";
|
|
|
6442
6514
|
function parseGenerateArguments(args) {
|
|
6443
6515
|
return parseInferArguments(args);
|
|
6444
6516
|
}
|
|
6517
|
+
var fieldTypes = {
|
|
6518
|
+
text: true,
|
|
6519
|
+
number: true,
|
|
6520
|
+
boolean: true,
|
|
6521
|
+
date: true,
|
|
6522
|
+
select: true,
|
|
6523
|
+
multiselect: true,
|
|
6524
|
+
tags: true,
|
|
6525
|
+
textarea: true,
|
|
6526
|
+
richtext: true,
|
|
6527
|
+
image: true,
|
|
6528
|
+
images: true,
|
|
6529
|
+
json: true,
|
|
6530
|
+
relation: true,
|
|
6531
|
+
color: true,
|
|
6532
|
+
url: true,
|
|
6533
|
+
email: true,
|
|
6534
|
+
phone: true,
|
|
6535
|
+
currency: true,
|
|
6536
|
+
file: true,
|
|
6537
|
+
markdown: true,
|
|
6538
|
+
password: true,
|
|
6539
|
+
array: true,
|
|
6540
|
+
"tree-select": true,
|
|
6541
|
+
treeselect: true,
|
|
6542
|
+
cascader: true,
|
|
6543
|
+
transfer: true,
|
|
6544
|
+
rate: true,
|
|
6545
|
+
rating: true,
|
|
6546
|
+
avatar: true,
|
|
6547
|
+
copy: true,
|
|
6548
|
+
code: true
|
|
6549
|
+
};
|
|
6550
|
+
function isFieldType(value) {
|
|
6551
|
+
return Object.hasOwn(fieldTypes, value);
|
|
6552
|
+
}
|
|
6553
|
+
function parseManualFields(fields, primaryKey) {
|
|
6554
|
+
const keys = new Set;
|
|
6555
|
+
return fields.split(",").map((field) => {
|
|
6556
|
+
const [rawKey, rawType, extra] = field.split(":");
|
|
6557
|
+
const key = rawKey?.trim();
|
|
6558
|
+
const type = rawType === undefined ? "text" : rawType.trim();
|
|
6559
|
+
if (!key || extra !== undefined)
|
|
6560
|
+
throw new Error(`Invalid field definition: ${field}`);
|
|
6561
|
+
if (!isFieldType(type))
|
|
6562
|
+
throw new Error(`Invalid field type: ${type}`);
|
|
6563
|
+
if (keys.has(key))
|
|
6564
|
+
throw new Error(`Duplicate field: ${key}`);
|
|
6565
|
+
keys.add(key);
|
|
6566
|
+
return {
|
|
6567
|
+
key,
|
|
6568
|
+
label: key.charAt(0).toUpperCase() + key.slice(1),
|
|
6569
|
+
type,
|
|
6570
|
+
required: key === primaryKey
|
|
6571
|
+
};
|
|
6572
|
+
});
|
|
6573
|
+
}
|
|
6445
6574
|
async function generateCommand(args) {
|
|
6446
6575
|
const options = parseGenerateArguments(args);
|
|
6447
6576
|
if (options.resource && options.fields) {
|
|
6448
6577
|
const resourceName = options.resource;
|
|
6449
6578
|
const primaryKey = options.primaryKey ?? "id";
|
|
6450
|
-
const fieldDefs = options.fields
|
|
6451
|
-
|
|
6452
|
-
|
|
6453
|
-
|
|
6454
|
-
|
|
6455
|
-
|
|
6456
|
-
|
|
6457
|
-
|
|
6458
|
-
|
|
6459
|
-
});
|
|
6460
|
-
const mockSample = {};
|
|
6461
|
-
for (const f of fieldDefs) {
|
|
6462
|
-
mockSample[f.key] = f.type === "number" ? 1 : f.type === "boolean" ? true : `sample_${f.key}`;
|
|
6463
|
-
}
|
|
6464
|
-
const inferRes = inferResource(resourceName, [mockSample], { primaryKey });
|
|
6465
|
-
const resources = [
|
|
6466
|
-
{
|
|
6467
|
-
name: resourceName,
|
|
6468
|
-
label: resourceName.charAt(0).toUpperCase() + resourceName.slice(1),
|
|
6469
|
-
primaryKey,
|
|
6470
|
-
fields: fieldDefs
|
|
6471
|
-
}
|
|
6472
|
-
];
|
|
6579
|
+
const fieldDefs = parseManualFields(options.fields, primaryKey);
|
|
6580
|
+
const resource = {
|
|
6581
|
+
name: resourceName,
|
|
6582
|
+
label: resourceName.charAt(0).toUpperCase() + resourceName.slice(1),
|
|
6583
|
+
primaryKey,
|
|
6584
|
+
fields: fieldDefs
|
|
6585
|
+
};
|
|
6586
|
+
const resources = [resource];
|
|
6587
|
+
const inferRes = generateResourceBundle(resource);
|
|
6473
6588
|
const bundles = new Map([[resourceName, inferRes]]);
|
|
6474
6589
|
const files = planGeneratedFiles(resources, bundles, options.format);
|
|
6475
6590
|
let wrote = false;
|
|
@@ -6488,7 +6603,7 @@ async function generateCommand(args) {
|
|
|
6488
6603
|
files,
|
|
6489
6604
|
sourceDescription: `manual schema: ${options.fields}`,
|
|
6490
6605
|
wrote,
|
|
6491
|
-
outDir: options.outDir
|
|
6606
|
+
...options.outDir === undefined ? {} : { outDir: options.outDir }
|
|
6492
6607
|
});
|
|
6493
6608
|
return;
|
|
6494
6609
|
}
|
|
@@ -6781,17 +6896,17 @@ import {
|
|
|
6781
6896
|
unlinkSync,
|
|
6782
6897
|
writeFileSync
|
|
6783
6898
|
} from "node:fs";
|
|
6784
|
-
function parseDependencyMap(candidate,
|
|
6899
|
+
function parseDependencyMap(candidate, path) {
|
|
6785
6900
|
if (candidate === undefined)
|
|
6786
6901
|
return;
|
|
6787
|
-
assertStringRecord(candidate,
|
|
6902
|
+
assertStringRecord(candidate, path);
|
|
6788
6903
|
return { ...candidate };
|
|
6789
6904
|
}
|
|
6790
6905
|
function parseMaintainedPackageJson(packageJsonCandidate) {
|
|
6791
6906
|
assertJsonObject(packageJsonCandidate, "package.json");
|
|
6792
6907
|
const packageJson = structuredClone(packageJsonCandidate);
|
|
6793
|
-
const dependencies = parseDependencyMap(packageJsonCandidate
|
|
6794
|
-
const devDependencies = parseDependencyMap(packageJsonCandidate
|
|
6908
|
+
const dependencies = parseDependencyMap(packageJsonCandidate["dependencies"], "package.json.dependencies");
|
|
6909
|
+
const devDependencies = parseDependencyMap(packageJsonCandidate["devDependencies"], "package.json.devDependencies");
|
|
6795
6910
|
if (dependencies !== undefined)
|
|
6796
6911
|
packageJson.dependencies = dependencies;
|
|
6797
6912
|
if (devDependencies !== undefined)
|
|
@@ -6837,6 +6952,8 @@ function desiredDependencies(project, scaffold) {
|
|
|
6837
6952
|
}
|
|
6838
6953
|
for (const packName of selectedDependencyPacks(project, scaffold)) {
|
|
6839
6954
|
const pack = scaffold.svadmin.dependencyPacks[packName];
|
|
6955
|
+
if (pack === undefined)
|
|
6956
|
+
throw new Error(`Unknown dependency pack: ${packName}`);
|
|
6840
6957
|
for (const [packageName, version] of Object.entries(pack)) {
|
|
6841
6958
|
const existing = desired.get(packageName);
|
|
6842
6959
|
if (existing !== undefined && (existing.version !== version || existing.section !== "dependencies")) {
|
|
@@ -6947,12 +7064,12 @@ function duplicateDependencyIssue(desired, state) {
|
|
|
6947
7064
|
function doctorIssueForDependency(project, desired) {
|
|
6948
7065
|
const state = dependencyState(project, desired);
|
|
6949
7066
|
if (state.canonicalVersion === undefined) {
|
|
6950
|
-
return state.alternateVersion === undefined ? missingDependencyIssue(desired) : misplacedDependencyIssue(desired, state);
|
|
7067
|
+
return state.alternateVersion === undefined ? missingDependencyIssue(desired) : misplacedDependencyIssue(desired, { ...state, alternateVersion: state.alternateVersion });
|
|
6951
7068
|
}
|
|
6952
7069
|
if (state.canonicalVersion !== desired.version) {
|
|
6953
7070
|
return versionDependencyIssue(desired, state.canonicalVersion);
|
|
6954
7071
|
}
|
|
6955
|
-
return state.alternateVersion === undefined ? null : duplicateDependencyIssue(desired, state);
|
|
7072
|
+
return state.alternateVersion === undefined ? null : duplicateDependencyIssue(desired, { ...state, alternateVersion: state.alternateVersion });
|
|
6956
7073
|
}
|
|
6957
7074
|
function doctorProjectPackageJson(packageJsonCandidate, scaffold) {
|
|
6958
7075
|
const project = parseMaintainedPackageJson(packageJsonCandidate);
|
|
@@ -6960,10 +7077,10 @@ function doctorProjectPackageJson(packageJsonCandidate, scaffold) {
|
|
|
6960
7077
|
return issues.length === 0 ? { status: "clean", exitCode: 0, issues: [] } : { status: "issues", exitCode: 1, issues };
|
|
6961
7078
|
}
|
|
6962
7079
|
function upgradeChangeForDependency(desired, state) {
|
|
6963
|
-
if (state.canonicalVersion === undefined && state.alternateVersion === undefined) {
|
|
6964
|
-
return { action: "add", packageName: desired.packageName, to: desired.version, section: desired.section };
|
|
6965
|
-
}
|
|
6966
7080
|
if (state.canonicalVersion === undefined) {
|
|
7081
|
+
if (state.alternateVersion === undefined) {
|
|
7082
|
+
return { action: "add", packageName: desired.packageName, to: desired.version, section: desired.section };
|
|
7083
|
+
}
|
|
6967
7084
|
return {
|
|
6968
7085
|
action: "move",
|
|
6969
7086
|
packageName: desired.packageName,
|
|
@@ -6981,7 +7098,7 @@ function upgradeChangeForDependency(desired, state) {
|
|
|
6981
7098
|
from: state.canonicalVersion,
|
|
6982
7099
|
to: desired.version,
|
|
6983
7100
|
section: desired.section,
|
|
6984
|
-
|
|
7101
|
+
...state.alternateVersion === undefined ? {} : { previousSection: state.alternateSection }
|
|
6985
7102
|
};
|
|
6986
7103
|
}
|
|
6987
7104
|
function applyDesiredDependency(updatedPackageJson, desired) {
|
package/guidance/AGENTS.md
CHANGED
|
@@ -13,6 +13,45 @@ acceptance checks in this project.
|
|
|
13
13
|
custom markup or raw colors.
|
|
14
14
|
5. Verify loading, empty, partial, error, permission, success, and mobile states.
|
|
15
15
|
|
|
16
|
+
## Type-safe AI development
|
|
17
|
+
|
|
18
|
+
- Read the actual provider API and resource models before generating a page.
|
|
19
|
+
Do not invent fields or duplicate server/schema types.
|
|
20
|
+
- Define module-level contracts with `defineResource(name, schemas)` and derive
|
|
21
|
+
models from TypeBox `Static<typeof Schema>`. Do not duplicate models or use
|
|
22
|
+
declaration merging as proof of runtime validation.
|
|
23
|
+
- Use `useList`, `useOne`, `useShow`, and `useMany` with a resource contract.
|
|
24
|
+
`useTable` also infers query rows and `clientData`; `useInfiniteList` and `useSelect`
|
|
25
|
+
use the same contract. String resources, implicit routes, and data generics are
|
|
26
|
+
rejected by the default entry point. Use getters for reactive query inputs.
|
|
27
|
+
- Treat network responses, storage contents, and AI tool arguments as `unknown`
|
|
28
|
+
until validated at their boundary. Resource contracts validate CRUD requests
|
|
29
|
+
and responses automatically, but they do not authorize a mutation.
|
|
30
|
+
- Provide the full closed record schema with a required string/number `id`.
|
|
31
|
+
Supply create and update schemas explicitly; missing schemas disable writes.
|
|
32
|
+
Extra fields, nested `any`, open dictionaries, and unsupported schema kinds
|
|
33
|
+
are rejected. Do not coerce values or silently remove unexpected fields.
|
|
34
|
+
- Define custom calls with `defineCommand` and use `useCustom` or
|
|
35
|
+
`useCustomMutation`. Inputs and outputs must have schemas; URLs and methods
|
|
36
|
+
cannot be overridden by individual calls.
|
|
37
|
+
- Bind `useCreate`, `useUpdate`, `useDelete`, and their `Many` variants to an
|
|
38
|
+
explicit contract to infer mutation inputs and results. A bound mutation
|
|
39
|
+
cannot target another resource. `useUpdate` and `useDelete` require an ID.
|
|
40
|
+
- Keep required delete variables in `useDeleteMany` calls; both native batch
|
|
41
|
+
providers and single-record fallbacks receive them.
|
|
42
|
+
- Bind forms to a contract and a fixed create/edit action. `defaultValues` and
|
|
43
|
+
`setFieldValue` follow the operation schema, including required fields.
|
|
44
|
+
Edit backfill excludes record-only fields.
|
|
45
|
+
- Do not import `@svadmin/core/unsafe` in new application pages. It is reserved
|
|
46
|
+
for reviewed metadata-driven framework boundaries, not compatibility fallback.
|
|
47
|
+
- Do not automatically retry an invalid write response: inspect the structured
|
|
48
|
+
`writeMayHaveSucceeded` diagnostic and read back the server state first.
|
|
49
|
+
- Never silence a mismatch with `any`, double assertions, `@ts-ignore`, or a
|
|
50
|
+
broader index signature. Fix the source contract or validate and narrow it.
|
|
51
|
+
- Run the generated project's `bun run check` and focused tests after each
|
|
52
|
+
change. Cover invalid fields and payloads as well as successful paths.
|
|
53
|
+
Passing runtime tests alone does not prove compile-time type safety.
|
|
54
|
+
|
|
16
55
|
## Feedback invariant
|
|
17
56
|
|
|
18
57
|
`one event -> one primary feedback surface`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svadmin/create",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.7",
|
|
4
4
|
"description": "Scaffolding tool for svadmin projects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"@types/node": "^26.4.0",
|
|
28
28
|
"@types/prompts": "^2.4.9",
|
|
29
29
|
"typescript": "^7.0.2",
|
|
30
|
-
"@svadmin/core": "^0.
|
|
30
|
+
"@svadmin/core": "^0.52.0"
|
|
31
31
|
},
|
|
32
32
|
"keywords": [
|
|
33
33
|
"svadmin",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"license": "MIT",
|
|
40
40
|
"repository": {
|
|
41
41
|
"type": "git",
|
|
42
|
-
"url": "https://github.com/vibeunion/svadmin.git",
|
|
42
|
+
"url": "git+https://github.com/vibeunion/svadmin.git",
|
|
43
43
|
"directory": "packages/create-svadmin"
|
|
44
44
|
}
|
|
45
45
|
}
|
package/scaffold-manifest.json
CHANGED
|
@@ -10,20 +10,17 @@
|
|
|
10
10
|
"check": "svelte-check --tsconfig ./tsconfig.json"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@svadmin/core": "^0.
|
|
14
|
-
"@svadmin/ai-elements": "^0.
|
|
15
|
-
"@svadmin/ui": "^0.
|
|
13
|
+
"@svadmin/core": "^0.52.0",
|
|
14
|
+
"@svadmin/ai-elements": "^0.7.4",
|
|
15
|
+
"@svadmin/ui": "^0.72.0",
|
|
16
16
|
"@sinclair/typebox": "^0.34.52",
|
|
17
17
|
"@tanstack/svelte-query": "^6.1.48",
|
|
18
18
|
"@lucide/svelte": "^1.35.0"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@sveltejs/vite-plugin-svelte": "^7.3.0",
|
|
22
|
-
"@tailwindcss/vite": "^4.3.3",
|
|
23
22
|
"svelte": "^5.56.10",
|
|
24
23
|
"svelte-check": "^4.7.6",
|
|
25
|
-
"tailwindcss": "^4.3.3",
|
|
26
|
-
"tw-animate-css": "^1.4.0",
|
|
27
24
|
"typescript": "~6.0.3",
|
|
28
25
|
"vite": "^8.2.2"
|
|
29
26
|
},
|
|
@@ -33,16 +30,16 @@
|
|
|
33
30
|
"@refinedev/core": "^5.0.12"
|
|
34
31
|
},
|
|
35
32
|
"simple-rest": {
|
|
36
|
-
"@svadmin/simple-rest": "^0.
|
|
33
|
+
"@svadmin/simple-rest": "^0.10.6",
|
|
37
34
|
"@refinedev/simple-rest": "^6.0.1"
|
|
38
35
|
},
|
|
39
36
|
"supabase": {
|
|
40
|
-
"@svadmin/supabase": "^0.
|
|
37
|
+
"@svadmin/supabase": "^0.15.0",
|
|
41
38
|
"@supabase/supabase-js": "^2.112.4",
|
|
42
39
|
"@refinedev/supabase": "^6.0.2"
|
|
43
40
|
},
|
|
44
41
|
"graphql": {
|
|
45
|
-
"@svadmin/graphql": "^0.
|
|
42
|
+
"@svadmin/graphql": "^0.10.6",
|
|
46
43
|
"@refinedev/graphql": "^8.0.1",
|
|
47
44
|
"graphql-request": "^7.4.0",
|
|
48
45
|
"graphql": "^16.8.0"
|
package/template/src/app.css
CHANGED
|
@@ -1,8 +1,2 @@
|
|
|
1
|
-
@import "
|
|
2
|
-
@import "tw-animate-css";
|
|
3
|
-
|
|
4
|
-
/* Tailwind source tokens and component discovery are owned by SVAdmin packages. */
|
|
5
|
-
@import "@svadmin/ui/app.theme.css";
|
|
1
|
+
@import "@svadmin/ui/app.css";
|
|
6
2
|
@import "@svadmin/ai-elements/ai.css";
|
|
7
|
-
|
|
8
|
-
@custom-variant dark (&:is(.dark *));
|
|
@@ -1,79 +1,92 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { useList } from '@svadmin/core';
|
|
3
|
-
import {
|
|
3
|
+
import { posts, users, comments } from '../resource-contracts';
|
|
4
|
+
import { Button, DataState, MetricBlock } from '@svadmin/ui';
|
|
4
5
|
|
|
5
|
-
const postsQuery = useList({ resource:
|
|
6
|
-
const usersQuery = useList({ resource:
|
|
7
|
-
const commentsQuery = useList({ resource:
|
|
6
|
+
const postsQuery = useList({ resource: posts, pagination: { current: 1, pageSize: 1 } });
|
|
7
|
+
const usersQuery = useList({ resource: users, pagination: { current: 1, pageSize: 1 } });
|
|
8
|
+
const commentsQuery = useList({ resource: comments, pagination: { current: 1, pageSize: 1 } });
|
|
8
9
|
|
|
9
10
|
const stats = $derived([
|
|
10
|
-
{ label: 'Total Posts',
|
|
11
|
-
{ label: 'Total Users',
|
|
12
|
-
{ label: 'Total Comments',
|
|
11
|
+
{ label: 'Total Posts', query: postsQuery },
|
|
12
|
+
{ label: 'Total Users', query: usersQuery },
|
|
13
|
+
{ label: 'Total Comments', query: commentsQuery },
|
|
13
14
|
]);
|
|
14
15
|
|
|
15
|
-
const
|
|
16
|
+
const hasMetricError = $derived(stats.some(stat => stat.query.isError));
|
|
16
17
|
|
|
17
|
-
const recentPosts = useList({ resource:
|
|
18
|
-
const recentUsers = useList({ resource:
|
|
18
|
+
const recentPosts = useList({ resource: posts, pagination: { current: 1, pageSize: 5 } });
|
|
19
|
+
const recentUsers = useList({ resource: users, pagination: { current: 1, pageSize: 5 } });
|
|
19
20
|
</script>
|
|
20
21
|
|
|
21
|
-
<div class="space-y-
|
|
22
|
-
<h1 class="text-
|
|
22
|
+
<div class="space-y-4">
|
|
23
|
+
<h1 class="text-xl font-semibold text-foreground">Dashboard</h1>
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
{#if hasMetricError}
|
|
26
|
+
<DataState state="error" title="Some totals are unavailable" retry={() => {
|
|
27
|
+
for (const stat of stats) if (stat.query.isError && !stat.query.isFetching) void stat.query.refetch();
|
|
28
|
+
}} />
|
|
29
|
+
{/if}
|
|
25
30
|
<div class="grid gap-4 sm:grid-cols-3">
|
|
26
|
-
{#each stats as stat
|
|
27
|
-
<
|
|
28
|
-
<div class="flex h-12 w-12 items-center justify-center rounded-xl {stat.color}">
|
|
29
|
-
<stat.Icon class="h-6 w-6" />
|
|
30
|
-
</div>
|
|
31
|
-
<div>
|
|
32
|
-
<p class="text-sm text-gray-500">{stat.label}</p>
|
|
33
|
-
{#if isLoading}
|
|
34
|
-
<Loader2 class="mt-1 h-5 w-5 animate-spin text-gray-300" />
|
|
35
|
-
{:else}
|
|
36
|
-
<p class="text-2xl font-bold text-gray-900">{stat.value}</p>
|
|
37
|
-
{/if}
|
|
38
|
-
</div>
|
|
39
|
-
</div>
|
|
31
|
+
{#each stats as stat (stat.label)}
|
|
32
|
+
<MetricBlock label={stat.label} value={stat.query.isError ? '—' : stat.query.data?.total ?? '—'} loading={stat.query.isLoading} />
|
|
40
33
|
{/each}
|
|
41
34
|
</div>
|
|
42
35
|
|
|
43
36
|
<!-- Recent Data -->
|
|
44
37
|
<div class="grid gap-6 lg:grid-cols-2">
|
|
45
|
-
<
|
|
46
|
-
<div class="flex items-center justify-between border-b
|
|
47
|
-
<h2 class="font-semibold text-
|
|
38
|
+
<section class="min-w-0">
|
|
39
|
+
<div class="flex items-center justify-between border-b py-3">
|
|
40
|
+
<h2 class="font-semibold text-foreground">Recent Posts</h2>
|
|
48
41
|
<a href="#/posts" class="text-sm text-primary hover:underline">View all</a>
|
|
49
42
|
</div>
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
43
|
+
{#if recentPosts.isLoading}
|
|
44
|
+
<DataState state="loading" />
|
|
45
|
+
{:else if recentPosts.isError}
|
|
46
|
+
<DataState state="error" retry={() => { if (!recentPosts.isFetching) void recentPosts.refetch(); }} />
|
|
47
|
+
{:else}
|
|
48
|
+
<div class="divide-y">
|
|
49
|
+
{#each recentPosts.data?.data ?? [] as post, _i (_i)}
|
|
50
|
+
<div class="flex items-center justify-between py-3">
|
|
51
|
+
<div class="min-w-0">
|
|
52
|
+
<p class="break-words text-sm font-medium text-foreground">{post.title}</p>
|
|
53
|
+
<p class="text-xs text-muted-foreground">User #{post.userId}</p>
|
|
56
54
|
</div>
|
|
57
55
|
</div>
|
|
56
|
+
{:else}
|
|
57
|
+
<DataState state="empty">
|
|
58
|
+
{#snippet action()}<Button href="#/posts/create">Create post</Button>{/snippet}
|
|
59
|
+
</DataState>
|
|
58
60
|
{/each}
|
|
59
61
|
</div>
|
|
60
|
-
|
|
62
|
+
{/if}
|
|
63
|
+
</section>
|
|
61
64
|
|
|
62
|
-
<
|
|
63
|
-
<div class="flex items-center justify-between border-b
|
|
64
|
-
<h2 class="font-semibold text-
|
|
65
|
+
<section class="min-w-0">
|
|
66
|
+
<div class="flex items-center justify-between border-b py-3">
|
|
67
|
+
<h2 class="font-semibold text-foreground">Users</h2>
|
|
65
68
|
<a href="#/users" class="text-sm text-primary hover:underline">View all</a>
|
|
66
69
|
</div>
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
{#if recentUsers.isLoading}
|
|
71
|
+
<DataState state="loading" />
|
|
72
|
+
{:else if recentUsers.isError}
|
|
73
|
+
<DataState state="error" retry={() => { if (!recentUsers.isFetching) void recentUsers.refetch(); }} />
|
|
74
|
+
{:else}
|
|
75
|
+
<div class="divide-y">
|
|
76
|
+
{#each recentUsers.data?.data ?? [] as user, _i (_i)}
|
|
77
|
+
<div class="flex items-center justify-between py-3">
|
|
78
|
+
<div class="min-w-0">
|
|
79
|
+
<p class="break-words text-sm font-medium text-foreground">{user.name}</p>
|
|
80
|
+
<p class="break-words text-xs text-muted-foreground">{user.email}</p>
|
|
73
81
|
</div>
|
|
74
82
|
</div>
|
|
83
|
+
{:else}
|
|
84
|
+
<DataState state="empty">
|
|
85
|
+
{#snippet action()}<Button href="#/users/create">Create user</Button>{/snippet}
|
|
86
|
+
</DataState>
|
|
75
87
|
{/each}
|
|
76
88
|
</div>
|
|
77
|
-
|
|
89
|
+
{/if}
|
|
90
|
+
</section>
|
|
78
91
|
</div>
|
|
79
92
|
</div>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Type } from '@sinclair/typebox';
|
|
2
|
+
import { defineResource } from '@svadmin/core';
|
|
3
|
+
|
|
4
|
+
export const posts = defineResource('posts', {
|
|
5
|
+
record: Type.Object({
|
|
6
|
+
id: Type.Number(), userId: Type.Number(), title: Type.String(), body: Type.String(),
|
|
7
|
+
}),
|
|
8
|
+
});
|
|
9
|
+
export const users = defineResource('users', {
|
|
10
|
+
record: Type.Object({
|
|
11
|
+
id: Type.Number(), name: Type.String(), username: Type.String(), email: Type.String(),
|
|
12
|
+
phone: Type.String(), website: Type.String(),
|
|
13
|
+
address: Type.Object({
|
|
14
|
+
street: Type.String(), suite: Type.String(), city: Type.String(), zipcode: Type.String(),
|
|
15
|
+
geo: Type.Object({ lat: Type.String(), lng: Type.String() }),
|
|
16
|
+
}),
|
|
17
|
+
company: Type.Object({ name: Type.String(), catchPhrase: Type.String(), bs: Type.String() }),
|
|
18
|
+
}),
|
|
19
|
+
});
|
|
20
|
+
export const comments = defineResource('comments', {
|
|
21
|
+
record: Type.Object({
|
|
22
|
+
id: Type.Number(), postId: Type.Number(), name: Type.String(), email: Type.String(), body: Type.String(),
|
|
23
|
+
}),
|
|
24
|
+
});
|
|
25
|
+
export const todos = defineResource('todos', {
|
|
26
|
+
record: Type.Object({
|
|
27
|
+
id: Type.Number(), userId: Type.Number(), title: Type.String(), completed: Type.Boolean(),
|
|
28
|
+
}),
|
|
29
|
+
});
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// Example resource definitions — generic demo data (JSONPlaceholder-style)
|
|
2
2
|
|
|
3
3
|
import type { ResourceDefinition } from '@svadmin/core';
|
|
4
|
+
import { posts, users, comments, todos } from './resource-contracts';
|
|
4
5
|
|
|
5
6
|
export const resources: ResourceDefinition[] = [
|
|
6
7
|
{
|
|
7
8
|
name: 'posts',
|
|
9
|
+
contract: posts,
|
|
8
10
|
label: 'Posts',
|
|
9
11
|
icon: 'file-text',
|
|
10
12
|
fields: [
|
|
@@ -18,6 +20,7 @@ export const resources: ResourceDefinition[] = [
|
|
|
18
20
|
|
|
19
21
|
{
|
|
20
22
|
name: 'users',
|
|
23
|
+
contract: users,
|
|
21
24
|
label: 'Users',
|
|
22
25
|
icon: 'users',
|
|
23
26
|
canCreate: false,
|
|
@@ -34,6 +37,7 @@ export const resources: ResourceDefinition[] = [
|
|
|
34
37
|
|
|
35
38
|
{
|
|
36
39
|
name: 'comments',
|
|
40
|
+
contract: comments,
|
|
37
41
|
label: 'Comments',
|
|
38
42
|
icon: 'message-circle',
|
|
39
43
|
fields: [
|
|
@@ -48,6 +52,7 @@ export const resources: ResourceDefinition[] = [
|
|
|
48
52
|
|
|
49
53
|
{
|
|
50
54
|
name: 'todos',
|
|
55
|
+
contract: todos,
|
|
51
56
|
label: 'Todos',
|
|
52
57
|
icon: 'check-square',
|
|
53
58
|
fields: [
|
package/template/vite.config.ts
CHANGED