@ultimat3/admin 30.1.0 → 30.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/admin",
3
- "version": "30.1.0",
3
+ "version": "30.3.0",
4
4
  "description": "Two dashboards: the /_x framework dev panels and the generated, AI-first app admin",
5
5
  "keywords": [
6
6
  "ultimate",
@@ -45,21 +45,21 @@
45
45
  "test": "bun test"
46
46
  },
47
47
  "dependencies": {
48
- "@ultimat3/action": "30.1.0",
49
- "@ultimat3/ai": "30.1.0",
50
- "@ultimat3/cache": "30.1.0",
51
- "@ultimat3/core": "30.1.0",
52
- "@ultimat3/db": "30.1.0",
53
- "@ultimat3/entity": "30.1.0",
54
- "@ultimat3/http": "30.1.0",
55
- "@ultimat3/i18n": "30.1.0",
56
- "@ultimat3/jobs": "30.1.0",
57
- "@ultimat3/mcp": "30.1.0",
58
- "@ultimat3/money": "30.1.0",
59
- "@ultimat3/policy": "30.1.0",
60
- "@ultimat3/query": "30.1.0",
61
- "@ultimat3/render": "30.1.0",
62
- "@ultimat3/schema": "30.1.0",
63
- "@ultimat3/ui": "30.1.0"
48
+ "@ultimat3/action": "30.3.0",
49
+ "@ultimat3/ai": "30.3.0",
50
+ "@ultimat3/cache": "30.3.0",
51
+ "@ultimat3/core": "30.3.0",
52
+ "@ultimat3/db": "30.3.0",
53
+ "@ultimat3/entity": "30.3.0",
54
+ "@ultimat3/http": "30.3.0",
55
+ "@ultimat3/i18n": "30.3.0",
56
+ "@ultimat3/jobs": "30.3.0",
57
+ "@ultimat3/mcp": "30.3.0",
58
+ "@ultimat3/money": "30.3.0",
59
+ "@ultimat3/policy": "30.3.0",
60
+ "@ultimat3/query": "30.3.0",
61
+ "@ultimat3/render": "30.3.0",
62
+ "@ultimat3/schema": "30.3.0",
63
+ "@ultimat3/ui": "30.3.0"
64
64
  }
65
65
  }
package/src/errors.ts CHANGED
@@ -30,6 +30,7 @@ export const ADMIN_OWNED_ERROR_CODES = [
30
30
  'X_ADMIN_ACTION_NOT_APPLICABLE',
31
31
  'X_ADMIN_MOUNT_MISSING',
32
32
  'X_ADMIN_VIEW_WRITE',
33
+ 'X_ADMIN_READ_ONLY_WRITE',
33
34
  ] as const;
34
35
 
35
36
  /**
@@ -64,6 +65,7 @@ export const ADMIN_ERROR_TITLES: Readonly<Record<AdminOwnedErrorCode, string>> =
64
65
  X_ADMIN_ACTION_NOT_APPLICABLE: 'an admin action was run on a row its when() rule excludes',
65
66
  X_ADMIN_MOUNT_MISSING: 'an admin batch job ran in a process that never declared its admin',
66
67
  X_ADMIN_VIEW_WRITE: 'an admin resource over a view entity offers a write',
68
+ X_ADMIN_READ_ONLY_WRITE: 'an admin resource over a readOnly entity offers a write',
67
69
  };
68
70
 
69
71
  // One unconditional call, so a second package claiming one of admin's codes throws
package/src/registry.ts CHANGED
@@ -86,6 +86,8 @@ export interface AdminEntity {
86
86
  * hand-built fixture has none; a view offers reads alone (`resource-operations.ts`).
87
87
  */
88
88
  readonly $viewQuery?: { readonly materialized: boolean } | null;
89
+ /** `entity({ readOnly })`'s reason, `null` on a table the app writes: reads alone, as a view. */
90
+ readonly $readOnly?: string | null;
89
91
  /** Resolves foreign-key targets. See `entity-columns.ts` for what the admin takes from it. */
90
92
  $describe(): AdminEntityDescription;
91
93
  }
@@ -1,7 +1,7 @@
1
1
  // Single responsibility: which operations an admin resource offers. Every one, by default, on a
2
- // table; only the reads on a view entity (`entity({ view })`), whose rows Postgres derives — so a
3
- // view gets no create/edit/delete route, button or MCP tool, and an override asking for one is
4
- // refused where it was declared.
2
+ // table; only the reads on a view entity (`entity({ view })`), whose rows Postgres derives, and on a
3
+ // `readOnly` one — so neither gets a create/edit/delete route, button or MCP tool, and an override
4
+ // asking for one is refused where it was declared.
5
5
 
6
6
  import { renderFixShellArg, UltimateError } from '@ultimat3/core';
7
7
  import { ADMIN_OPERATIONS, type AdminOperation } from './permissions';
@@ -23,14 +23,34 @@ export class AdminViewWriteError extends UltimateError {
23
23
  }
24
24
  }
25
25
 
26
+ /** An admin resource over a `readOnly` entity was declared with a write its repository refuses. */
27
+ export class AdminReadOnlyWriteError extends UltimateError {
28
+ constructor(input: {
29
+ readonly entity: string;
30
+ readonly reason: string;
31
+ readonly writes: readonly AdminOperation[];
32
+ }) {
33
+ super({
34
+ code: 'X_ADMIN_READ_ONLY_WRITE',
35
+ cause: `${input.entity} is readOnly (${input.reason}), and its admin resource offers ${input.writes.join(', ')} — every one would be refused with X_ENTITY_READ_ONLY`,
36
+ fix: `x entities show ${renderFixShellArg(input.entity, '<entity>')} --json # then drop operations from its admin resource (a readOnly table lists read-only), or name only ${VIEW_OPERATIONS.join(', ')}`,
37
+ });
38
+ }
39
+ }
40
+
26
41
  export function operationsFor(
27
42
  entity: AdminEntity,
28
43
  declared: readonly AdminOperation[] | undefined,
29
44
  ): readonly AdminOperation[] {
30
45
  const isView = entity.$viewQuery !== undefined && entity.$viewQuery !== null;
31
- if (!isView) return declared ?? ADMIN_OPERATIONS;
46
+ const reason = entity.$readOnly ?? null;
47
+ if (!isView && reason === null) return declared ?? ADMIN_OPERATIONS;
32
48
  if (declared === undefined) return VIEW_OPERATIONS;
33
49
  const writes = declared.filter((operation) => !READS.has(operation));
34
- if (writes.length > 0) throw new AdminViewWriteError({ entity: entity.$name, writes });
50
+ if (writes.length > 0) {
51
+ throw isView
52
+ ? new AdminViewWriteError({ entity: entity.$name, writes })
53
+ : new AdminReadOnlyWriteError({ entity: entity.$name, reason: reason ?? '', writes });
54
+ }
35
55
  return declared;
36
56
  }