create-astram-app 0.12.2 → 0.12.4
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-astram-app",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.4",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Scaffolder for a new Astram app — emits a running, framework-enforced NestJS server (deny-by-default, built-in auth/identity to switch on) and, with --with-frontend, a React admin/auth UI, over the immutable @astram/* framework packages.",
|
|
6
6
|
"type": "module",
|
|
@@ -32,26 +32,11 @@ import {
|
|
|
32
32
|
SelectTrigger,
|
|
33
33
|
SelectValue,
|
|
34
34
|
} from '@/components/ui/select';
|
|
35
|
+
import { operatorLabel, isNullaryOperator, buildCondition } from './recordPolicyBuilder.js';
|
|
35
36
|
|
|
36
37
|
/** The object types this app lets an admin author policies for. Extend as the app grows. */
|
|
37
38
|
const AUTHORABLE_TYPES = ['note'];
|
|
38
39
|
|
|
39
|
-
/** Human labels for the closed operator set. */
|
|
40
|
-
const OPERATOR_LABELS = {
|
|
41
|
-
eq: 'equals',
|
|
42
|
-
ne: 'does not equal',
|
|
43
|
-
in: 'is any of',
|
|
44
|
-
not_in: 'is none of',
|
|
45
|
-
gt: 'is greater than',
|
|
46
|
-
lt: 'is less than',
|
|
47
|
-
gte: 'is at least',
|
|
48
|
-
lte: 'is at most',
|
|
49
|
-
contains: 'contains',
|
|
50
|
-
not_contains: 'does not contain',
|
|
51
|
-
is_null: 'is empty',
|
|
52
|
-
is_not_null: 'is not empty',
|
|
53
|
-
};
|
|
54
|
-
|
|
55
40
|
/** A fresh, empty condition row. */
|
|
56
41
|
const emptyCondition = () => ({ field: '', operator: '', value: '' });
|
|
57
42
|
|
|
@@ -126,19 +111,7 @@ export default function RecordPoliciesPage() {
|
|
|
126
111
|
for (const c of conditions) {
|
|
127
112
|
if (!c.field || !c.operator) continue;
|
|
128
113
|
const fieldSchema = fieldsByKey.get(c.field);
|
|
129
|
-
|
|
130
|
-
const isArray = fieldSchema?.arrayOperators?.includes(c.operator);
|
|
131
|
-
if (isNullary) {
|
|
132
|
-
built.push({ field: c.field, operator: c.operator });
|
|
133
|
-
} else if (isArray) {
|
|
134
|
-
const values = String(c.value ?? '')
|
|
135
|
-
.split(',')
|
|
136
|
-
.map((v) => v.trim())
|
|
137
|
-
.filter(Boolean);
|
|
138
|
-
built.push({ field: c.field, operator: c.operator, value: values });
|
|
139
|
-
} else {
|
|
140
|
-
built.push({ field: c.field, operator: c.operator, value: c.value });
|
|
141
|
-
}
|
|
114
|
+
built.push(buildCondition(c, fieldSchema));
|
|
142
115
|
}
|
|
143
116
|
|
|
144
117
|
setSaving(true);
|
|
@@ -170,9 +143,10 @@ export default function RecordPoliciesPage() {
|
|
|
170
143
|
// Render one policy as a human sentence: "allow read where branch equals north AND …".
|
|
171
144
|
const describe = (policy) => {
|
|
172
145
|
const parts = (policy.conditions ?? []).map((c) => {
|
|
173
|
-
const label =
|
|
146
|
+
const label = operatorLabel(c.operator);
|
|
174
147
|
const val = Array.isArray(c.value) ? c.value.join(', ') : c.value;
|
|
175
|
-
const isNullary =
|
|
148
|
+
const isNullary =
|
|
149
|
+
c.value === undefined || isNullaryOperator(c.operator, fieldsByKey.get(c.field));
|
|
176
150
|
return `${c.field} ${label}${isNullary ? '' : ` ${val}`}`;
|
|
177
151
|
});
|
|
178
152
|
const where = parts.length ? ` where ${parts.join(' AND ')}` : ' (all records)';
|
|
@@ -284,7 +258,7 @@ export default function RecordPoliciesPage() {
|
|
|
284
258
|
{conditions.map((c, i) => {
|
|
285
259
|
const fieldSchema = fieldsByKey.get(c.field);
|
|
286
260
|
const validOps = fieldSchema?.validOperators ?? [];
|
|
287
|
-
const isNullary =
|
|
261
|
+
const isNullary = isNullaryOperator(c.operator, fieldSchema);
|
|
288
262
|
const isArray = fieldSchema?.arrayOperators?.includes(c.operator);
|
|
289
263
|
const isContains = c.operator === 'contains' || c.operator === 'not_contains';
|
|
290
264
|
return (
|
|
@@ -318,7 +292,7 @@ export default function RecordPoliciesPage() {
|
|
|
318
292
|
<SelectContent>
|
|
319
293
|
{validOps.map((op) => (
|
|
320
294
|
<SelectItem key={op} value={op}>
|
|
321
|
-
{
|
|
295
|
+
{operatorLabel(op)}
|
|
322
296
|
</SelectItem>
|
|
323
297
|
))}
|
|
324
298
|
</SelectContent>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// recordPolicyBuilder.js — pure logic for the guided record-policy builder
|
|
2
|
+
// (RecordPoliciesPage.jsx). Plain ESM, no JSX, no `@/` aliases, so `node --test` can load it.
|
|
3
|
+
|
|
4
|
+
/** Human labels for the closed operator set. */
|
|
5
|
+
export const OPERATOR_LABELS = {
|
|
6
|
+
eq: 'equals',
|
|
7
|
+
ne: 'does not equal',
|
|
8
|
+
in: 'is any of',
|
|
9
|
+
not_in: 'is none of',
|
|
10
|
+
gt: 'is greater than',
|
|
11
|
+
lt: 'is less than',
|
|
12
|
+
gte: 'is at least',
|
|
13
|
+
lte: 'is at most',
|
|
14
|
+
contains: 'contains',
|
|
15
|
+
not_contains: 'does not contain',
|
|
16
|
+
is_null: 'is empty',
|
|
17
|
+
is_not_null: 'is not empty',
|
|
18
|
+
is_owner: 'is me',
|
|
19
|
+
is_not_owner: 'is not me',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** The label for an operator; falls back to the operator key. */
|
|
23
|
+
export function operatorLabel(op) {
|
|
24
|
+
return OPERATOR_LABELS[op] ?? op;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** True when the field's schema lists the operator as taking no value. */
|
|
28
|
+
export function isNullaryOperator(op, fieldSchema) {
|
|
29
|
+
return Boolean(fieldSchema?.nullaryOperators?.includes(op));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Build the structured condition submitted to the server from one builder row. */
|
|
33
|
+
export function buildCondition(row, fieldSchema) {
|
|
34
|
+
if (isNullaryOperator(row.operator, fieldSchema)) {
|
|
35
|
+
return { field: row.field, operator: row.operator };
|
|
36
|
+
}
|
|
37
|
+
if (fieldSchema?.arrayOperators?.includes(row.operator)) {
|
|
38
|
+
const values = String(row.value ?? '')
|
|
39
|
+
.split(',')
|
|
40
|
+
.map((v) => v.trim())
|
|
41
|
+
.filter(Boolean);
|
|
42
|
+
return { field: row.field, operator: row.operator, value: values };
|
|
43
|
+
}
|
|
44
|
+
return { field: row.field, operator: row.operator, value: row.value };
|
|
45
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// recordPolicyBuilder.test.mjs — separation-of-duties R10 / AC-15: the guided policy builder's
|
|
2
|
+
// pure logic for the owner operators. The page (RecordPoliciesPage.jsx) cannot be rendered
|
|
3
|
+
// under Node's runner, so its operator logic lives in ./recordPolicyBuilder.js:
|
|
4
|
+
// operatorLabel(op), isNullaryOperator(op, fieldSchema), buildCondition(row, fieldSchema)
|
|
5
|
+
//
|
|
6
|
+
// node --test src/pages/access/recordPolicyBuilder.test.mjs
|
|
7
|
+
import { test, describe } from 'node:test';
|
|
8
|
+
import assert from 'node:assert/strict';
|
|
9
|
+
import { operatorLabel, isNullaryOperator, buildCondition } from './recordPolicyBuilder.js';
|
|
10
|
+
|
|
11
|
+
const ownerField = {
|
|
12
|
+
key: 'created_by',
|
|
13
|
+
validOperators: ['eq', 'is_null', 'is_owner', 'is_not_owner'],
|
|
14
|
+
nullaryOperators: ['is_null', 'is_owner', 'is_not_owner'],
|
|
15
|
+
arrayOperators: [],
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
describe('owner operators in the guided policy builder (AC-15)', () => {
|
|
19
|
+
test('is_owner is labelled "is me"', () => {
|
|
20
|
+
assert.equal(operatorLabel('is_owner'), 'is me');
|
|
21
|
+
});
|
|
22
|
+
test('is_not_owner is labelled "is not me"', () => {
|
|
23
|
+
assert.equal(operatorLabel('is_not_owner'), 'is not me');
|
|
24
|
+
});
|
|
25
|
+
test('existing labels are unchanged', () => {
|
|
26
|
+
assert.equal(operatorLabel('is_null'), 'is empty');
|
|
27
|
+
assert.equal(operatorLabel('eq'), 'equals');
|
|
28
|
+
});
|
|
29
|
+
test('both owner operators are nullary when the schema lists them', () => {
|
|
30
|
+
assert.equal(isNullaryOperator('is_owner', ownerField), true);
|
|
31
|
+
assert.equal(isNullaryOperator('is_not_owner', ownerField), true);
|
|
32
|
+
assert.equal(isNullaryOperator('eq', ownerField), false);
|
|
33
|
+
});
|
|
34
|
+
test('submitted condition for is_owner has no value, even if a stale value was typed', () => {
|
|
35
|
+
const c = buildCondition(
|
|
36
|
+
{ field: 'created_by', operator: 'is_owner', value: 'stale' },
|
|
37
|
+
ownerField,
|
|
38
|
+
);
|
|
39
|
+
assert.deepEqual(c, { field: 'created_by', operator: 'is_owner' });
|
|
40
|
+
assert.equal('value' in c, false);
|
|
41
|
+
});
|
|
42
|
+
test('submitted condition for is_not_owner has no value', () => {
|
|
43
|
+
const c = buildCondition(
|
|
44
|
+
{ field: 'created_by', operator: 'is_not_owner', value: '' },
|
|
45
|
+
ownerField,
|
|
46
|
+
);
|
|
47
|
+
assert.deepEqual(c, { field: 'created_by', operator: 'is_not_owner' });
|
|
48
|
+
});
|
|
49
|
+
test('non-nullary operators still carry their value', () => {
|
|
50
|
+
assert.deepEqual(
|
|
51
|
+
buildCondition({ field: 'created_by', operator: 'eq', value: 'x' }, ownerField),
|
|
52
|
+
{ field: 'created_by', operator: 'eq', value: 'x' },
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -21,13 +21,13 @@
|
|
|
21
21
|
"postinstall": "node scripts/sync-astram-guide.mjs"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@astram/core": "1.
|
|
25
|
-
"@astram/engine": "0.
|
|
26
|
-
"@astram/guard": "0.
|
|
24
|
+
"@astram/core": "1.12.0",
|
|
25
|
+
"@astram/engine": "0.14.0",
|
|
26
|
+
"@astram/guard": "0.21.0",
|
|
27
27
|
"@astram/guide": "^0.2.8",
|
|
28
|
-
"@astram/identity": "0.
|
|
29
|
-
"@astram/messaging": "0.1.
|
|
30
|
-
"@astram/migrate": "0.7.
|
|
28
|
+
"@astram/identity": "0.18.0",
|
|
29
|
+
"@astram/messaging": "0.1.6",
|
|
30
|
+
"@astram/migrate": "0.7.2",
|
|
31
31
|
"@astram/sdk": "^0.2.0",
|
|
32
32
|
"@nestjs/common": "^11.2.7",
|
|
33
33
|
"@nestjs/core": "^11.2.7",
|