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.2",
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
- const isNullary = fieldSchema?.nullaryOperators?.includes(c.operator);
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 = OPERATOR_LABELS[c.operator] ?? c.operator;
146
+ const label = operatorLabel(c.operator);
174
147
  const val = Array.isArray(c.value) ? c.value.join(', ') : c.value;
175
- const isNullary = c.operator === 'is_null' || c.operator === 'is_not_null';
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 = fieldSchema?.nullaryOperators?.includes(c.operator);
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
- {OPERATOR_LABELS[op] ?? op}
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.11.0",
25
- "@astram/engine": "0.12.0",
26
- "@astram/guard": "0.20.1",
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.17.1",
29
- "@astram/messaging": "0.1.4",
30
- "@astram/migrate": "0.7.1",
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",