@antelopejs/dms 0.13.1 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/frontend-vue/layers/dms-auth/app/build/composables/useAuthFormError.ts +1 -1
- package/frontend-vue/layers/dms-auth/app/custom-pages/auth/signup.vue +1 -1
- package/frontend-vue/layers/dms-core/app/composables/{useApiError.ts → resolveApiError.ts} +0 -16
- package/frontend-vue/layers/dms-core/app/composables/useFieldErrors.ts +1 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +1 -4
- package/frontend-vue/scripts/test-api-error-presentation.mjs +2 -66
- package/package.json +1 -2
- package/frontend-vue/scripts/codemods/api-error-presenter.mjs +0 -196
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.14.0
|
|
4
|
+
|
|
5
|
+
[compare changes](https://github.com/AntelopeJS/dms/compare/v0.13.1...v0.14.0)
|
|
6
|
+
|
|
7
|
+
### 💅 Refactors
|
|
8
|
+
|
|
9
|
+
- **frontend:** ⚠️ Remove API error migration helpers ([#638](https://github.com/AntelopeJS/dms/pull/638))
|
|
10
|
+
|
|
11
|
+
#### ⚠️ Breaking Changes
|
|
12
|
+
|
|
13
|
+
- **frontend:** ⚠️ Remove API error migration helpers ([#638](https://github.com/AntelopeJS/dms/pull/638))
|
|
14
|
+
|
|
15
|
+
### ❤️ Contributors
|
|
16
|
+
|
|
17
|
+
- Antony Rizzitelli ([@Upd4ting](http://github.com/Upd4ting))
|
|
18
|
+
|
|
3
19
|
## v0.13.1
|
|
4
20
|
|
|
5
21
|
[compare changes](https://github.com/AntelopeJS/dms/compare/v0.13.0...v0.13.1)
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
type FieldErrorCode,
|
|
5
5
|
resolveFieldErrors,
|
|
6
6
|
} from "#dms-core/app/composables/useFieldErrors";
|
|
7
|
-
import { resolveApiErrorMessage } from "#dms-core/app/composables/
|
|
7
|
+
import { resolveApiErrorMessage } from "#dms-core/app/composables/resolveApiError";
|
|
8
8
|
|
|
9
9
|
export interface AuthFormError {
|
|
10
10
|
title: string;
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
useLiveFormErrors,
|
|
16
16
|
useLocalizedSchema,
|
|
17
17
|
} from "#dms-core/app/composables/useFormValidation";
|
|
18
|
-
import { resolveApiErrorMessage } from "#dms-core/app/composables/
|
|
18
|
+
import { resolveApiErrorMessage } from "#dms-core/app/composables/resolveApiError";
|
|
19
19
|
import { isString } from "#dms-core/app/utils/type-check";
|
|
20
20
|
|
|
21
21
|
const MIN_NAME_LENGTH = 2;
|
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import { isObject, isString } from "../utils/type-check";
|
|
2
2
|
|
|
3
|
-
interface Options {
|
|
4
|
-
context?: string;
|
|
5
|
-
title?: string;
|
|
6
|
-
description?: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
3
|
interface ApiError {
|
|
10
4
|
statusCode?: number;
|
|
11
5
|
status?: number;
|
|
@@ -68,13 +62,3 @@ export function resolveApiErrorMessage(
|
|
|
68
62
|
): string {
|
|
69
63
|
return isApiError(error) ? resolveDescription(error) : FALLBACK_ERROR_KEY;
|
|
70
64
|
}
|
|
71
|
-
|
|
72
|
-
/** @deprecated Capture useApiErrorPresenter during setup instead. */
|
|
73
|
-
export function useApiError(
|
|
74
|
-
error: Parameters<typeof resolveApiErrorTitle>[0],
|
|
75
|
-
options?: Options,
|
|
76
|
-
) {
|
|
77
|
-
useApiErrorPresenter().presentError(error, {
|
|
78
|
-
fallbackKey: options?.description || options?.title || FALLBACK_ERROR_KEY,
|
|
79
|
-
});
|
|
80
|
-
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { nextTick, reactive } from "vue";
|
|
2
2
|
import { type I18nTranslate } from "./translation/useTranslation";
|
|
3
|
-
import { resolveApiErrorMessage } from "./
|
|
3
|
+
import { resolveApiErrorMessage } from "./resolveApiError";
|
|
4
4
|
import { isBoolean, isNumber, isObject, isString } from "../utils/type-check";
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -25,10 +25,7 @@ export interface ActionTargetsConfig {
|
|
|
25
25
|
/** Called once a target changed something (an API call, a closed form). */
|
|
26
26
|
refreshCallback?: () => void;
|
|
27
27
|
/** Toasts a failure no dialog shows. */
|
|
28
|
-
handleApiError: (
|
|
29
|
-
error: Parameters<typeof useApiError>[0],
|
|
30
|
-
title?: string,
|
|
31
|
-
) => void;
|
|
28
|
+
handleApiError: (error: unknown, title?: string) => void;
|
|
32
29
|
/** The rows a drawer or modal opened on a row steps through (J / K). */
|
|
33
30
|
rowNavigation?: RowNavigationSource;
|
|
34
31
|
/** Where a `deepLink` action writes the row it has open in the URL. */
|
|
@@ -11,12 +11,7 @@ import { tmpdir } from "node:os";
|
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
import ts from "typescript";
|
|
14
|
-
import {
|
|
15
|
-
analyzeApiErrorScript,
|
|
16
|
-
apiErrorScripts,
|
|
17
|
-
} from "./check-api-error-presentation.mjs";
|
|
18
|
-
import { migrateApiErrorSource } from "./codemods/api-error-presenter.mjs";
|
|
19
|
-
import { parse } from "vue/compiler-sfc";
|
|
14
|
+
import { analyzeApiErrorScript } from "./check-api-error-presentation.mjs";
|
|
20
15
|
|
|
21
16
|
const rejected = [
|
|
22
17
|
`try { await save() } catch (error) { toast.add({ title: t('save.failed'), color: 'error' }) }`,
|
|
@@ -49,58 +44,6 @@ for (const source of allowed)
|
|
|
49
44
|
source,
|
|
50
45
|
);
|
|
51
46
|
|
|
52
|
-
const vue = `<script setup lang="ts">
|
|
53
|
-
const toast = useToast();
|
|
54
|
-
async function save() {
|
|
55
|
-
try { await write() } catch (failure) { toast.add({ title: t("save.failed"), color: "error" }); }
|
|
56
|
-
}
|
|
57
|
-
</script><template><button @click="save">Save</button></template>`;
|
|
58
|
-
const migrated = migrateApiErrorSource("fixture.vue", vue);
|
|
59
|
-
assert.ok(migrated.rewritten);
|
|
60
|
-
assert.deepEqual(migrated.manual, []);
|
|
61
|
-
assert.ok(
|
|
62
|
-
migrated.output.includes(
|
|
63
|
-
'presentError(failure, { fallbackKey: "save.failed" })',
|
|
64
|
-
),
|
|
65
|
-
);
|
|
66
|
-
assert.equal(
|
|
67
|
-
migrateApiErrorSource("fixture.vue", migrated.output).rewritten,
|
|
68
|
-
false,
|
|
69
|
-
);
|
|
70
|
-
for (const script of apiErrorScripts("fixture.vue", migrated.output, { parse }))
|
|
71
|
-
assert.equal(
|
|
72
|
-
analyzeApiErrorScript(script.source, "fixture.vue", ts).length,
|
|
73
|
-
0,
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
const composable = `export function useSave() { return async () => { try { await save() } catch (error) { toast.add({ title: t('save.failed'), color: 'error' }); } } }`;
|
|
77
|
-
const converted = migrateApiErrorSource("fixture.ts", composable);
|
|
78
|
-
assert.ok(
|
|
79
|
-
converted.output.indexOf("const { presentError }") <
|
|
80
|
-
converted.output.indexOf("return async"),
|
|
81
|
-
);
|
|
82
|
-
assert.ok(converted.output.includes("import { useApiErrorPresenter }"));
|
|
83
|
-
|
|
84
|
-
for (const source of [
|
|
85
|
-
`async function save() { try {} catch (error) { toast.add({ title: t('save.failed'), color: 'error' }); } }`,
|
|
86
|
-
`export function useSave() { try {} catch { toast.add({ title: t('save.failed'), color: 'error' }); } }`,
|
|
87
|
-
`export function useSave() { try {} catch (error) { toast.update(id, { title: t('save.failed'), color: 'error' }); } }`,
|
|
88
|
-
`export function useSave() { try {} catch (error) { toast.add({ title: t(dynamic), color: 'error' }); } }`,
|
|
89
|
-
`export function useSave() { try {} catch (error) { toast.add({ title: t('save.failed'), color: color() }); } }`,
|
|
90
|
-
`export function useSave() { try {} catch (error) { toast.add({ title: t('save.failed'), icon: icon(), color: 'error' }); } }`,
|
|
91
|
-
`<script>try {} catch (error) { toast.add({ title: t('save.failed'), color: 'error' }); }</script>`,
|
|
92
|
-
]) {
|
|
93
|
-
const file = source.startsWith("<script") ? "manual.vue" : "manual.ts";
|
|
94
|
-
const result = migrateApiErrorSource(file, source);
|
|
95
|
-
assert.equal(result.output, source);
|
|
96
|
-
assert.deepEqual(Object.keys(result.manual[0]), [
|
|
97
|
-
"file",
|
|
98
|
-
"line",
|
|
99
|
-
"reason",
|
|
100
|
-
"expression",
|
|
101
|
-
]);
|
|
102
|
-
assert.equal(result.manual[0].reason, "manual-error-presentation");
|
|
103
|
-
}
|
|
104
47
|
const linkRoot = mkdtempSync(join(tmpdir(), "dms-api-error-cli-"));
|
|
105
48
|
const appRoot = mkdtempSync(
|
|
106
49
|
fileURLToPath(new URL("../.api-error-cli-", import.meta.url)),
|
|
@@ -120,15 +63,8 @@ try {
|
|
|
120
63
|
{ encoding: "utf8" },
|
|
121
64
|
);
|
|
122
65
|
assert.notEqual(checker.status, 0, "checker fails through a symlinked path");
|
|
123
|
-
const codemod = spawnSync(
|
|
124
|
-
process.execPath,
|
|
125
|
-
[join(scripts, "codemods", "api-error-presenter.mjs"), join(appRoot, "app")],
|
|
126
|
-
{ encoding: "utf8", cwd: appRoot },
|
|
127
|
-
);
|
|
128
|
-
assert.equal(codemod.status, 0, codemod.stderr);
|
|
129
|
-
assert.match(codemod.stdout, /"reason":"manual-error-presentation"/);
|
|
130
66
|
} finally {
|
|
131
67
|
rmSync(linkRoot, { recursive: true, force: true });
|
|
132
68
|
rmSync(appRoot, { recursive: true, force: true });
|
|
133
69
|
}
|
|
134
|
-
console.log("PASS API error invariant
|
|
70
|
+
console.log("PASS API error invariant fixtures");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antelopejs/dms",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "AntelopeJS DMS. Plug in your PostgreSQL, MongoDB or RethinkDB database and your coding agent builds a back office and analytics dashboard as typed TypeScript. Real-time updates on the data the DMS manages.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"antelopejs",
|
|
@@ -124,7 +124,6 @@
|
|
|
124
124
|
},
|
|
125
125
|
"./package.json": "./package.json",
|
|
126
126
|
"./checks/api-error-presentation": "./frontend-vue/scripts/check-api-error-presentation.mjs",
|
|
127
|
-
"./codemods/api-error-presenter": "./frontend-vue/scripts/codemods/api-error-presenter.mjs",
|
|
128
127
|
"./interfaces/*": null
|
|
129
128
|
},
|
|
130
129
|
"publishConfig": {
|
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
import { readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { createRequire } from "node:module";
|
|
3
|
-
import { resolve } from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
import {
|
|
6
|
-
apiErrorSources,
|
|
7
|
-
apiErrorScripts,
|
|
8
|
-
analyzeApiErrorScript,
|
|
9
|
-
} from "../check-api-error-presentation.mjs";
|
|
10
|
-
|
|
11
|
-
const require = createRequire(resolve(process.cwd(), "package.json"));
|
|
12
|
-
const tools = {
|
|
13
|
-
ts: require("typescript"),
|
|
14
|
-
parse: require("vue/compiler-sfc").parse,
|
|
15
|
-
};
|
|
16
|
-
const { ts } = tools;
|
|
17
|
-
|
|
18
|
-
function translatedKey(expression) {
|
|
19
|
-
if (
|
|
20
|
-
!expression ||
|
|
21
|
-
!ts.isCallExpression(expression) ||
|
|
22
|
-
!["t", "processI18n"].includes(expression.expression.getText())
|
|
23
|
-
)
|
|
24
|
-
return null;
|
|
25
|
-
const key = expression.arguments[0];
|
|
26
|
-
return expression.arguments.length === 1 && key && ts.isStringLiteral(key)
|
|
27
|
-
? key.text
|
|
28
|
-
: null;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function literalKey(payload) {
|
|
32
|
-
if (!payload || !ts.isObjectLiteralExpression(payload)) return null;
|
|
33
|
-
const allowed = new Set(["title", "description", "color", "icon"]);
|
|
34
|
-
if (
|
|
35
|
-
payload.properties.some(
|
|
36
|
-
(property) =>
|
|
37
|
-
!ts.isPropertyAssignment(property) ||
|
|
38
|
-
!allowed.has(property.name.getText()) ||
|
|
39
|
-
(["title", "description"].includes(property.name.getText())
|
|
40
|
-
? translatedKey(property.initializer) === null
|
|
41
|
-
: !(
|
|
42
|
-
ts.isStringLiteral(property.initializer) ||
|
|
43
|
-
(property.name.getText() === "color" &&
|
|
44
|
-
/^Color\.(error|warning|info)$/.test(
|
|
45
|
-
property.initializer.getText(),
|
|
46
|
-
))
|
|
47
|
-
)),
|
|
48
|
-
)
|
|
49
|
-
)
|
|
50
|
-
return null;
|
|
51
|
-
const title =
|
|
52
|
-
payload.properties.find(
|
|
53
|
-
(property) => property.name.getText() === "description",
|
|
54
|
-
)?.initializer ??
|
|
55
|
-
payload.properties.find((property) => property.name.getText() === "title")
|
|
56
|
-
?.initializer;
|
|
57
|
-
return translatedKey(title);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function composableScope(node) {
|
|
61
|
-
let outer = null;
|
|
62
|
-
for (let parent = node.parent; parent; parent = parent.parent) {
|
|
63
|
-
if (
|
|
64
|
-
(ts.isFunctionDeclaration(parent) ||
|
|
65
|
-
ts.isFunctionExpression(parent) ||
|
|
66
|
-
ts.isArrowFunction(parent)) &&
|
|
67
|
-
ts.isBlock(parent.body)
|
|
68
|
-
)
|
|
69
|
-
outer = parent;
|
|
70
|
-
}
|
|
71
|
-
const name = outer?.name ?? outer?.parent?.name;
|
|
72
|
-
return name && ts.isIdentifier(name) && /^use[A-Z]/.test(name.text)
|
|
73
|
-
? outer.body
|
|
74
|
-
: null;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** Rewrites supported setup/composable catches and returns unsupported cases. */
|
|
78
|
-
export function migrateApiErrorSource(file, source) {
|
|
79
|
-
const edits = [];
|
|
80
|
-
const manual = [];
|
|
81
|
-
for (const script of apiErrorScripts(file, source, tools)) {
|
|
82
|
-
const calls = analyzeApiErrorScript(script.source, file, ts);
|
|
83
|
-
let capture = false;
|
|
84
|
-
for (const call of calls) {
|
|
85
|
-
const fallbackKey = literalKey(call.payload);
|
|
86
|
-
const standalone = ts.isExpressionStatement(call.node.parent);
|
|
87
|
-
const safeCatch = call.failure && /^[A-Za-z_$][\w$]*$/.test(call.failure);
|
|
88
|
-
const collision = /\b(?:presentError|useApiErrorPresenter)\b/.test(
|
|
89
|
-
script.source,
|
|
90
|
-
);
|
|
91
|
-
const hasSetup = file.endsWith(".vue")
|
|
92
|
-
? script.isSetup
|
|
93
|
-
: composableScope(call.node);
|
|
94
|
-
if (
|
|
95
|
-
fallbackKey &&
|
|
96
|
-
standalone &&
|
|
97
|
-
safeCatch &&
|
|
98
|
-
hasSetup &&
|
|
99
|
-
call.method === "add" &&
|
|
100
|
-
!collision
|
|
101
|
-
) {
|
|
102
|
-
edits.push({
|
|
103
|
-
start: script.offset + call.node.getStart(),
|
|
104
|
-
end: script.offset + call.node.end,
|
|
105
|
-
text: `presentError(${call.failure}, { fallbackKey: ${JSON.stringify(fallbackKey)} })`,
|
|
106
|
-
});
|
|
107
|
-
capture = true;
|
|
108
|
-
} else {
|
|
109
|
-
manual.push({
|
|
110
|
-
file,
|
|
111
|
-
line: source
|
|
112
|
-
.slice(0, script.offset + call.node.getStart())
|
|
113
|
-
.split("\n").length,
|
|
114
|
-
reason: "manual-error-presentation",
|
|
115
|
-
expression: call.node.getText(call.tree),
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
if (capture) {
|
|
120
|
-
const tree = ts.createSourceFile(
|
|
121
|
-
file,
|
|
122
|
-
script.source,
|
|
123
|
-
ts.ScriptTarget.Latest,
|
|
124
|
-
true,
|
|
125
|
-
);
|
|
126
|
-
const imports = tree.statements.filter(ts.isImportDeclaration);
|
|
127
|
-
const offset = imports.at(-1)?.end ?? 0;
|
|
128
|
-
const declaration = file.endsWith(".vue")
|
|
129
|
-
? "\nconst { presentError } = useApiErrorPresenter();\n"
|
|
130
|
-
: `\nimport { useApiErrorPresenter } from "#dms-core/app/composables/useApiErrorPresenter";\n`;
|
|
131
|
-
if (file.endsWith(".vue"))
|
|
132
|
-
edits.push({
|
|
133
|
-
start: script.offset + offset,
|
|
134
|
-
end: script.offset + offset,
|
|
135
|
-
text: declaration,
|
|
136
|
-
});
|
|
137
|
-
else {
|
|
138
|
-
const scopes = new Set();
|
|
139
|
-
for (const call of calls) {
|
|
140
|
-
if (
|
|
141
|
-
!edits.some(
|
|
142
|
-
(edit) => edit.start === script.offset + call.node.getStart(),
|
|
143
|
-
)
|
|
144
|
-
)
|
|
145
|
-
continue;
|
|
146
|
-
const outer = composableScope(call.node);
|
|
147
|
-
if (outer) scopes.add(outer.getStart() + 1);
|
|
148
|
-
}
|
|
149
|
-
for (const start of scopes)
|
|
150
|
-
edits.push({
|
|
151
|
-
start: script.offset + start,
|
|
152
|
-
end: script.offset + start,
|
|
153
|
-
text: "\nconst { presentError } = useApiErrorPresenter();\n",
|
|
154
|
-
});
|
|
155
|
-
edits.push({
|
|
156
|
-
start: script.offset + offset,
|
|
157
|
-
end: script.offset + offset,
|
|
158
|
-
text: declaration,
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
let output = source;
|
|
164
|
-
for (const edit of edits.sort((a, b) => b.start - a.start))
|
|
165
|
-
output = output.slice(0, edit.start) + edit.text + output.slice(edit.end);
|
|
166
|
-
return { output, rewritten: output !== source, manual };
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/** Dry-run by default; writes only source cases accepted by the bounded migration. */
|
|
170
|
-
export function migrateApiErrorFile(file, write = false) {
|
|
171
|
-
const result = migrateApiErrorSource(file, readFileSync(file, "utf8"));
|
|
172
|
-
if (write && result.rewritten) writeFileSync(file, result.output);
|
|
173
|
-
return result;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (
|
|
177
|
-
process.argv[1] &&
|
|
178
|
-
realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))
|
|
179
|
-
) {
|
|
180
|
-
const roots = process.argv
|
|
181
|
-
.slice(2)
|
|
182
|
-
.filter((argument) => argument !== "--write");
|
|
183
|
-
if (!roots.length)
|
|
184
|
-
throw new Error(
|
|
185
|
-
"Pass app directories. Add --write to apply safe rewrites. Manual cases are JSONL on stdout.",
|
|
186
|
-
);
|
|
187
|
-
for (const file of roots.flatMap(apiErrorSources)) {
|
|
188
|
-
const result = migrateApiErrorFile(file, process.argv.includes("--write"));
|
|
189
|
-
for (const entry of result.manual)
|
|
190
|
-
process.stdout.write(`${JSON.stringify(entry)}\n`);
|
|
191
|
-
if (result.rewritten)
|
|
192
|
-
process.stderr.write(
|
|
193
|
-
`${process.argv.includes("--write") ? "rewrote" : "would rewrite"} ${file}\n`,
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
}
|