@objectstack/cli 9.10.0 → 10.0.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/dist/commands/compile.d.ts.map +1 -1
- package/dist/commands/compile.js +32 -0
- package/dist/commands/compile.js.map +1 -1
- package/dist/commands/plugin/sign.d.ts.map +1 -1
- package/dist/commands/plugin/sign.js +5 -2
- package/dist/commands/plugin/sign.js.map +1 -1
- package/dist/commands/serve.d.ts.map +1 -1
- package/dist/commands/serve.js +8 -2
- package/dist/commands/serve.js.map +1 -1
- package/dist/commands/verify.d.ts +21 -0
- package/dist/commands/verify.d.ts.map +1 -0
- package/dist/commands/verify.js +95 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/utils/lint-autonumber-formats.d.ts +19 -0
- package/dist/utils/lint-autonumber-formats.d.ts.map +1 -0
- package/dist/utils/lint-autonumber-formats.js +123 -0
- package/dist/utils/lint-autonumber-formats.js.map +1 -0
- package/dist/utils/lint-autonumber-formats.test.d.ts +2 -0
- package/dist/utils/lint-autonumber-formats.test.d.ts.map +1 -0
- package/dist/utils/lint-autonumber-formats.test.js +114 -0
- package/dist/utils/lint-autonumber-formats.test.js.map +1 -0
- package/dist/utils/validate-expressions.d.ts.map +1 -1
- package/dist/utils/validate-expressions.js +62 -2
- package/dist/utils/validate-expressions.js.map +1 -1
- package/dist/utils/validate-expressions.test.js +99 -0
- package/dist/utils/validate-expressions.test.js.map +1 -1
- package/package.json +55 -54
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
import { Command, Flags } from '@oclif/core';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { readEnvWithDeprecation } from '@objectstack/types';
|
|
5
|
+
import { bootStack, runCrudVerification, formatReport, runRlsProofs, formatRlsReport, } from '@objectstack/verify';
|
|
6
|
+
import { loadConfig } from '../utils/config.js';
|
|
7
|
+
/**
|
|
8
|
+
* `objectstack verify` — boot the app in-process and exercise it through the
|
|
9
|
+
* real HTTP stack, asserting runtime behavior the static gates can't see:
|
|
10
|
+
* - data fidelity: author → write → read → assert, per object/field type
|
|
11
|
+
* - authorization (--rls): "you can't write what you can't read" (#1994 class)
|
|
12
|
+
*
|
|
13
|
+
* Exits non-zero on real failures so it drops straight into CI.
|
|
14
|
+
*/
|
|
15
|
+
export default class Verify extends Command {
|
|
16
|
+
static description = 'Boot the app in-process and verify it through the real HTTP stack (CRUD round-trip fidelity + the cross-owner RLS invariant)';
|
|
17
|
+
static examples = [
|
|
18
|
+
'<%= config.bin %> verify',
|
|
19
|
+
'<%= config.bin %> verify --app ./objectstack.config.ts --rls',
|
|
20
|
+
'<%= config.bin %> verify --rls --multi-tenant --json',
|
|
21
|
+
];
|
|
22
|
+
static flags = {
|
|
23
|
+
app: Flags.string({
|
|
24
|
+
char: 'a',
|
|
25
|
+
description: 'Path to the app config (defaults to ./objectstack.config.{ts,js,mjs})',
|
|
26
|
+
}),
|
|
27
|
+
rls: Flags.boolean({
|
|
28
|
+
description: 'Also run the cross-owner RLS invariant (a fresh member must not write what it cannot read)',
|
|
29
|
+
default: false,
|
|
30
|
+
}),
|
|
31
|
+
'multi-tenant': Flags.boolean({
|
|
32
|
+
description: 'Boot org-scoped (register plugin-org-scoping) so tenant-isolation RLS policies apply (also honors $OS_MULTI_ORG_ENABLED)',
|
|
33
|
+
default: false,
|
|
34
|
+
}),
|
|
35
|
+
json: Flags.boolean({ description: 'Emit the structured report as JSON', default: false }),
|
|
36
|
+
};
|
|
37
|
+
async run() {
|
|
38
|
+
const { flags } = await this.parse(Verify);
|
|
39
|
+
const { config, absolutePath } = await loadConfig(flags.app);
|
|
40
|
+
const multiTenant = flags['multi-tenant'] ||
|
|
41
|
+
String(readEnvWithDeprecation('OS_MULTI_ORG_ENABLED', 'OS_MULTI_TENANT') ?? 'false').toLowerCase() !==
|
|
42
|
+
'false';
|
|
43
|
+
// Data fidelity runs on its own pristine stack.
|
|
44
|
+
let crud;
|
|
45
|
+
{
|
|
46
|
+
const stack = await bootStack(config, { multiTenant });
|
|
47
|
+
try {
|
|
48
|
+
const adminToken = await stack.signIn();
|
|
49
|
+
crud = await runCrudVerification(stack, adminToken, config);
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
await stack.stop();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// The RLS proofs run on a SEPARATE, fresh stack. Reusing the fidelity stack
|
|
56
|
+
// would let the RLS phase's admin-creates collide with the rows the fidelity
|
|
57
|
+
// phase already wrote on unique-constrained fields (e.g. a unique `sku` or
|
|
58
|
+
// `account_number`) — a 409 that silently skips the object instead of
|
|
59
|
+
// proving its authorization.
|
|
60
|
+
let rls;
|
|
61
|
+
if (flags.rls) {
|
|
62
|
+
const rlsStack = await bootStack(config, { multiTenant });
|
|
63
|
+
try {
|
|
64
|
+
const adminToken = await rlsStack.signIn();
|
|
65
|
+
const memberToken = await rlsStack.signUp('verify-member@objectstack.test');
|
|
66
|
+
rls = await runRlsProofs(rlsStack, adminToken, memberToken, config);
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
await rlsStack.stop();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Failure contract: a "real" runtime break the app's author must see.
|
|
73
|
+
const hardFailures = crud.summary.createFailed +
|
|
74
|
+
crud.summary.readFailed +
|
|
75
|
+
crud.summary.fidelityGaps +
|
|
76
|
+
(rls?.summary.holes ?? 0);
|
|
77
|
+
if (flags.json) {
|
|
78
|
+
this.log(JSON.stringify({ app: crud.app, config: absolutePath, multiTenant, crud, rls, hardFailures }, null, 2));
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
this.log(formatReport(crud));
|
|
82
|
+
if (rls)
|
|
83
|
+
this.log(formatRlsReport(rls));
|
|
84
|
+
this.log('');
|
|
85
|
+
this.log(hardFailures > 0
|
|
86
|
+
? chalk.red(`✗ verify FAILED — ${hardFailures} runtime failure(s)`)
|
|
87
|
+
: chalk.green('✓ verify passed — no runtime failures'));
|
|
88
|
+
}
|
|
89
|
+
// Force process exit: the in-process stack leaves handles open (http server,
|
|
90
|
+
// sqlite-wasm, better-auth timers) that keep the event loop alive after
|
|
91
|
+
// stop(), so a bare return would hang. exit() also encodes the CI contract.
|
|
92
|
+
this.exit(hardFailures > 0 ? 1 : 0);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=verify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify.js","sourceRoot":"","sources":["../../src/commands/verify.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface AutonumberLintFinding {
|
|
2
|
+
where: string;
|
|
3
|
+
message: string;
|
|
4
|
+
hint: string;
|
|
5
|
+
rule: string;
|
|
6
|
+
severity: 'error' | 'warning';
|
|
7
|
+
}
|
|
8
|
+
type AnyRec = Record<string, unknown>;
|
|
9
|
+
export declare const AUTONUMBER_UNKNOWN_FIELD = "autonumber-references-unknown-field";
|
|
10
|
+
export declare const AUTONUMBER_OPTIONAL_FIELD = "autonumber-references-optional-field";
|
|
11
|
+
export declare const AUTONUMBER_SELF_REFERENCE = "autonumber-references-self";
|
|
12
|
+
export declare const AUTONUMBER_LITERAL_TOKEN = "autonumber-unrecognized-token";
|
|
13
|
+
/**
|
|
14
|
+
* Lint every `autonumber` field's format for unresolvable / fragile `{field}`
|
|
15
|
+
* interpolation. Returns a (possibly empty) list of findings; never throws.
|
|
16
|
+
*/
|
|
17
|
+
export declare function lintAutonumberFormats(stack: AnyRec): AutonumberLintFinding[];
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=lint-autonumber-formats.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lint-autonumber-formats.d.ts","sourceRoot":"","sources":["../../src/utils/lint-autonumber-formats.ts"],"names":[],"mappings":"AAyBA,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;CAC/B;AAED,KAAK,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEtC,eAAO,MAAM,wBAAwB,wCAAwC,CAAC;AAC9E,eAAO,MAAM,yBAAyB,yCAAyC,CAAC;AAChF,eAAO,MAAM,yBAAyB,+BAA+B,CAAC;AACtE,eAAO,MAAM,wBAAwB,kCAAkC,CAAC;AAUxE;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,qBAAqB,EAAE,CA+E5E"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
/**
|
|
3
|
+
* Build-time lint for `autonumber` field formats. An `autonumberFormat` may
|
|
4
|
+
* interpolate other fields of the same record (`{plan_no}{000}`,
|
|
5
|
+
* `{section}{island_zone}{000}`). That field value forms the counter SCOPE, so
|
|
6
|
+
* if it is missing at create time the record number silently collapses into the
|
|
7
|
+
* wrong scope — and the runtime now throws rather than emit a wrong number
|
|
8
|
+
* (see sql-driver / engine `missingFieldValues`). This lint catches the two
|
|
9
|
+
* ways an author (very often an AI generating templates) gets that wrong,
|
|
10
|
+
* BEFORE it ships:
|
|
11
|
+
*
|
|
12
|
+
* - ERROR: `{field}` names a field that does not exist on the object — the
|
|
13
|
+
* generation will always throw. This is broken, so it fails the build.
|
|
14
|
+
* - WARNING: `{field}` names an OPTIONAL field — generation throws on any
|
|
15
|
+
* record left blank. The robust shape marks the referenced field
|
|
16
|
+
* `required: true` (mirroring ERPNext/Odoo, where a field that drives the
|
|
17
|
+
* naming series must be mandatory). Advisory; does not fail the build.
|
|
18
|
+
*
|
|
19
|
+
* A self-reference (`{self}` on the autonumber field itself) is always an
|
|
20
|
+
* ERROR — the value does not exist yet when the format renders.
|
|
21
|
+
*/
|
|
22
|
+
import { parseAutonumberFormat, referencedFields } from '@objectstack/spec/data';
|
|
23
|
+
export const AUTONUMBER_UNKNOWN_FIELD = 'autonumber-references-unknown-field';
|
|
24
|
+
export const AUTONUMBER_OPTIONAL_FIELD = 'autonumber-references-optional-field';
|
|
25
|
+
export const AUTONUMBER_SELF_REFERENCE = 'autonumber-references-self';
|
|
26
|
+
export const AUTONUMBER_LITERAL_TOKEN = 'autonumber-unrecognized-token';
|
|
27
|
+
function asArray(v) {
|
|
28
|
+
if (Array.isArray(v))
|
|
29
|
+
return v;
|
|
30
|
+
if (v && typeof v === 'object') {
|
|
31
|
+
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
32
|
+
}
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Lint every `autonumber` field's format for unresolvable / fragile `{field}`
|
|
37
|
+
* interpolation. Returns a (possibly empty) list of findings; never throws.
|
|
38
|
+
*/
|
|
39
|
+
export function lintAutonumberFormats(stack) {
|
|
40
|
+
const findings = [];
|
|
41
|
+
for (const obj of asArray(stack.objects)) {
|
|
42
|
+
const objectName = typeof obj.name === 'string' ? obj.name : '(unnamed object)';
|
|
43
|
+
const fields = asArray(obj.fields);
|
|
44
|
+
// name → required?, for schema-aware reference checks.
|
|
45
|
+
const fieldMeta = new Map();
|
|
46
|
+
for (const f of fields) {
|
|
47
|
+
if (typeof f.name === 'string')
|
|
48
|
+
fieldMeta.set(f.name, { required: f.required === true });
|
|
49
|
+
}
|
|
50
|
+
for (const f of fields) {
|
|
51
|
+
if (f.type !== 'autonumber')
|
|
52
|
+
continue;
|
|
53
|
+
const name = typeof f.name === 'string' ? f.name : '(unnamed field)';
|
|
54
|
+
const fmt = typeof f.autonumberFormat === 'string'
|
|
55
|
+
? f.autonumberFormat
|
|
56
|
+
: (typeof f.format === 'string' ? f.format : '');
|
|
57
|
+
if (!fmt)
|
|
58
|
+
continue;
|
|
59
|
+
const tokens = parseAutonumberFormat(fmt);
|
|
60
|
+
const refs = referencedFields(tokens);
|
|
61
|
+
const where = `object '${objectName}' · field '${name}' (autonumber "${fmt}")`;
|
|
62
|
+
// An unrecognized `{...}` group is kept as literal text by the parser, so
|
|
63
|
+
// it ships VERBATIM in the record number. This catches case/spacing/typo
|
|
64
|
+
// mistakes the field-reference checks miss — date tokens are exact
|
|
65
|
+
// (`{YYYY}`, not `{yyyy}` or `{ YYYY }`), and only one `{0..0}` slot counts.
|
|
66
|
+
for (const t of tokens) {
|
|
67
|
+
if (t.kind !== 'literal')
|
|
68
|
+
continue;
|
|
69
|
+
const braced = t.text.match(/\{[^{}]*\}/g);
|
|
70
|
+
if (!braced)
|
|
71
|
+
continue;
|
|
72
|
+
for (const tok of braced) {
|
|
73
|
+
const body = tok.slice(1, -1);
|
|
74
|
+
const isExtraSeq = /^0+$/.test(body);
|
|
75
|
+
findings.push({
|
|
76
|
+
where,
|
|
77
|
+
message: isExtraSeq
|
|
78
|
+
? `format has a second sequence slot \`${tok}\` — only the first \`{0..0}\` counts; this one renders literally as "${tok}".`
|
|
79
|
+
: `format has an unrecognized token \`${tok}\` — it is not a counter/date/{field} token, so it renders literally as "${tok}" in every record number.`,
|
|
80
|
+
hint: isExtraSeq
|
|
81
|
+
? `Use a single \`{0000}\` slot; fold any second number into a literal or a {field} token.`
|
|
82
|
+
: `Date tokens are case-sensitive and exact: {YYYY} {YY} {MM} {DD} {YYYYMMDD} (no spaces/punctuation inside). For a field value use {field_name}.`,
|
|
83
|
+
rule: AUTONUMBER_LITERAL_TOKEN,
|
|
84
|
+
severity: 'warning',
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
for (const ref of refs) {
|
|
89
|
+
if (ref === name) {
|
|
90
|
+
findings.push({
|
|
91
|
+
where,
|
|
92
|
+
message: `format interpolates \`{${ref}}\` — its own value, which does not exist yet when the number is generated.`,
|
|
93
|
+
hint: `Reference a DIFFERENT field that is set before create (e.g. \`{plan_no}{000}\`), or drop the token.`,
|
|
94
|
+
rule: AUTONUMBER_SELF_REFERENCE,
|
|
95
|
+
severity: 'error',
|
|
96
|
+
});
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const meta = fieldMeta.get(ref);
|
|
100
|
+
if (!meta) {
|
|
101
|
+
findings.push({
|
|
102
|
+
where,
|
|
103
|
+
message: `format interpolates \`{${ref}}\`, but object '${objectName}' has no field named '${ref}' — generation will always throw.`,
|
|
104
|
+
hint: `Reference an existing field, or remove the \`{${ref}}\` token from the format.`,
|
|
105
|
+
rule: AUTONUMBER_UNKNOWN_FIELD,
|
|
106
|
+
severity: 'error',
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
else if (!meta.required) {
|
|
110
|
+
findings.push({
|
|
111
|
+
where,
|
|
112
|
+
message: `format interpolates \`{${ref}}\`, but '${ref}' is optional — any record left blank fails autonumber generation at create time.`,
|
|
113
|
+
hint: `Mark '${ref}' as \`required: true\` so it is always set before the record number is rendered.`,
|
|
114
|
+
rule: AUTONUMBER_OPTIONAL_FIELD,
|
|
115
|
+
severity: 'warning',
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return findings;
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=lint-autonumber-formats.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lint-autonumber-formats.js","sourceRoot":"","sources":["../../src/utils/lint-autonumber-formats.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lint-autonumber-formats.test.d.ts","sourceRoot":"","sources":["../../src/utils/lint-autonumber-formats.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
import { lintAutonumberFormats, AUTONUMBER_UNKNOWN_FIELD, AUTONUMBER_OPTIONAL_FIELD, AUTONUMBER_SELF_REFERENCE, AUTONUMBER_LITERAL_TOKEN, } from './lint-autonumber-formats.js';
|
|
4
|
+
describe('lintAutonumberFormats', () => {
|
|
5
|
+
it('passes a date-only / fixed-prefix format with no {field} tokens', () => {
|
|
6
|
+
const stack = {
|
|
7
|
+
objects: [
|
|
8
|
+
{ name: 'audit', fields: { audit_no: { type: 'autonumber', autonumberFormat: 'AD{YYYYMMDD}{0000}' } } },
|
|
9
|
+
{ name: 'case', fields: { case_no: { type: 'autonumber', autonumberFormat: 'CASE-{0000}' } } },
|
|
10
|
+
],
|
|
11
|
+
};
|
|
12
|
+
expect(lintAutonumberFormats(stack)).toEqual([]);
|
|
13
|
+
});
|
|
14
|
+
it('passes when every {field} token is a required field on the object', () => {
|
|
15
|
+
const stack = {
|
|
16
|
+
objects: [
|
|
17
|
+
{
|
|
18
|
+
name: 'task',
|
|
19
|
+
fields: {
|
|
20
|
+
section: { type: 'text', required: true },
|
|
21
|
+
island_zone: { type: 'text', required: true },
|
|
22
|
+
task_no: { type: 'autonumber', autonumberFormat: '{section}{island_zone}{000}' },
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
expect(lintAutonumberFormats(stack)).toEqual([]);
|
|
28
|
+
});
|
|
29
|
+
it('errors when a {field} token names a non-existent field', () => {
|
|
30
|
+
const stack = {
|
|
31
|
+
objects: [
|
|
32
|
+
{ name: 'task', fields: { task_no: { type: 'autonumber', autonumberFormat: '{plan_no}{000}' } } },
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
const out = lintAutonumberFormats(stack);
|
|
36
|
+
expect(out).toHaveLength(1);
|
|
37
|
+
expect(out[0].severity).toBe('error');
|
|
38
|
+
expect(out[0].rule).toBe(AUTONUMBER_UNKNOWN_FIELD);
|
|
39
|
+
});
|
|
40
|
+
it('warns when a {field} token names an optional field', () => {
|
|
41
|
+
const stack = {
|
|
42
|
+
objects: [
|
|
43
|
+
{
|
|
44
|
+
name: 'task',
|
|
45
|
+
fields: {
|
|
46
|
+
plan_no: { type: 'text' }, // not required
|
|
47
|
+
task_no: { type: 'autonumber', autonumberFormat: '{plan_no}{000}' },
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
const out = lintAutonumberFormats(stack);
|
|
53
|
+
expect(out).toHaveLength(1);
|
|
54
|
+
expect(out[0].severity).toBe('warning');
|
|
55
|
+
expect(out[0].rule).toBe(AUTONUMBER_OPTIONAL_FIELD);
|
|
56
|
+
});
|
|
57
|
+
it('errors when the format interpolates the autonumber field itself', () => {
|
|
58
|
+
const stack = {
|
|
59
|
+
objects: [
|
|
60
|
+
{ name: 'task', fields: { task_no: { type: 'autonumber', autonumberFormat: '{task_no}{000}' } } },
|
|
61
|
+
],
|
|
62
|
+
};
|
|
63
|
+
const out = lintAutonumberFormats(stack);
|
|
64
|
+
expect(out).toHaveLength(1);
|
|
65
|
+
expect(out[0].severity).toBe('error');
|
|
66
|
+
expect(out[0].rule).toBe(AUTONUMBER_SELF_REFERENCE);
|
|
67
|
+
});
|
|
68
|
+
it('warns on an unrecognized token that would render literally', () => {
|
|
69
|
+
const stack = {
|
|
70
|
+
objects: [
|
|
71
|
+
{ name: 'wo', fields: { wo_no: { type: 'autonumber', autonumberFormat: 'WO-{ YYYY }-{0000}' } } },
|
|
72
|
+
],
|
|
73
|
+
};
|
|
74
|
+
const out = lintAutonumberFormats(stack);
|
|
75
|
+
expect(out).toHaveLength(1);
|
|
76
|
+
expect(out[0].severity).toBe('warning');
|
|
77
|
+
expect(out[0].rule).toBe(AUTONUMBER_LITERAL_TOKEN);
|
|
78
|
+
expect(out[0].message).toContain('{ YYYY }');
|
|
79
|
+
});
|
|
80
|
+
it('warns on a second sequence slot (only the first counts)', () => {
|
|
81
|
+
const stack = {
|
|
82
|
+
objects: [
|
|
83
|
+
{ name: 'wo', fields: { wo_no: { type: 'autonumber', autonumberFormat: '{0000}-{000}' } } },
|
|
84
|
+
],
|
|
85
|
+
};
|
|
86
|
+
const out = lintAutonumberFormats(stack);
|
|
87
|
+
expect(out).toHaveLength(1);
|
|
88
|
+
expect(out[0].rule).toBe(AUTONUMBER_LITERAL_TOKEN);
|
|
89
|
+
expect(out[0].message).toContain('second sequence slot');
|
|
90
|
+
});
|
|
91
|
+
it('does not warn on valid date/counter tokens', () => {
|
|
92
|
+
const stack = {
|
|
93
|
+
objects: [
|
|
94
|
+
{ name: 'audit', fields: { audit_no: { type: 'autonumber', autonumberFormat: 'AD{YYYYMMDD}{0000}' } } },
|
|
95
|
+
],
|
|
96
|
+
};
|
|
97
|
+
expect(lintAutonumberFormats(stack)).toEqual([]);
|
|
98
|
+
});
|
|
99
|
+
it('handles array-shaped fields and the `format` shorthand', () => {
|
|
100
|
+
const stack = {
|
|
101
|
+
objects: [
|
|
102
|
+
{
|
|
103
|
+
name: 'task',
|
|
104
|
+
fields: [
|
|
105
|
+
{ name: 'plan_no', type: 'text', required: true },
|
|
106
|
+
{ name: 'task_no', type: 'autonumber', format: '{plan_no}{000}' },
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
};
|
|
111
|
+
expect(lintAutonumberFormats(stack)).toEqual([]);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
//# sourceMappingURL=lint-autonumber-formats.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lint-autonumber-formats.test.js","sourceRoot":"","sources":["../../src/utils/lint-autonumber-formats.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-expressions.d.ts","sourceRoot":"","sources":["../../src/utils/validate-expressions.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"validate-expressions.d.ts","sourceRoot":"","sources":["../../src/utils/validate-expressions.ts"],"names":[],"mappings":"AAmBA,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAChC;AAED,KAAK,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AA0BtC;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,CAoKnE"}
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
* agent `validate_expression` tool exactly.
|
|
10
10
|
*
|
|
11
11
|
* Scope (v1): flow predicates (start/decision `config.condition` + edge
|
|
12
|
-
* `condition`)
|
|
13
|
-
*
|
|
12
|
+
* `condition`), object validation-rule / formula predicates, and UI action
|
|
13
|
+
* `visible` / `disabled` predicates. Each error is located (flow/object/action
|
|
14
|
+
* + node/edge/field) with a corrective message.
|
|
14
15
|
*/
|
|
15
16
|
import { validateExpression } from '@objectstack/formula';
|
|
16
17
|
/** Coerce an `objects` collection (array or name-keyed map) to an array. */
|
|
@@ -117,6 +118,8 @@ export function validateStackExpressions(stack) {
|
|
|
117
118
|
// Common predicate keys across rule shapes. Validation predicates are
|
|
118
119
|
// `record`-scoped — no field flattening — so bare refs are flagged (#1928).
|
|
119
120
|
check(where, rule.expression ?? rule.predicate ?? rule.condition ?? rule.formula, objectName, 'record');
|
|
121
|
+
// `conditional` rules carry a nested `when` predicate (record-scoped).
|
|
122
|
+
check(`${where} when`, rule.when, objectName, 'record');
|
|
120
123
|
}
|
|
121
124
|
// Field-level formulas (computed fields) reference the same object.
|
|
122
125
|
const fields = obj.fields;
|
|
@@ -124,6 +127,15 @@ export function validateStackExpressions(stack) {
|
|
|
124
127
|
? fields
|
|
125
128
|
: (fields && typeof fields === 'object' ? Object.values(fields) : []);
|
|
126
129
|
for (const f of fieldList) {
|
|
130
|
+
// Field-level conditional rules are server-enforced (rule-validator) and
|
|
131
|
+
// record-scoped — a bare ref silently fails the rule (required/readonly
|
|
132
|
+
// not enforced = data-integrity hole). #1928 class, same as actions.
|
|
133
|
+
if (f && typeof f === 'object') {
|
|
134
|
+
const fname = f.name ?? '?';
|
|
135
|
+
for (const key of ['requiredWhen', 'readonlyWhen', 'conditionalRequired', 'visibleWhen']) {
|
|
136
|
+
check(`object '${objectName}' · field '${fname}' ${key}`, f[key], objectName, 'record');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
127
139
|
if (f && typeof f === 'object' && f.formula) {
|
|
128
140
|
// formulas are `value` role (any return type), still CEL. They are
|
|
129
141
|
// `record`-scoped — `record.<field>`, never bare — so flag bare refs (#1928).
|
|
@@ -136,6 +148,54 @@ export function validateStackExpressions(stack) {
|
|
|
136
148
|
}
|
|
137
149
|
}
|
|
138
150
|
}
|
|
151
|
+
// ── Action `visible` / `disabled` predicates ───────────────────────
|
|
152
|
+
// Record-scoped, same as validation rules: a record-header / row action's
|
|
153
|
+
// `visible` is evaluated by ActionEngine against `{ record, recordId,
|
|
154
|
+
// objectName, user, … }` with fail-closed semantics, so a BARE field ref
|
|
155
|
+
// (`done` instead of `record.done`) throws and the action is silently hidden
|
|
156
|
+
// on every record (the trap behind the #2183 "Mark Done never hides" hunt).
|
|
157
|
+
// Flagging it here turns that into a build error with a corrective message.
|
|
158
|
+
// `disabled` may be a boolean (skip) or a predicate (check).
|
|
159
|
+
const seenActions = new Set();
|
|
160
|
+
const checkAction = (where, action, objectName) => {
|
|
161
|
+
const obj = objectName
|
|
162
|
+
?? (typeof action.objectName === 'string' ? action.objectName : undefined)
|
|
163
|
+
?? (typeof action.object === 'string' ? action.object : undefined);
|
|
164
|
+
const name = typeof action.name === 'string' ? action.name : '?';
|
|
165
|
+
const key = `${obj ?? ''}:${name}`;
|
|
166
|
+
if (seenActions.has(key))
|
|
167
|
+
return; // de-dup (actions are merged onto objects AND kept top-level)
|
|
168
|
+
seenActions.add(key);
|
|
169
|
+
check(`${where} · action '${name}' visible`, action.visible, obj, 'record');
|
|
170
|
+
if (typeof action.disabled !== 'boolean') {
|
|
171
|
+
check(`${where} · action '${name}' disabled`, action.disabled, obj, 'record');
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
for (const action of asArray(stack.actions)) {
|
|
175
|
+
checkAction('stack', action);
|
|
176
|
+
}
|
|
177
|
+
for (const obj of objects) {
|
|
178
|
+
const objectName = typeof obj.name === 'string' ? obj.name : undefined;
|
|
179
|
+
for (const action of asArray(obj.actions)) {
|
|
180
|
+
checkAction(`object '${objectName}'`, action, objectName);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// ── Sharing-rule predicates (security-critical, record-scoped) ─────
|
|
184
|
+
// A criteria sharing rule's `condition` decides which rows a principal sees.
|
|
185
|
+
// It is evaluated against the record, so a bare ref silently changes access.
|
|
186
|
+
for (const rule of asArray(stack.sharingRules)) {
|
|
187
|
+
const ruleObj = typeof rule.object === 'string' ? rule.object : undefined;
|
|
188
|
+
const where = `sharingRule '${rule.name ?? '?'}'${ruleObj ? ` (${ruleObj})` : ''} condition`;
|
|
189
|
+
check(where, rule.condition ?? rule.criteria ?? rule.predicate, ruleObj, 'record');
|
|
190
|
+
}
|
|
191
|
+
// ── Hook `condition` predicates (record-scoped gate) ───────────────
|
|
192
|
+
// A lifecycle hook's `condition` skips the handler when false; it is
|
|
193
|
+
// evaluated against the record, so a bare ref silently makes the hook
|
|
194
|
+
// run on every record (or never) instead of the intended subset.
|
|
195
|
+
for (const hook of asArray(stack.hooks)) {
|
|
196
|
+
const hookObj = typeof hook.object === 'string' ? hook.object : undefined; // array targets → no single field set
|
|
197
|
+
check(`hook '${hook.name ?? '?'}'${hookObj ? ` (${hookObj})` : ''} condition`, hook.condition, hookObj, 'record');
|
|
198
|
+
}
|
|
139
199
|
return issues;
|
|
140
200
|
}
|
|
141
201
|
//# sourceMappingURL=validate-expressions.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-expressions.js","sourceRoot":"","sources":["../../src/utils/validate-expressions.ts"],"names":[],"mappings":"AAAA,yEAAyE;AAEzE
|
|
1
|
+
{"version":3,"file":"validate-expressions.js","sourceRoot":"","sources":["../../src/utils/validate-expressions.ts"],"names":[],"mappings":"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"}
|
|
@@ -217,5 +217,104 @@ describe('validateStackExpressions (ADR-0032 build-time)', () => {
|
|
|
217
217
|
expect(issues[0].severity).toBe('error');
|
|
218
218
|
});
|
|
219
219
|
});
|
|
220
|
+
describe('action visible/disabled predicates (record-scoped) — #2183 class', () => {
|
|
221
|
+
it('flags a bare-field `visible` on a stack action (the trap that hid Mark Done)', () => {
|
|
222
|
+
const issues = validateStackExpressions({
|
|
223
|
+
objects: [{ name: 'showcase_task', fields: { done: { type: 'boolean' }, status: { type: 'select' } } }],
|
|
224
|
+
actions: [{ name: 'mark_done', objectName: 'showcase_task', type: 'script', locations: ['record_header'], visible: '!done' }],
|
|
225
|
+
});
|
|
226
|
+
const v = issues.filter(i => i.where.includes("action 'mark_done' visible"));
|
|
227
|
+
expect(v).toHaveLength(1);
|
|
228
|
+
expect(v[0].severity).toBe('error');
|
|
229
|
+
expect(v[0].message).toMatch(/bare reference `done`/);
|
|
230
|
+
});
|
|
231
|
+
it('accepts the record-qualified form', () => {
|
|
232
|
+
const issues = validateStackExpressions({
|
|
233
|
+
objects: [{ name: 'showcase_task', fields: { done: { type: 'boolean' } } }],
|
|
234
|
+
actions: [{ name: 'mark_done', objectName: 'showcase_task', type: 'script', visible: '!record.done' }],
|
|
235
|
+
});
|
|
236
|
+
expect(issues).toHaveLength(0);
|
|
237
|
+
});
|
|
238
|
+
it('accepts ambient globals (ctx / features / user) used by platform actions', () => {
|
|
239
|
+
const issues = validateStackExpressions({
|
|
240
|
+
objects: [{ name: 'sys_user', fields: { id: { type: 'text' }, email_verified: { type: 'boolean' } } }],
|
|
241
|
+
actions: [{ name: 'verify_email', objectName: 'sys_user', visible: 'record.id == ctx.user.id && record.email_verified == false && features.x != true' }],
|
|
242
|
+
});
|
|
243
|
+
expect(issues).toHaveLength(0);
|
|
244
|
+
});
|
|
245
|
+
it('flags a bare-field `disabled` predicate but ignores a boolean `disabled`', () => {
|
|
246
|
+
const bad = validateStackExpressions({
|
|
247
|
+
objects: [{ name: 'crm_lead', fields: { status: { type: 'select' } } }],
|
|
248
|
+
actions: [{ name: 'park', objectName: 'crm_lead', disabled: 'status == "converted"' }],
|
|
249
|
+
});
|
|
250
|
+
expect(bad.filter(i => i.where.includes("action 'park' disabled"))).toHaveLength(1);
|
|
251
|
+
const ok = validateStackExpressions({
|
|
252
|
+
objects: [{ name: 'crm_lead', fields: { status: { type: 'select' } } }],
|
|
253
|
+
actions: [{ name: 'park', objectName: 'crm_lead', disabled: true }],
|
|
254
|
+
});
|
|
255
|
+
expect(ok).toHaveLength(0);
|
|
256
|
+
});
|
|
257
|
+
it('validates an action attached to an object (record scope = parent object)', () => {
|
|
258
|
+
const issues = validateStackExpressions({
|
|
259
|
+
objects: [{
|
|
260
|
+
name: 'showcase_task',
|
|
261
|
+
fields: { done: { type: 'boolean' } },
|
|
262
|
+
actions: [{ name: 'mark_done', type: 'script', visible: '!done' }],
|
|
263
|
+
}],
|
|
264
|
+
});
|
|
265
|
+
expect(issues.filter(i => i.where.includes("action 'mark_done' visible"))).toHaveLength(1);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
describe('record-scoped coverage extensions (field rules / sharing / hooks / nested when)', () => {
|
|
269
|
+
it('flags a bare-field `readonlyWhen`/`requiredWhen` on a field', () => {
|
|
270
|
+
const issues = validateStackExpressions({
|
|
271
|
+
objects: [{
|
|
272
|
+
name: 'showcase_task',
|
|
273
|
+
fields: {
|
|
274
|
+
done: { type: 'boolean', readonlyWhen: 'done == true' },
|
|
275
|
+
title: { type: 'text', requiredWhen: 'status == "x"' },
|
|
276
|
+
},
|
|
277
|
+
}],
|
|
278
|
+
});
|
|
279
|
+
expect(issues.some(i => i.where.includes('readonlyWhen') && /bare reference `done`/.test(i.message))).toBe(true);
|
|
280
|
+
expect(issues.some(i => i.where.includes('requiredWhen') && /bare reference `status`/.test(i.message))).toBe(true);
|
|
281
|
+
});
|
|
282
|
+
it('accepts record-qualified field rules and the master-detail `parent` namespace', () => {
|
|
283
|
+
const issues = validateStackExpressions({
|
|
284
|
+
objects: [{
|
|
285
|
+
name: 'inv_line',
|
|
286
|
+
fields: {
|
|
287
|
+
qty: { type: 'number', readonlyWhen: "parent.status == 'paid'" },
|
|
288
|
+
note: { type: 'text', requiredWhen: 'record.qty >= 100' },
|
|
289
|
+
},
|
|
290
|
+
}],
|
|
291
|
+
});
|
|
292
|
+
expect(issues).toHaveLength(0);
|
|
293
|
+
});
|
|
294
|
+
it('flags a bare-field sharing-rule condition', () => {
|
|
295
|
+
const issues = validateStackExpressions({
|
|
296
|
+
objects: [{ name: 'crm_account', fields: { region: { type: 'text' } } }],
|
|
297
|
+
sharingRules: [{ name: 'sales_region', object: 'crm_account', condition: 'region == "EMEA"' }],
|
|
298
|
+
});
|
|
299
|
+
expect(issues.some(i => i.where.includes("sharingRule 'sales_region'") && /bare reference `region`/.test(i.message))).toBe(true);
|
|
300
|
+
});
|
|
301
|
+
it('flags a bare-field hook condition', () => {
|
|
302
|
+
const issues = validateStackExpressions({
|
|
303
|
+
objects: [{ name: 'crm_lead', fields: { status: { type: 'select' } } }],
|
|
304
|
+
hooks: [{ name: 'on_close', object: 'crm_lead', condition: 'status == "closed"' }],
|
|
305
|
+
});
|
|
306
|
+
expect(issues.some(i => i.where.includes("hook 'on_close'") && /bare reference `status`/.test(i.message))).toBe(true);
|
|
307
|
+
});
|
|
308
|
+
it('flags a bare-field nested `when` on a conditional validation rule', () => {
|
|
309
|
+
const issues = validateStackExpressions({
|
|
310
|
+
objects: [{
|
|
311
|
+
name: 'crm_account',
|
|
312
|
+
fields: { tier: { type: 'select' } },
|
|
313
|
+
validations: [{ name: 'cond', type: 'conditional', when: 'tier == "gold"', then: { type: 'required' } }],
|
|
314
|
+
}],
|
|
315
|
+
});
|
|
316
|
+
expect(issues.some(i => i.where.includes('when') && /bare reference `tier`/.test(i.message))).toBe(true);
|
|
317
|
+
});
|
|
318
|
+
});
|
|
220
319
|
});
|
|
221
320
|
//# sourceMappingURL=validate-expressions.test.js.map
|