@antelopejs/dms 0.12.1 → 0.13.1
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 +40 -0
- package/data/dbip-country-lite.mmdb +0 -0
- package/data/module-store/catalog.json +0 -6
- package/dist/index.js +13 -3
- package/dist/index.js.map +1 -1
- package/dist/utils/ensure-default-tenant.js +7 -2
- package/dist/utils/ensure-default-tenant.js.map +1 -1
- package/frontend-vue/dms.frontend.ts +2 -0
- package/frontend-vue/layers/dms-auth/app/build/components/AuthFormAlert.vue +1 -1
- package/frontend-vue/layers/dms-auth/app/build/composables/useAuthFormError.ts +11 -14
- package/frontend-vue/layers/dms-auth/app/composables/useOAuthErrorToast.ts +3 -8
- package/frontend-vue/layers/dms-auth/app/custom-pages/auth/accounts.vue +3 -2
- package/frontend-vue/layers/dms-core/app/build/composables/api-error.ts +145 -0
- package/frontend-vue/layers/dms-core/app/build/composables/auth/usePermissionPreview.ts +5 -1
- package/frontend-vue/layers/dms-core/app/composables/auth/useAuthFetch.ts +0 -2
- package/frontend-vue/layers/dms-core/app/composables/page/useSiteLayout.ts +6 -5
- package/frontend-vue/layers/dms-core/app/composables/useApiError.ts +3 -11
- package/frontend-vue/layers/dms-core/app/composables/useApiErrorPresenter.ts +159 -0
- package/frontend-vue/layers/dms-core/app/composables/useFieldErrors.ts +14 -29
- package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionBar.vue +3 -1
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationInboxDisplay.vue +3 -3
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/profile/ProfilePersonalInfo.vue +4 -4
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityEmail.vue +7 -9
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityPassword.vue +4 -4
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecuritySessions.vue +6 -7
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityTwoFactor.vue +2 -1
- package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useAccountData.ts +14 -34
- package/frontend-vue/layers/dms-layout/app/build/composables/settings/security/useSecurityFormat.ts +3 -11
- package/frontend-vue/layers/dms-layout/app/composables/page/useModuleStore.ts +9 -0
- package/frontend-vue/layers/dms-layout/app/composables/page/useModuleStoreActions.ts +13 -28
- package/frontend-vue/layers/dms-layout/app/custom-pages/modules-store.vue +2 -2
- package/frontend-vue/layers/dms-layout/app/custom-pages/settings/roles.vue +5 -2
- package/frontend-vue/layers/dms-layout/app/plugins/command-palette-session.client.ts +4 -4
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +0 -4
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +0 -4
- package/frontend-vue/layers/dms-ui/app/build/components/blocks/BlockStatus.vue +2 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/actions/actionTargetRequests.ts +6 -10
- package/frontend-vue/layers/dms-ui/app/build/composables/chart/groupPoints.ts +13 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/actionError.ts +10 -139
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/useActionConfirm.ts +2 -10
- package/frontend-vue/layers/dms-ui/app/build/composables/form/formState.ts +2 -12
- package/frontend-vue/layers/dms-ui/app/build/composables/table/useTableConfigClipboard.ts +7 -10
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/tableRowActionBulkRequests.ts +11 -10
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/tableRowActionValues.ts +1 -4
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/tableViewGrouping.ts +7 -10
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useExpandedRowDetails.ts +4 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRecordLink.ts +4 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +7 -18
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/recordLink.ts +3 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/writeError.ts +9 -5
- package/frontend-vue/layers/dms-ui/app/build/utils/fetchNotFound.ts +4 -3
- package/frontend-vue/layers/dms-ui/app/components/block-preview/BlockPreview.vue +6 -1
- package/frontend-vue/layers/dms-ui/app/components/blocks/BannerBlock.vue +3 -1
- package/frontend-vue/layers/dms-ui/app/components/chart/ChartCard.vue +3 -2
- package/frontend-vue/layers/dms-ui/app/components/confirm/ConfirmModal.vue +24 -19
- package/frontend-vue/layers/dms-ui/app/components/form/components/Cascader.vue +2 -6
- package/frontend-vue/layers/dms-ui/app/components/form/components/File.vue +6 -2
- package/frontend-vue/layers/dms-ui/app/components/form/components/Image.vue +5 -1
- package/frontend-vue/layers/dms-ui/app/components/form/components/ImagePresentation.vue +2 -2
- package/frontend-vue/layers/dms-ui/app/components/form/components/InputAddress.vue +2 -6
- package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +2 -6
- package/frontend-vue/layers/dms-ui/app/components/form/components/imageItems.ts +1 -0
- package/frontend-vue/layers/dms-ui/app/components/guard/Guard.vue +2 -1
- package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanBoard.vue +13 -11
- package/frontend-vue/layers/dms-ui/app/composables/confirm/types.ts +6 -6
- package/frontend-vue/layers/dms-ui/app/composables/form/useForm.ts +14 -25
- package/frontend-vue/layers/dms-ui/app/composables/global/keyboardPlatform.ts +36 -9
- package/frontend-vue/layers/dms-ui/app/composables/useExportJob.ts +13 -59
- package/frontend-vue/layers/dms-ui/app/plugins/kbd-platform.ts +13 -0
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +1 -0
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +1 -0
- package/frontend-vue/package.json +1 -3
- package/frontend-vue/scripts/check-api-error-presentation.mjs +248 -0
- package/frontend-vue/scripts/check-invariants.mjs +23 -0
- package/frontend-vue/scripts/codemods/api-error-presenter.mjs +196 -0
- package/frontend-vue/scripts/test-api-error-presentation.mjs +134 -0
- package/package.json +10 -4
- package/skills/dms-module/REFERENCE.md +3 -1
- package/skills/dms-module/SKILL.md +7 -2
- package/skills/dms-project/SKILL.md +9 -3
- package/frontend-vue/layers/dms-core/app/composables/useReadOnlyRefusal.ts +0 -49
|
@@ -0,0 +1,196 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import {
|
|
4
|
+
mkdirSync,
|
|
5
|
+
mkdtempSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
symlinkSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
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";
|
|
20
|
+
|
|
21
|
+
const rejected = [
|
|
22
|
+
`try { await save() } catch (error) { toast.add({ title: t('save.failed'), color: 'error' }) }`,
|
|
23
|
+
`save().catch(error => { toast.update(id, { color: 'error' }) })`,
|
|
24
|
+
`save().then(done, error => { toast.add({ color: Color.error }) })`,
|
|
25
|
+
`const notices = useToast(); try {} catch (error) { notices.add({ color: 'warning' }) }`,
|
|
26
|
+
`const { add: notify } = useToast(); try {} catch (error) { notify({ color: FAILURE_COLOR }) }`,
|
|
27
|
+
`const notices = useToast(); const alias = notices; try {} catch (error) { alias.update(id, payload) }`,
|
|
28
|
+
`import { ERROR_COLOR } from './colors'; try {} catch (error) { toast.add({ color: ERROR_COLOR }) }`,
|
|
29
|
+
`const payload = { color: 'error' }; try {} catch (error) { toast.add(payload) }`,
|
|
30
|
+
`save().catch(error => { toast.add({ title: t('save.failed') }) })`,
|
|
31
|
+
];
|
|
32
|
+
for (const source of rejected)
|
|
33
|
+
assert.equal(
|
|
34
|
+
analyzeApiErrorScript(source, "fixture.ts", ts).length,
|
|
35
|
+
1,
|
|
36
|
+
source,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const allowed = [
|
|
40
|
+
`try { await save(); toast.add({ color: 'success' }) } catch (error) { presentError(error, { fallbackKey: 'save.failed' }) }`,
|
|
41
|
+
`try { await track() } catch {} finally { loading.value = false }`,
|
|
42
|
+
`toast.add({ color: 'info', title: t('product.notice') })`,
|
|
43
|
+
`save().catch(() => { toast.add({ color: 'success' }) })`,
|
|
44
|
+
];
|
|
45
|
+
for (const source of allowed)
|
|
46
|
+
assert.equal(
|
|
47
|
+
analyzeApiErrorScript(source, "fixture.ts", ts).length,
|
|
48
|
+
0,
|
|
49
|
+
source,
|
|
50
|
+
);
|
|
51
|
+
|
|
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
|
+
const linkRoot = mkdtempSync(join(tmpdir(), "dms-api-error-cli-"));
|
|
105
|
+
const appRoot = mkdtempSync(
|
|
106
|
+
fileURLToPath(new URL("../.api-error-cli-", import.meta.url)),
|
|
107
|
+
);
|
|
108
|
+
try {
|
|
109
|
+
const scripts = join(linkRoot, "scripts");
|
|
110
|
+
symlinkSync(fileURLToPath(new URL(".", import.meta.url)), scripts, "dir");
|
|
111
|
+
writeFileSync(join(appRoot, "package.json"), "{}");
|
|
112
|
+
mkdirSync(join(appRoot, "app"));
|
|
113
|
+
writeFileSync(
|
|
114
|
+
join(appRoot, "app", "manual.ts"),
|
|
115
|
+
`export function useSave() { try {} catch (error) { toast.add({ title: t(dynamic), color: 'error' }); } }`,
|
|
116
|
+
);
|
|
117
|
+
const checker = spawnSync(
|
|
118
|
+
process.execPath,
|
|
119
|
+
[join(scripts, "check-api-error-presentation.mjs"), join(appRoot, "app")],
|
|
120
|
+
{ encoding: "utf8" },
|
|
121
|
+
);
|
|
122
|
+
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
|
+
} finally {
|
|
131
|
+
rmSync(linkRoot, { recursive: true, force: true });
|
|
132
|
+
rmSync(appRoot, { recursive: true, force: true });
|
|
133
|
+
}
|
|
134
|
+
console.log("PASS API error invariant and codemod fixtures");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antelopejs/dms",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.1",
|
|
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",
|
|
@@ -71,11 +71,11 @@
|
|
|
71
71
|
"2fa": "^0.1.2",
|
|
72
72
|
"@antelopejs/interface-api": ">=0.0.15 <1.0.0",
|
|
73
73
|
"@antelopejs/interface-api-util": ">=0.1.3 <1.0.0",
|
|
74
|
-
"@antelopejs/interface-core": ">=0.2.
|
|
74
|
+
"@antelopejs/interface-core": ">=0.2.6 <1.0.0",
|
|
75
75
|
"@antelopejs/interface-data-api": ">=0.5.0 <1.0.0",
|
|
76
76
|
"@antelopejs/interface-database": ">=0.1.11 <1.0.0",
|
|
77
77
|
"@antelopejs/interface-database-decorators": ">=0.1.10 <1.0.0",
|
|
78
|
-
"@antelopejs/interface-dms": ">=0.10.
|
|
78
|
+
"@antelopejs/interface-dms": ">=0.10.2 <0.11.0",
|
|
79
79
|
"@antelopejs/interface-email": ">=0.0.6 <1.0.0",
|
|
80
80
|
"@antelopejs/interface-file-storage": ">=0.1.4 <1.0.0",
|
|
81
81
|
"archiver": "^7.0.1",
|
|
@@ -114,7 +114,8 @@
|
|
|
114
114
|
"release-it-changelogen": "^0.1.0",
|
|
115
115
|
"rimraf": "^6.1.3",
|
|
116
116
|
"tsc-alias": "^1.9.1",
|
|
117
|
-
"typescript": "^6.0.3"
|
|
117
|
+
"typescript": "^6.0.3",
|
|
118
|
+
"vue": "^3.5.18"
|
|
118
119
|
},
|
|
119
120
|
"exports": {
|
|
120
121
|
".": {
|
|
@@ -122,6 +123,8 @@
|
|
|
122
123
|
"default": "./dist/index.js"
|
|
123
124
|
},
|
|
124
125
|
"./package.json": "./package.json",
|
|
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",
|
|
125
128
|
"./interfaces/*": null
|
|
126
129
|
},
|
|
127
130
|
"publishConfig": {
|
|
@@ -145,6 +148,8 @@
|
|
|
145
148
|
"build": "pnpm --filter @antelopejs/interface-dms build && rimraf dist && tsc && tsc-alias",
|
|
146
149
|
"dev": "ajs project run --inspect -w -p playground",
|
|
147
150
|
"frontend:dev": "pnpm --dir playground frontend:dev",
|
|
151
|
+
"frontend:build": "pnpm --dir playground frontend:build",
|
|
152
|
+
"frontend:start": "pnpm --dir playground frontend:start",
|
|
148
153
|
"typecheck": "node scripts/unless-prebuilt.mjs pnpm --filter @antelopejs/interface-dms build && tsc --noEmit",
|
|
149
154
|
"test": "node scripts/unless-prebuilt.mjs pnpm run build && NODE_OPTIONS=\"${NODE_OPTIONS:+$NODE_OPTIONS }--require=\\\"$PWD/scripts/backend-runtime-boundaries.cjs\\\"\" ajs module test .",
|
|
150
155
|
"test:unit": "node scripts/unless-prebuilt.mjs pnpm run build && node scripts/backend-checks.cjs && DMS_TEST_DIR=dist/test/unit NODE_OPTIONS=\"${NODE_OPTIONS:+$NODE_OPTIONS }--require=\\\"$PWD/scripts/backend-runtime-boundaries.cjs\\\"\" ajs module test .",
|
|
@@ -156,6 +161,7 @@
|
|
|
156
161
|
"knip:all": "knip",
|
|
157
162
|
"docs:catalog": "node scripts/docs-catalog.mjs",
|
|
158
163
|
"docs:catalog:check": "node scripts/docs-catalog.mjs --check",
|
|
164
|
+
"check:frontend-declarations": "node scripts/check-frontend-declarations.mjs",
|
|
159
165
|
"docs:screenshots": "node scripts/docs-screenshots.mjs",
|
|
160
166
|
"realtime:tabs-check": "node scripts/realtime-tabs-check.mjs"
|
|
161
167
|
}
|
|
@@ -33,6 +33,7 @@ first-party modules follow `dms-<name>` purely for discoverability.
|
|
|
33
33
|
```jsonc
|
|
34
34
|
{
|
|
35
35
|
"main": "dist/index.js", "types": "dist/index.d.ts",
|
|
36
|
+
"dmsFrontend": "dist/frontend-module.js", // exports frontendModule(config, { dev }) (omit if no frontend)
|
|
36
37
|
"antelopeJs": {
|
|
37
38
|
"baseUrl": "dist",
|
|
38
39
|
"implements": ["<your-interface-package>"], // the interface THIS module provides (omit/[] if none)
|
|
@@ -80,7 +81,8 @@ AntelopeJS — see the **ajs-author-interface** and **ajs-consume-interface** sk
|
|
|
80
81
|
`frontend-vue/` (components registered from the root `dms.frontend.ts`), `skills/<name>/SKILL.md`,
|
|
81
82
|
and the manifest (`antelopeJs` block with `skills`, plus the interface package's `exports`).
|
|
82
83
|
4. **Wire the basics**: `RegisterModule({ id, title, icon, … })`, one `@RegisterPage()` page,
|
|
83
|
-
|
|
84
|
+
`src/frontend-module.ts` (`frontendModule`, declared as `dmsFrontend`) passed to
|
|
85
|
+
`AddFrontendModule`. Add the `$dms_<name>.*` keys to **every** locale file under
|
|
84
86
|
`frontend-vue/i18n/locales/`.
|
|
85
87
|
5. **Load and verify**: add the module to a consumer project's `antelope.config.ts` (the
|
|
86
88
|
backend's `playground` is the reference consumer) as a `package`/`git`/local-path module,
|
|
@@ -31,7 +31,9 @@ manifest, including the `exports` entries for an interface subpath).
|
|
|
31
31
|
when they provide none (`dms-lang`, `dms-saas`); consumed interfaces are plain `dependencies`
|
|
32
32
|
(no manifest field). Typical dependencies: `@antelopejs/dms` plus
|
|
33
33
|
`@antelopejs/interface-{core,api,api-util,auth,database,database-decorators}`. The frontend is
|
|
34
|
-
attached
|
|
34
|
+
attached by the `AddFrontendModule` call in `construct` and declared as `"dmsFrontend": "dist/frontend-module.js"`
|
|
35
|
+
in `package.json`: `ajs dms dev`, `build` and `start` read that declaration, not the backend, and leave
|
|
36
|
+
out a module without it.
|
|
35
37
|
|
|
36
38
|
**Ship a skill.** Add `skills/<module-name>/SKILL.md` (frontmatter `name`, a trigger-rich
|
|
37
39
|
`description`, `icon`, `category`, `tags`) saying when and how to use the module, list `"skills"` in
|
|
@@ -81,7 +83,10 @@ export function destroy(): void { /* deactivate subscriptions */ }
|
|
|
81
83
|
|
|
82
84
|
`RegisterModule` runs at import time (top level), so the module exists before its pages
|
|
83
85
|
register — it returns the module's root `CategoryInfo`. `AddFrontendModule` is what surfaces the
|
|
84
|
-
module's frontend; without it the module is backend-only.
|
|
86
|
+
module's frontend; without it the module is backend-only. Keep its argument in a pure
|
|
87
|
+
`export function frontendModule(config, { dev })` in `src/frontend-module.ts` (no runtime imports:
|
|
88
|
+
`ajs dms build` loads that file on its own) and call `AddFrontendModule(frontendModule(config, …))`,
|
|
89
|
+
as the official modules do; `dmsFrontend` names its compiled file.
|
|
85
90
|
|
|
86
91
|
`renderer` names the framework (Vue 3), not a loader release. The `@antelopejs/dms-frontend`
|
|
87
92
|
releases the frontend module runs on go in the frontend module's own `package.json`, as
|
|
@@ -37,7 +37,8 @@ infrastructure it needs) and whose own `src/` is the `local` module. It provides
|
|
|
37
37
|
its `package.json` has **no `antelopeJs.implements`** (that field marks a distributable module);
|
|
38
38
|
deps are `@antelopejs/dms` + the interface libs, plus `@antelopejs/dms-frontend` (the
|
|
39
39
|
`ajs dms` CLI) as a dependency — the template keeps it in `dependencies`; a devDependency also works
|
|
40
|
-
since it is only a CLI. The frontend attaches
|
|
40
|
+
since it is only a CLI. The frontend attaches with `AddFrontendModule` and a `dmsFrontend`
|
|
41
|
+
declaration in `package.json` (below), and the DMS
|
|
41
42
|
**auto-registers its base dashboard layer** (shell, login, component vocabulary) — a pages-only
|
|
42
43
|
project needs no `frontend-vue/` of its own.
|
|
43
44
|
|
|
@@ -133,8 +134,13 @@ export function stop(): void {}
|
|
|
133
134
|
export function getConfig(): Config { return globalConfig; }
|
|
134
135
|
```
|
|
135
136
|
|
|
136
|
-
Frontend wiring is the `AddFrontendModule` call
|
|
137
|
-
|
|
137
|
+
Frontend wiring is the `AddFrontendModule` call plus `"dmsFrontend": "dist/frontend-module.js"` in
|
|
138
|
+
the project's `package.json` — no DMS config entry. `ajs dms dev`, `build` and `start` read the project,
|
|
139
|
+
not the backend's registrations, and leave out a frontend that is not declared:
|
|
140
|
+
move the registration into a pure `export function frontendModule()` in `src/frontend-module.ts`
|
|
141
|
+
(no runtime imports), call `AddFrontendModule(frontendModule())` from `start()`, and declare it. A
|
|
142
|
+
**project** attaches the layer from `start()`; a **distributable module** attaches it in `construct`
|
|
143
|
+
(see **dms-module**).
|
|
138
144
|
Full layer authoring: `docs/02.building/08.frontend-layer.md`.
|
|
139
145
|
|
|
140
146
|
## What you register (delegates to the other skills)
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { isFunction, isObject } from "../utils/type-check";
|
|
2
|
-
|
|
3
|
-
const READ_ONLY_STATUS = 403;
|
|
4
|
-
const READ_ONLY_HEADER = "x-dms-readonly";
|
|
5
|
-
const READ_ONLY_MARK = "1";
|
|
6
|
-
const READ_ONLY_TOAST_ID = "dms-read-only";
|
|
7
|
-
const READ_ONLY_STATE_KEY = "dms.readOnly.presentedResponses";
|
|
8
|
-
|
|
9
|
-
function refusedResponse(
|
|
10
|
-
failure: Parameters<typeof isObject>[0],
|
|
11
|
-
): object | undefined {
|
|
12
|
-
if (!failure || !isObject(failure)) return;
|
|
13
|
-
const response = failure.response;
|
|
14
|
-
if (!response || !isObject(response) || response.status !== READ_ONLY_STATUS)
|
|
15
|
-
return;
|
|
16
|
-
const headers = response.headers;
|
|
17
|
-
if (!headers || !isObject(headers) || !isFunction(headers.get)) return;
|
|
18
|
-
return headers.get(READ_ONLY_HEADER) === READ_ONLY_MARK
|
|
19
|
-
? response
|
|
20
|
-
: undefined;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Captures app-local refusal presentation. The returned consumer shows one
|
|
25
|
-
* generic toast per marked response and returns whether it owns presentation.
|
|
26
|
-
* A consumed refusal remains a failed operation; callers skip only local notices.
|
|
27
|
-
*/
|
|
28
|
-
export function useReadOnlyRefusal() {
|
|
29
|
-
const toast = useToast();
|
|
30
|
-
const { processApiMessage } = getAppTranslation();
|
|
31
|
-
const presented = useDmsState(
|
|
32
|
-
READ_ONLY_STATE_KEY,
|
|
33
|
-
() => new WeakSet<object>(),
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
return (failure: Parameters<typeof refusedResponse>[0]): boolean => {
|
|
37
|
-
const response = refusedResponse(failure);
|
|
38
|
-
if (!response) return false;
|
|
39
|
-
if (presented.value.has(response)) return true;
|
|
40
|
-
presented.value.add(response);
|
|
41
|
-
toast.add({
|
|
42
|
-
id: READ_ONLY_TOAST_ID,
|
|
43
|
-
color: "warning",
|
|
44
|
-
icon: "i-ph-lock-simple",
|
|
45
|
-
title: processApiMessage("error.read_only_demo"),
|
|
46
|
-
});
|
|
47
|
-
return true;
|
|
48
|
-
};
|
|
49
|
-
}
|