flowdular 0.4.3 → 0.5.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/dist/distribution.js +52 -2
- package/dist/index.js +274 -26
- package/package.json +2 -2
package/dist/distribution.js
CHANGED
|
@@ -18,6 +18,9 @@ var RegistryError = class extends Error {
|
|
|
18
18
|
var ALLOWED = Object.freeze({ allowed: true });
|
|
19
19
|
var NO_POLICIES = Object.freeze([]);
|
|
20
20
|
|
|
21
|
+
// ../kernel/src/lifecycle.ts
|
|
22
|
+
var ALLOWED2 = Object.freeze({ allowed: true });
|
|
23
|
+
|
|
21
24
|
// ../kernel/src/version-range.ts
|
|
22
25
|
var VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
23
26
|
var COMPARATOR = /^(\^|~|>=|<=|>|<|=)?\s*(.+)$/;
|
|
@@ -1682,8 +1685,55 @@ var WEB_PAGE_PATH = new RegExp(
|
|
|
1682
1685
|
`^(?:/|(?:/(?:${SEGMENT}|:[a-z][a-zA-Z0-9]*))+)$`
|
|
1683
1686
|
);
|
|
1684
1687
|
|
|
1688
|
+
// ../contracts/src/branding.ts
|
|
1689
|
+
var DEFAULT_APPLICATION_BRANDING = Object.freeze({
|
|
1690
|
+
appName: "Flowdular",
|
|
1691
|
+
documentTitle: "",
|
|
1692
|
+
description: "",
|
|
1693
|
+
ogImageUrl: "/og.png",
|
|
1694
|
+
faviconUrl: "/favicon.svg",
|
|
1695
|
+
themeColor: "#141B2E",
|
|
1696
|
+
logoUrl: ""
|
|
1697
|
+
});
|
|
1698
|
+
var BRANDING_NAME_MAX = 64;
|
|
1699
|
+
var BRANDING_TITLE_MAX = 120;
|
|
1700
|
+
var BRANDING_DESCRIPTION_MAX = 200;
|
|
1701
|
+
var BRANDING_URL_MAX = 512;
|
|
1702
|
+
var TEXT_BODY = `[^<>"'\\\\\\p{Cc}]`;
|
|
1703
|
+
var BRANDING_NAME_PATTERN = `${TEXT_BODY}{1,${BRANDING_NAME_MAX}}`;
|
|
1704
|
+
var BRANDING_TITLE_PATTERN = `|${TEXT_BODY}{1,${BRANDING_TITLE_MAX}}`;
|
|
1705
|
+
var BRANDING_DESCRIPTION_PATTERN = `|${TEXT_BODY}{1,${BRANDING_DESCRIPTION_MAX}}`;
|
|
1706
|
+
var ASSET_URL = `/(?![/\\\\])[^\\s<>"'\\\\]{0,${BRANDING_URL_MAX - 1}}|https://[^\\s<>"'\\\\/]{1,255}(?:/[^\\s<>"']{0,${BRANDING_URL_MAX - 1}})?`;
|
|
1707
|
+
var BRANDING_ASSET_URL_PATTERN = ASSET_URL;
|
|
1708
|
+
var BRANDING_OPTIONAL_ASSET_URL_PATTERN = `|${ASSET_URL}`;
|
|
1709
|
+
var BRANDING_COLOR_PATTERN = "#[0-9a-fA-F]{6}";
|
|
1710
|
+
var FIELDS = {
|
|
1711
|
+
appName: { pattern: BRANDING_NAME_PATTERN, max: BRANDING_NAME_MAX },
|
|
1712
|
+
documentTitle: {
|
|
1713
|
+
pattern: BRANDING_TITLE_PATTERN,
|
|
1714
|
+
max: BRANDING_TITLE_MAX
|
|
1715
|
+
},
|
|
1716
|
+
description: {
|
|
1717
|
+
pattern: BRANDING_DESCRIPTION_PATTERN,
|
|
1718
|
+
max: BRANDING_DESCRIPTION_MAX
|
|
1719
|
+
},
|
|
1720
|
+
ogImageUrl: { pattern: BRANDING_ASSET_URL_PATTERN, max: BRANDING_URL_MAX },
|
|
1721
|
+
faviconUrl: { pattern: BRANDING_ASSET_URL_PATTERN, max: BRANDING_URL_MAX },
|
|
1722
|
+
themeColor: { pattern: BRANDING_COLOR_PATTERN, max: 7 },
|
|
1723
|
+
logoUrl: {
|
|
1724
|
+
pattern: BRANDING_OPTIONAL_ASSET_URL_PATTERN,
|
|
1725
|
+
max: BRANDING_URL_MAX
|
|
1726
|
+
}
|
|
1727
|
+
};
|
|
1728
|
+
var EXPRESSIONS = new Map(
|
|
1729
|
+
Object.entries(FIELDS).map(([key, field]) => [
|
|
1730
|
+
key,
|
|
1731
|
+
new RegExp(`^(?:${field.pattern})$`, "u")
|
|
1732
|
+
])
|
|
1733
|
+
);
|
|
1734
|
+
|
|
1685
1735
|
// ../contracts/src/index.ts
|
|
1686
|
-
var PLATFORM_API_VERSION = "0.1.
|
|
1736
|
+
var PLATFORM_API_VERSION = "0.1.29";
|
|
1687
1737
|
|
|
1688
1738
|
// ../kernel/src/module-compatibility.ts
|
|
1689
1739
|
function satisfiesModuleVersion(version, range) {
|
|
@@ -2371,7 +2421,7 @@ import { dirname as dirname6, join as join6, relative as relative5 } from "node:
|
|
|
2371
2421
|
import { createRequire } from "node:module";
|
|
2372
2422
|
import { readFile as readFile5 } from "node:fs/promises";
|
|
2373
2423
|
import { dirname as dirname4, join as join3 } from "node:path";
|
|
2374
|
-
var SDK_VERSION = "0.
|
|
2424
|
+
var SDK_VERSION = "0.5.1";
|
|
2375
2425
|
function npmPackage(specifier) {
|
|
2376
2426
|
return specifier.startsWith("@") ? specifier.split("/").slice(0, 2).join("/") : specifier.split("/")[0];
|
|
2377
2427
|
}
|
package/dist/index.js
CHANGED
|
@@ -1786,8 +1786,55 @@ var WEB_PAGE_PATH = new RegExp(
|
|
|
1786
1786
|
`^(?:/|(?:/(?:${SEGMENT}|:[a-z][a-zA-Z0-9]*))+)$`
|
|
1787
1787
|
);
|
|
1788
1788
|
|
|
1789
|
+
// ../contracts/src/branding.ts
|
|
1790
|
+
var DEFAULT_APPLICATION_BRANDING = Object.freeze({
|
|
1791
|
+
appName: "Flowdular",
|
|
1792
|
+
documentTitle: "",
|
|
1793
|
+
description: "",
|
|
1794
|
+
ogImageUrl: "/og.png",
|
|
1795
|
+
faviconUrl: "/favicon.svg",
|
|
1796
|
+
themeColor: "#141B2E",
|
|
1797
|
+
logoUrl: ""
|
|
1798
|
+
});
|
|
1799
|
+
var BRANDING_NAME_MAX = 64;
|
|
1800
|
+
var BRANDING_TITLE_MAX = 120;
|
|
1801
|
+
var BRANDING_DESCRIPTION_MAX = 200;
|
|
1802
|
+
var BRANDING_URL_MAX = 512;
|
|
1803
|
+
var TEXT_BODY = `[^<>"'\\\\\\p{Cc}]`;
|
|
1804
|
+
var BRANDING_NAME_PATTERN = `${TEXT_BODY}{1,${BRANDING_NAME_MAX}}`;
|
|
1805
|
+
var BRANDING_TITLE_PATTERN = `|${TEXT_BODY}{1,${BRANDING_TITLE_MAX}}`;
|
|
1806
|
+
var BRANDING_DESCRIPTION_PATTERN = `|${TEXT_BODY}{1,${BRANDING_DESCRIPTION_MAX}}`;
|
|
1807
|
+
var ASSET_URL = `/(?![/\\\\])[^\\s<>"'\\\\]{0,${BRANDING_URL_MAX - 1}}|https://[^\\s<>"'\\\\/]{1,255}(?:/[^\\s<>"']{0,${BRANDING_URL_MAX - 1}})?`;
|
|
1808
|
+
var BRANDING_ASSET_URL_PATTERN = ASSET_URL;
|
|
1809
|
+
var BRANDING_OPTIONAL_ASSET_URL_PATTERN = `|${ASSET_URL}`;
|
|
1810
|
+
var BRANDING_COLOR_PATTERN = "#[0-9a-fA-F]{6}";
|
|
1811
|
+
var FIELDS = {
|
|
1812
|
+
appName: { pattern: BRANDING_NAME_PATTERN, max: BRANDING_NAME_MAX },
|
|
1813
|
+
documentTitle: {
|
|
1814
|
+
pattern: BRANDING_TITLE_PATTERN,
|
|
1815
|
+
max: BRANDING_TITLE_MAX
|
|
1816
|
+
},
|
|
1817
|
+
description: {
|
|
1818
|
+
pattern: BRANDING_DESCRIPTION_PATTERN,
|
|
1819
|
+
max: BRANDING_DESCRIPTION_MAX
|
|
1820
|
+
},
|
|
1821
|
+
ogImageUrl: { pattern: BRANDING_ASSET_URL_PATTERN, max: BRANDING_URL_MAX },
|
|
1822
|
+
faviconUrl: { pattern: BRANDING_ASSET_URL_PATTERN, max: BRANDING_URL_MAX },
|
|
1823
|
+
themeColor: { pattern: BRANDING_COLOR_PATTERN, max: 7 },
|
|
1824
|
+
logoUrl: {
|
|
1825
|
+
pattern: BRANDING_OPTIONAL_ASSET_URL_PATTERN,
|
|
1826
|
+
max: BRANDING_URL_MAX
|
|
1827
|
+
}
|
|
1828
|
+
};
|
|
1829
|
+
var EXPRESSIONS = new Map(
|
|
1830
|
+
Object.entries(FIELDS).map(([key, field]) => [
|
|
1831
|
+
key,
|
|
1832
|
+
new RegExp(`^(?:${field.pattern})$`, "u")
|
|
1833
|
+
])
|
|
1834
|
+
);
|
|
1835
|
+
|
|
1789
1836
|
// ../contracts/src/index.ts
|
|
1790
|
-
var PLATFORM_API_VERSION = "0.1.
|
|
1837
|
+
var PLATFORM_API_VERSION = "0.1.29";
|
|
1791
1838
|
|
|
1792
1839
|
// src/module-templates.ts
|
|
1793
1840
|
function packageSuffix(id) {
|
|
@@ -2112,11 +2159,23 @@ function buildModel(spec) {
|
|
|
2112
2159
|
(permission) => permission.primary && permission.action === "read"
|
|
2113
2160
|
);
|
|
2114
2161
|
const { fields, columns } = buildFields(spec, plural);
|
|
2162
|
+
const declaredEntity = scaffoldEntity(spec, plural);
|
|
2163
|
+
const transitions = declaredEntity?.states?.transitions ?? [];
|
|
2164
|
+
const lifecycle = declaredEntity?.states && transitions.length > 0 ? {
|
|
2165
|
+
field: declaredEntity.states.field,
|
|
2166
|
+
values: [...declaredEntity.states.values],
|
|
2167
|
+
transitions: transitions.map((transition) => ({
|
|
2168
|
+
from: transition.from,
|
|
2169
|
+
to: transition.to,
|
|
2170
|
+
...transition.permission === void 0 ? {} : { permission: transition.permission }
|
|
2171
|
+
}))
|
|
2172
|
+
} : void 0;
|
|
2115
2173
|
return {
|
|
2116
2174
|
spec,
|
|
2117
2175
|
names,
|
|
2118
2176
|
entity,
|
|
2119
2177
|
fields,
|
|
2178
|
+
lifecycle,
|
|
2120
2179
|
inputFields: fields.filter((field) => field.required && !field.state),
|
|
2121
2180
|
orderField: fields.find(isSortable),
|
|
2122
2181
|
columns,
|
|
@@ -2164,7 +2223,7 @@ function manifest(model) {
|
|
|
2164
2223
|
});
|
|
2165
2224
|
}
|
|
2166
2225
|
function packageJson(model) {
|
|
2167
|
-
const { spec, names, hasApi, hasClient, hasDatabase, hasCli } = model;
|
|
2226
|
+
const { spec, names, hasApi, hasClient, hasDatabase, hasCli, lifecycle } = model;
|
|
2168
2227
|
return json({
|
|
2169
2228
|
name: names.packageName,
|
|
2170
2229
|
version: spec.specVersion,
|
|
@@ -2198,6 +2257,10 @@ function packageJson(model) {
|
|
|
2198
2257
|
...hasCli ? { "@flowdular/cli-protocol": "workspace:*" } : {},
|
|
2199
2258
|
...hasClient ? { "@flowdular/client": "workspace:*" } : {},
|
|
2200
2259
|
"@flowdular/contracts": "workspace:*",
|
|
2260
|
+
/* The generated lifecycle guard imports the lifecycle engine. Without
|
|
2261
|
+
this the module typechecks and then fails the dependencies gate on
|
|
2262
|
+
the file the scaffold itself emitted. */
|
|
2263
|
+
...lifecycle ? { "@flowdular/kernel": "workspace:*" } : {},
|
|
2201
2264
|
...hasDatabase ? { "@flowdular/database": "workspace:*" } : {},
|
|
2202
2265
|
...hasApi ? {
|
|
2203
2266
|
"@flowdular/module-auth": "workspace:*",
|
|
@@ -2566,6 +2629,92 @@ export class Memory${names.pascal}Repository implements ${names.pascal}Repositor
|
|
|
2566
2629
|
}
|
|
2567
2630
|
`;
|
|
2568
2631
|
}
|
|
2632
|
+
function lifecycleFile(model) {
|
|
2633
|
+
const { names, lifecycle } = model;
|
|
2634
|
+
if (!lifecycle) return "";
|
|
2635
|
+
const field = lifecycle.field;
|
|
2636
|
+
const stateType = pascalCase(field);
|
|
2637
|
+
const constantFor = (id) => {
|
|
2638
|
+
const permission = model.permissions.find(
|
|
2639
|
+
(candidate) => candidate.id === id
|
|
2640
|
+
);
|
|
2641
|
+
return permission ? `${names.constant}_PERMISSIONS.${permission.key}` : void 0;
|
|
2642
|
+
};
|
|
2643
|
+
const transitions = lifecycle.transitions.map((transition) => {
|
|
2644
|
+
const constant = transition.permission === void 0 ? void 0 : constantFor(transition.permission);
|
|
2645
|
+
return ` {
|
|
2646
|
+
from: '${transition.from}',
|
|
2647
|
+
to: '${transition.to}',${constant === void 0 ? "" : `
|
|
2648
|
+
permission: ${constant},`}
|
|
2649
|
+
},`;
|
|
2650
|
+
}).join("\n");
|
|
2651
|
+
return `import { lifecycleFromSpec } from '@flowdular/kernel';
|
|
2652
|
+
import type { Lifecycle } from '@flowdular/kernel';
|
|
2653
|
+
import { ${names.constant}_PERMISSIONS } from '../acl/permissions.ts';
|
|
2654
|
+
|
|
2655
|
+
export type ${stateType} = ${lifecycle.values.map((value) => `'${value}'`).join(" | ")};
|
|
2656
|
+
|
|
2657
|
+
/* Declared once, from the specification the operator approved. A status column
|
|
2658
|
+
is writable by any update, so this is the only thing that makes the lifecycle
|
|
2659
|
+
real: every write to ${field} has to come through
|
|
2660
|
+
${names.camel}Lifecycle.authorize. */
|
|
2661
|
+
export const ${names.camel}Lifecycle: Lifecycle<${stateType}> =
|
|
2662
|
+
lifecycleFromSpec({
|
|
2663
|
+
id: '${model.spec.id}.${lifecycle.field}',
|
|
2664
|
+
field: '${field}',
|
|
2665
|
+
values: [${lifecycle.values.map((value) => `'${value}'`).join(", ")}],
|
|
2666
|
+
transitions: [
|
|
2667
|
+
${transitions}
|
|
2668
|
+
],
|
|
2669
|
+
}) as Lifecycle<${stateType}>;
|
|
2670
|
+
|
|
2671
|
+
/** The state a new record starts in. */
|
|
2672
|
+
export const initial${stateType}: ${stateType} = ${names.camel}Lifecycle.initial;
|
|
2673
|
+
|
|
2674
|
+
/**
|
|
2675
|
+
* Guard a status write. Returns the state to store, or a refusal that names the
|
|
2676
|
+
* move that was refused and the scope it needed, so the caller can turn it into
|
|
2677
|
+
* a problem response instead of letting an undeclared state through.
|
|
2678
|
+
*/
|
|
2679
|
+
export function authorize${stateType}(
|
|
2680
|
+
principal: { readonly permissions: ReadonlySet<string> },
|
|
2681
|
+
from: ${stateType},
|
|
2682
|
+
to: ${stateType},
|
|
2683
|
+
): { readonly allowed: true; readonly state: ${stateType} } | {
|
|
2684
|
+
readonly allowed: false;
|
|
2685
|
+
readonly code: 'STATE_UNKNOWN' | 'TRANSITION_NOT_ALLOWED' | 'PERMISSION_REQUIRED';
|
|
2686
|
+
readonly message: string;
|
|
2687
|
+
readonly permission?: string;
|
|
2688
|
+
} {
|
|
2689
|
+
const decision = ${names.camel}Lifecycle.authorize(principal, from, to);
|
|
2690
|
+
if (decision.allowed) return { allowed: true, state: to };
|
|
2691
|
+
return {
|
|
2692
|
+
allowed: false,
|
|
2693
|
+
code: decision.reason ?? 'TRANSITION_NOT_ALLOWED',
|
|
2694
|
+
message: decision.message ?? 'That move is not allowed.',
|
|
2695
|
+
...(decision.permission === undefined
|
|
2696
|
+
? {}
|
|
2697
|
+
: { permission: decision.permission }),
|
|
2698
|
+
};
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2701
|
+
/** The moves a screen may offer from a state, with the scope each one needs. */
|
|
2702
|
+
export function ${names.camel}TransitionsFrom(state: ${stateType}) {
|
|
2703
|
+
return ${names.camel}Lifecycle.movesFrom(state).map((transition) => ({
|
|
2704
|
+
to: transition.to as ${stateType},
|
|
2705
|
+
permission: transition.permission,
|
|
2706
|
+
}));
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2709
|
+
/** True when nothing further can happen without a new transition. */
|
|
2710
|
+
export function isTerminal${stateType}(state: ${stateType}): boolean {
|
|
2711
|
+
return ${names.camel}Lifecycle.isTerminal(state);
|
|
2712
|
+
}
|
|
2713
|
+
`;
|
|
2714
|
+
}
|
|
2715
|
+
function lifecycleFieldName(lifecycle) {
|
|
2716
|
+
return snakeCase(lifecycle.field);
|
|
2717
|
+
}
|
|
2569
2718
|
function servicesIndex(model) {
|
|
2570
2719
|
const { names, hasDatabase } = model;
|
|
2571
2720
|
const implementation = hasDatabase ? `export {
|
|
@@ -3595,6 +3744,12 @@ function planScaffold(spec, specSource) {
|
|
|
3595
3744
|
["src/services/index.ts", servicesIndex(model)],
|
|
3596
3745
|
["src/client/index.ts", clientIndex(model)]
|
|
3597
3746
|
]);
|
|
3747
|
+
if (model.lifecycle) {
|
|
3748
|
+
files.set(
|
|
3749
|
+
`src/domain/${lifecycleFieldName(model.lifecycle)}-lifecycle.ts`,
|
|
3750
|
+
lifecycleFile(model)
|
|
3751
|
+
);
|
|
3752
|
+
}
|
|
3598
3753
|
if (hasDatabase) {
|
|
3599
3754
|
files.set("src/services/migration.ts", migrationFile(model));
|
|
3600
3755
|
files.set(
|
|
@@ -3684,14 +3839,40 @@ var validators = {
|
|
|
3684
3839
|
cliExtension: ajv.compile(cliExtensionSchema),
|
|
3685
3840
|
platformSpec: ajv.compile(platformSpecSchema)
|
|
3686
3841
|
};
|
|
3842
|
+
function allowedKeysAt(schemaPath) {
|
|
3843
|
+
const pointer = schemaPath.replace(/^#\/?/, "").split("/").filter((segment) => segment.length > 0).map((segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
3844
|
+
let node = moduleSpecSchema;
|
|
3845
|
+
for (const segment of pointer.slice(0, -1)) {
|
|
3846
|
+
if (typeof node !== "object" || node === null) return [];
|
|
3847
|
+
node = node[segment];
|
|
3848
|
+
}
|
|
3849
|
+
const properties = node?.properties;
|
|
3850
|
+
return properties ? Object.keys(properties).sort() : [];
|
|
3851
|
+
}
|
|
3852
|
+
function looksLikeWrappedValue(name) {
|
|
3853
|
+
return /\s/.test(name) || /[,.]/.test(name) || name.length > 40;
|
|
3854
|
+
}
|
|
3687
3855
|
function issuesFrom(errors) {
|
|
3688
|
-
return (errors ?? []).map((error) =>
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3856
|
+
return (errors ?? []).map((error) => {
|
|
3857
|
+
const extra = error.params?.additionalProperty;
|
|
3858
|
+
if (error.keyword === "additionalProperties" && extra !== void 0) {
|
|
3859
|
+
const allowed = allowedKeysAt(error.schemaPath);
|
|
3860
|
+
const wrapped = looksLikeWrappedValue(extra);
|
|
3861
|
+
return {
|
|
3862
|
+
code: `SCHEMA_${error.keyword.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}`,
|
|
3863
|
+
message: wrapped ? `"${extra}" was read as a property name, which means a value wrapped onto the next line. YAML only folds a multi-line value outside a flow collection, so \`- { ... }\` turns the continuation into more keys. Write this entry in block style with one key per line, or keep the whole value on one line: ${allowed.join(", ")}.` : `Unknown property "${extra}". Allowed here: ${allowed.join(", ")}.`,
|
|
3864
|
+
path: `${error.instancePath || "/"}/${extra}`,
|
|
3865
|
+
severity: "error"
|
|
3866
|
+
};
|
|
3867
|
+
}
|
|
3868
|
+
return {
|
|
3869
|
+
/* Ajv reports a rejected branch as the keyword "false schema". */
|
|
3870
|
+
code: `SCHEMA_${error.keyword.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}`,
|
|
3871
|
+
message: error.message ?? "Schema validation failed.",
|
|
3872
|
+
path: error.instancePath || "/",
|
|
3873
|
+
severity: "error"
|
|
3874
|
+
};
|
|
3875
|
+
});
|
|
3695
3876
|
}
|
|
3696
3877
|
function specIssue(code, message, path) {
|
|
3697
3878
|
return { code, message, path, severity: "error" };
|
|
@@ -3930,6 +4111,15 @@ function moduleSpecIssues(value) {
|
|
|
3930
4111
|
)
|
|
3931
4112
|
);
|
|
3932
4113
|
}
|
|
4114
|
+
if (action.risk === "external") {
|
|
4115
|
+
issues.push(
|
|
4116
|
+
specIssue(
|
|
4117
|
+
"SPEC_ACTION_RISK_UNSUPPORTED",
|
|
4118
|
+
`Action "${action.id}" declares risk "external", which the platform refuses. Model the call as a connector definition plus an adapter, or lower the risk.`,
|
|
4119
|
+
`${path}/risk`
|
|
4120
|
+
)
|
|
4121
|
+
);
|
|
4122
|
+
}
|
|
3933
4123
|
});
|
|
3934
4124
|
(spec.widgets ?? []).forEach((widget, index) => {
|
|
3935
4125
|
const unknownEntity = entityIssue(widget.entity, `/widgets/${index}`);
|
|
@@ -4337,6 +4527,9 @@ function verifyApprovalGrant(keyring, token, expected, now = Date.now()) {
|
|
|
4337
4527
|
return { ok: true, claims };
|
|
4338
4528
|
}
|
|
4339
4529
|
|
|
4530
|
+
// ../kernel/src/lifecycle.ts
|
|
4531
|
+
var ALLOWED2 = Object.freeze({ allowed: true });
|
|
4532
|
+
|
|
4340
4533
|
// ../kernel/src/version-range.ts
|
|
4341
4534
|
var VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
4342
4535
|
var COMPARATOR = /^(\^|~|>=|<=|>|<|=)?\s*(.+)$/;
|
|
@@ -4994,7 +5187,7 @@ import { dirname as dirname6, join as join5, relative as relative5 } from "node:
|
|
|
4994
5187
|
import { createRequire } from "node:module";
|
|
4995
5188
|
import { readFile as readFile4 } from "node:fs/promises";
|
|
4996
5189
|
import { dirname as dirname4, join as join2 } from "node:path";
|
|
4997
|
-
var SDK_VERSION = "0.
|
|
5190
|
+
var SDK_VERSION = "0.5.1";
|
|
4998
5191
|
var LIBRARIES = /* @__PURE__ */ new Set([
|
|
4999
5192
|
"ai-provider",
|
|
5000
5193
|
"cli-protocol",
|
|
@@ -7206,7 +7399,8 @@ function flowdularEnvironment(environment) {
|
|
|
7206
7399
|
}
|
|
7207
7400
|
return result2;
|
|
7208
7401
|
}
|
|
7209
|
-
|
|
7402
|
+
var SPLIT_LOCAL_STATE_MESSAGE = "Both .flowdular and .coreloom state directories exist. Stop the application and choose one complete state directory before restarting.";
|
|
7403
|
+
function localStateRoots(workspaceRoot) {
|
|
7210
7404
|
const current = resolve3(workspaceRoot, ".flowdular");
|
|
7211
7405
|
const legacy = resolve3(workspaceRoot, ".coreloom");
|
|
7212
7406
|
const exists5 = (path) => {
|
|
@@ -7223,12 +7417,16 @@ function flowdularStateDirectory(workspaceRoot) {
|
|
|
7223
7417
|
};
|
|
7224
7418
|
const hasCurrent = exists5(current);
|
|
7225
7419
|
const hasLegacy = exists5(legacy);
|
|
7226
|
-
|
|
7227
|
-
|
|
7228
|
-
|
|
7229
|
-
|
|
7230
|
-
}
|
|
7231
|
-
|
|
7420
|
+
return {
|
|
7421
|
+
current,
|
|
7422
|
+
legacy: hasLegacy ? legacy : null,
|
|
7423
|
+
split: hasCurrent && hasLegacy
|
|
7424
|
+
};
|
|
7425
|
+
}
|
|
7426
|
+
function flowdularStateDirectory(workspaceRoot) {
|
|
7427
|
+
const roots2 = localStateRoots(workspaceRoot);
|
|
7428
|
+
if (roots2.split) throw new Error(SPLIT_LOCAL_STATE_MESSAGE);
|
|
7429
|
+
return roots2.legacy ?? roots2.current;
|
|
7232
7430
|
}
|
|
7233
7431
|
|
|
7234
7432
|
// ../database/src/provider.ts
|
|
@@ -7623,6 +7821,7 @@ var DEFAULT_CONNECT_TIMEOUT_MS = 5e3;
|
|
|
7623
7821
|
var DEFAULT_IDLE_TIMEOUT_MS = 3e4;
|
|
7624
7822
|
var DEFAULT_QUERY_TIMEOUT_MS = 2e4;
|
|
7625
7823
|
var DEFAULT_STATEMENT_TIMEOUT_MS = 15e3;
|
|
7824
|
+
var LEASE_DRAIN_TIMEOUT_MS = 3e4;
|
|
7626
7825
|
var DEFAULT_LOCK_TIMEOUT_MS = 5e3;
|
|
7627
7826
|
var DEFAULT_POOL_MAX = 10;
|
|
7628
7827
|
var DEFAULT_POOL_MIN = 0;
|
|
@@ -7910,6 +8109,7 @@ function createDatabaseProvider(config, factories = {}) {
|
|
|
7910
8109
|
let state = "ready";
|
|
7911
8110
|
let leases = 0;
|
|
7912
8111
|
let resolveDrained;
|
|
8112
|
+
const leaseNames = /* @__PURE__ */ new Set();
|
|
7913
8113
|
let disposePromise;
|
|
7914
8114
|
const adapters = /* @__PURE__ */ new Map();
|
|
7915
8115
|
if (config.adapter === "postgresql" && !factories.postgresPool) {
|
|
@@ -7998,6 +8198,8 @@ function createDatabaseProvider(config, factories = {}) {
|
|
|
7998
8198
|
const database = databaseFor(request);
|
|
7999
8199
|
assertDatabaseRequirements(database, request.requirements);
|
|
8000
8200
|
leases += 1;
|
|
8201
|
+
const leaseName = `${request.namespace}:${request.purpose}`;
|
|
8202
|
+
leaseNames.add(leaseName);
|
|
8001
8203
|
let released = false;
|
|
8002
8204
|
return {
|
|
8003
8205
|
database,
|
|
@@ -8005,6 +8207,7 @@ function createDatabaseProvider(config, factories = {}) {
|
|
|
8005
8207
|
if (released) return;
|
|
8006
8208
|
released = true;
|
|
8007
8209
|
leases -= 1;
|
|
8210
|
+
leaseNames.delete(leaseName);
|
|
8008
8211
|
if (leases === 0) resolveDrained?.();
|
|
8009
8212
|
}
|
|
8010
8213
|
};
|
|
@@ -8036,9 +8239,22 @@ function createDatabaseProvider(config, factories = {}) {
|
|
|
8036
8239
|
state = "disposing";
|
|
8037
8240
|
disposePromise = (async () => {
|
|
8038
8241
|
if (leases > 0) {
|
|
8039
|
-
await
|
|
8040
|
-
|
|
8041
|
-
|
|
8242
|
+
await Promise.race([
|
|
8243
|
+
new Promise((resolveDrain) => {
|
|
8244
|
+
resolveDrained = resolveDrain;
|
|
8245
|
+
}),
|
|
8246
|
+
new Promise((resolveAfter) => {
|
|
8247
|
+
const timer = setTimeout(() => {
|
|
8248
|
+
resolveAfter();
|
|
8249
|
+
console.error(
|
|
8250
|
+
`The database provider is closing with ${leases} lease(s) still held (${[
|
|
8251
|
+
...leaseNames
|
|
8252
|
+
].join(", ")}). Closing anyway.`
|
|
8253
|
+
);
|
|
8254
|
+
}, LEASE_DRAIN_TIMEOUT_MS);
|
|
8255
|
+
timer.unref?.();
|
|
8256
|
+
})
|
|
8257
|
+
]);
|
|
8042
8258
|
}
|
|
8043
8259
|
const results = await Promise.allSettled(
|
|
8044
8260
|
[...adapters.values()].map((adapter) => adapter.dispose())
|
|
@@ -9317,6 +9533,13 @@ async function runDoctor(workspace) {
|
|
|
9317
9533
|
evidence: path
|
|
9318
9534
|
});
|
|
9319
9535
|
}
|
|
9536
|
+
const roots2 = localStateRoots(workspace.root);
|
|
9537
|
+
checks.push({
|
|
9538
|
+
id: "state.root",
|
|
9539
|
+
status: roots2.split ? "fail" : "pass",
|
|
9540
|
+
message: roots2.split ? 'Both .flowdular and .coreloom state directories exist. Keep the one holding the state you want, remove the other, and run "flowdular setup migrate-state" if a .octane-erp directory is also present.' : `Local state root: ${relative6(workspace.root, roots2.legacy ?? roots2.current)}`,
|
|
9541
|
+
evidence: relative6(workspace.root, roots2.legacy ?? roots2.current)
|
|
9542
|
+
});
|
|
9320
9543
|
const packageJson2 = JSON.parse(
|
|
9321
9544
|
await readFile13(join11(workspace.root, "package.json"), "utf8")
|
|
9322
9545
|
);
|
|
@@ -9333,6 +9556,22 @@ async function runDoctor(workspace) {
|
|
|
9333
9556
|
message: blueprints.length > 0 ? `${blueprints.length} blueprint(s) discoverable.` : 'No blueprint.json was found; "blueprint validate --all" would pass vacuously.',
|
|
9334
9557
|
evidence: blueprints.map((file) => relative6(workspace.root, file)).join(", ")
|
|
9335
9558
|
});
|
|
9559
|
+
const entryPath = join11(workspace.root, "platform/src/App.tsrx");
|
|
9560
|
+
if (await exists4(entryPath)) {
|
|
9561
|
+
const entry = await readFile13(entryPath, "utf8");
|
|
9562
|
+
const pagePath = join11(workspace.root, "platform/index.html");
|
|
9563
|
+
const page = await exists4(pagePath) ? await readFile13(pagePath, "utf8") : "";
|
|
9564
|
+
const renders = entry.includes("configureBrandingFromPage");
|
|
9565
|
+
const staticHead = /<meta[^>]+name="theme-color"|<link[^>]+rel="icon"/.test(
|
|
9566
|
+
page
|
|
9567
|
+
);
|
|
9568
|
+
checks.push({
|
|
9569
|
+
id: "platform.branding",
|
|
9570
|
+
status: renders && !staticHead ? "pass" : "warn",
|
|
9571
|
+
message: !renders ? "The application entry does not render the deployment branding. Add configureBrandingFromPage(props) and the head tags the scaffold template carries, so the name, title, icon and link preview follow the settings." : staticHead ? "platform/index.html declares its own icon or theme colour while the entry renders them too, so the head carries two answers. Remove the static tags." : "The application entry renders the deployment branding.",
|
|
9572
|
+
evidence: "platform/src/App.tsrx"
|
|
9573
|
+
});
|
|
9574
|
+
}
|
|
9336
9575
|
try {
|
|
9337
9576
|
const sync = await syncPlatformModules(workspace, false);
|
|
9338
9577
|
const stale = sync.files.filter((file) => file.changed).map((file) => file.path);
|
|
@@ -10134,7 +10373,8 @@ function relativePath(workspace, path) {
|
|
|
10134
10373
|
}
|
|
10135
10374
|
async function migrationPlan(workspace) {
|
|
10136
10375
|
const sourceDirectory = join15(workspace.root, LEGACY_DATA_DIRECTORY);
|
|
10137
|
-
const
|
|
10376
|
+
const roots2 = localStateRoots(workspace.root);
|
|
10377
|
+
const stateDirectory = roots2.current;
|
|
10138
10378
|
const destinationDirectory = join15(stateDirectory, "data");
|
|
10139
10379
|
const destinationLabel = relativePath(workspace, destinationDirectory);
|
|
10140
10380
|
const blockers = [];
|
|
@@ -10166,7 +10406,8 @@ async function migrationPlan(workspace) {
|
|
|
10166
10406
|
files,
|
|
10167
10407
|
missing: [...STATE_FILES],
|
|
10168
10408
|
unsupported,
|
|
10169
|
-
blockers
|
|
10409
|
+
blockers,
|
|
10410
|
+
legacyRoot: roots2.split ? roots2.legacy : null
|
|
10170
10411
|
};
|
|
10171
10412
|
}
|
|
10172
10413
|
for (const name of (await readdir8(sourceDirectory)).sort()) {
|
|
@@ -10196,7 +10437,8 @@ async function migrationPlan(workspace) {
|
|
|
10196
10437
|
files,
|
|
10197
10438
|
missing,
|
|
10198
10439
|
unsupported,
|
|
10199
|
-
blockers
|
|
10440
|
+
blockers,
|
|
10441
|
+
legacyRoot: roots2.split ? roots2.legacy : null
|
|
10200
10442
|
};
|
|
10201
10443
|
}
|
|
10202
10444
|
async function digest(path) {
|
|
@@ -10253,6 +10495,7 @@ function data(plan, applied) {
|
|
|
10253
10495
|
applied,
|
|
10254
10496
|
ready: plan.blockers.length === 0,
|
|
10255
10497
|
source: LEGACY_DATA_DIRECTORY,
|
|
10498
|
+
splitStateRoot: plan.legacyRoot !== null,
|
|
10256
10499
|
destination: relative9(
|
|
10257
10500
|
dirname14(dirname14(plan.destinationDirectory)),
|
|
10258
10501
|
plan.destinationDirectory
|
|
@@ -10265,9 +10508,14 @@ function data(plan, applied) {
|
|
|
10265
10508
|
};
|
|
10266
10509
|
}
|
|
10267
10510
|
function unsupportedWarnings(plan) {
|
|
10268
|
-
return
|
|
10269
|
-
(
|
|
10270
|
-
|
|
10511
|
+
return [
|
|
10512
|
+
...plan.unsupported.map(
|
|
10513
|
+
(name) => `${LEGACY_DATA_DIRECTORY}/${name} is a SQLite database and cannot be migrated to the PostgreSQL or PGlite adapter; it was left in place. Start a fresh workspace on the current adapter; its data does not carry over.`
|
|
10514
|
+
),
|
|
10515
|
+
...plan.legacyRoot ? [
|
|
10516
|
+
`This workspace has both a .flowdular and a .coreloom state directory, so the application refuses to start. The copy targets .flowdular; keep the root holding the state you want and remove the other one.`
|
|
10517
|
+
] : []
|
|
10518
|
+
];
|
|
10271
10519
|
}
|
|
10272
10520
|
async function migrateLegacyState(workspace, apply) {
|
|
10273
10521
|
const plan = await migrationPlan(workspace);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flowdular",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"flowdular": "./dist/index.js",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"ajv": "8.20.0",
|
|
17
17
|
"pg": "8.23.0",
|
|
18
18
|
"yaml": "2.9.0",
|
|
19
|
-
"@flowdular/sdk": "0.
|
|
19
|
+
"@flowdular/sdk": "0.5.1"
|
|
20
20
|
},
|
|
21
21
|
"license": "MIT",
|
|
22
22
|
"repository": {
|