@adula/kit 1.0.0 → 1.2.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.
Files changed (87) hide show
  1. package/build/agent/AGENTS.template.md +2 -2
  2. package/build/agent/capabilities.md +10 -5
  3. package/build/agent/skills/adula-frontend-design/SKILL.md +1 -1
  4. package/build/commands/doctor.js +37 -1
  5. package/build/commands/gaps.js +16 -6
  6. package/build/commands/install.js +19 -5
  7. package/build/commands/main.js +2 -0
  8. package/build/commands/module_add.js +2 -2
  9. package/build/commands/resource.js +1 -1
  10. package/build/commands/resource_snapshot.d.ts +15 -0
  11. package/build/commands/resource_snapshot.js +61 -0
  12. package/build/database/migrations/1770000000011_kit_managed_assignments.d.ts +5 -0
  13. package/build/database/migrations/1770000000011_kit_managed_assignments.js +10 -0
  14. package/build/database/migrations/1770000000012_kit_role_keys.d.ts +5 -0
  15. package/build/database/migrations/1770000000012_kit_role_keys.js +10 -0
  16. package/build/database/migrations/1770000000013_kit_notification_targets.d.ts +5 -0
  17. package/build/database/migrations/1770000000013_kit_notification_targets.js +10 -0
  18. package/build/database/migrations/1770000000014_kit_upload_grants.d.ts +5 -0
  19. package/build/database/migrations/1770000000014_kit_upload_grants.js +10 -0
  20. package/build/database/migrations/1770000000015_kit_inbound_webhooks.d.ts +5 -0
  21. package/build/database/migrations/1770000000015_kit_inbound_webhooks.js +10 -0
  22. package/build/index.d.ts +14 -5
  23. package/build/index.js +6 -2
  24. package/build/src/admin/contracts.d.ts +7 -0
  25. package/build/src/admin/contracts.js +15 -2
  26. package/build/src/admin/controller.d.ts +1 -0
  27. package/build/src/admin/controller.js +46 -1
  28. package/build/src/admin/presentation.d.ts +6 -0
  29. package/build/src/admin/record_title.d.ts +14 -0
  30. package/build/src/admin/record_title.js +50 -0
  31. package/build/src/admin/resource_service.d.ts +163 -2
  32. package/build/src/admin/resource_service.js +552 -43
  33. package/build/src/attachments/attachment_service.d.ts +12 -0
  34. package/build/src/attachments/attachment_service.js +28 -2
  35. package/build/src/attachments/upload_grants.d.ts +44 -0
  36. package/build/src/attachments/upload_grants.js +105 -0
  37. package/build/src/auth/ability.d.ts +1 -1
  38. package/build/src/auth/ability.js +4 -1
  39. package/build/src/auth/actor_store.js +6 -1
  40. package/build/src/auth/conditions.d.ts +15 -0
  41. package/build/src/auth/conditions.js +36 -0
  42. package/build/src/auth/sql.js +6 -2
  43. package/build/src/collaboration/assignments.d.ts +54 -3
  44. package/build/src/collaboration/assignments.js +125 -11
  45. package/build/src/collaboration/record_collaboration.js +5 -17
  46. package/build/src/commands/capabilities.js +22 -3
  47. package/build/src/commands/doctor.d.ts +31 -0
  48. package/build/src/commands/doctor.js +114 -0
  49. package/build/src/commands/gap_report.d.ts +50 -2
  50. package/build/src/commands/gap_report.js +102 -4
  51. package/build/src/commands/generator.js +3 -3
  52. package/build/src/commands/snapshot.d.ts +28 -0
  53. package/build/src/commands/snapshot.js +48 -0
  54. package/build/src/commands/source_markers.d.ts +10 -1
  55. package/build/src/commands/source_markers.js +36 -4
  56. package/build/src/core/administration_guard.js +3 -1
  57. package/build/src/core/message_templates.d.ts +4 -1
  58. package/build/src/core/message_templates.js +7 -2
  59. package/build/src/core/module_seed.d.ts +15 -0
  60. package/build/src/core/module_seed.js +31 -0
  61. package/build/src/core/notifications.d.ts +18 -1
  62. package/build/src/core/notifications.js +27 -1
  63. package/build/src/core/roles.d.ts +27 -1
  64. package/build/src/core/roles.js +133 -5
  65. package/build/src/database/schema.d.ts +27 -0
  66. package/build/src/database/schema.js +98 -0
  67. package/build/src/eslint/index.js +26 -0
  68. package/build/src/events/record_mutation.d.ts +4 -0
  69. package/build/src/events/record_mutation.js +2 -0
  70. package/build/src/integrations/imports.js +4 -2
  71. package/build/src/integrations/inbound_webhooks.d.ts +94 -0
  72. package/build/src/integrations/inbound_webhooks.js +276 -0
  73. package/build/src/integrations/openapi.js +69 -2
  74. package/build/src/integrations/print.js +1 -0
  75. package/build/src/resource/define_resource.js +12 -0
  76. package/build/src/resource/registry.d.ts +1 -0
  77. package/build/src/resource/registry.js +33 -0
  78. package/build/src/resource/types.d.ts +64 -1
  79. package/build/src/resource/values.js +2 -1
  80. package/build/src/services/settings.d.ts +13 -1
  81. package/build/src/services/settings.js +9 -2
  82. package/build/src/workflows/define_workflow.d.ts +30 -1
  83. package/build/src/workflows/define_workflow.js +44 -4
  84. package/build/src/workflows/engine.d.ts +24 -2
  85. package/build/src/workflows/engine.js +89 -24
  86. package/build/stubs/resource_contract.txt +103 -41
  87. package/package.json +1 -1
@@ -16,11 +16,11 @@ Structure
16
16
  8. Deletes are soft. Changeable lists come from lookups. Approvable documents use `submittable: true`.
17
17
  9. Notify via notify(), number via sequence, configure via settings. Nothing else.
18
18
 
19
- Security 10. Every route passes the authorize middleware. Every transformer uses `serialize` (explicit pick). 11. Never write crypto, sessions, or auth flows; use kit. 2FA and impersonation changes need a human review. 12. Never delete or weaken a test to make the build pass.
19
+ Security 10. Every route passes the authorize middleware. Every transformer uses `serialize` (explicit pick). `systemSave` and `rehome` skip role rules: use them only in listeners and module services after an explicit authorization, never in a controller or route (enforced by lint). 11. Never write crypto, sessions, or auth flows; use kit. 2FA and impersonation changes need a human review. 12. Never delete or weaken a test to make the build pass.
20
20
 
21
21
  UI 13. Before designing or changing any interface, read and apply `.agents/skills/adula-frontend-design/SKILL.md` (the kit's bundled frontend-design skill for business applications). At project kickoff, request the company's identity before the first design: name, logo, colors, fonts and brand guidelines when available. Reuse supplied identity, ask only for missing information, and preserve it in project-owned docs/design-identity.md. Fill the resource definition before writing a page. Override only via pages/<resource>/. 14. Compose interfaces from the project-owned shadcn/ui components in inertia/components/ui/. Use Button, Input, Select, Table, Card and the other registry primitives instead of hand-styled native controls or homemade equivalents. Semantic HTML for structure, text and form submission is allowed. Add components only via `node ace adula:ui add`, never `shadcn add`. 15. All forms (including create/edit) and row/record detail views use shadcn Dialog (modal) by default. Use ResourceSurface for resource forms/details; do not choose a standalone page, Sheet or custom overlay unless the user explicitly requests that alternative. View dialogs close on outside click; edit dialogs ask before closing by default. Honor system preferences for Gregorian, Hijri or both calendars and reduced motion. RTL by default; Arabic labels; English identifiers. Installation terminal instructions are English. Preserve explicit user exceptions and project-owned customizations.
22
22
 
23
- Boundaries 16. Never modify node_modules/@adula and never use patch-package (enforced by lint). Do not re-implement a kit service under another name (checked in review). 17. A limitation is recorded in KIT_GAPS.md (template inside) and reported to the developer — never worked around. 18. Only packages already in package.json. Ask before adding one. 19. New module or workflow → run the idea-review skill first. Field changes do not need it. 20. Before finishing: `npm run typecheck && npm test && node ace adula:doctor`.
23
+ Boundaries 16. Never modify node_modules/@adula and never use patch-package (enforced by lint). Do not re-implement a kit service under another name (checked in review). 17. A limitation is recorded in KIT_GAPS.md (template inside) and reported to the developer — never worked around. Describe the kit capability, not this project, and reproduce it on a new application from create-app. The developer submits it through the prefilled form link printed by `node ace adula:gaps report`, then records `Issue: #<number>`; never open kit issues through the API. 18. Only packages already in package.json. Ask before adding one. 19. New module or workflow → run the idea-review skill first. Field changes do not need it. 20. Before finishing: `npm run typecheck && npm test && node ace adula:doctor`.
24
24
  <!-- adula-kit:end -->
25
25
 
26
26
  ## Project rules
@@ -1,4 +1,4 @@
1
- # adula-kit capabilities (1.0.0)
1
+ # adula-kit capabilities (1.2.0)
2
2
 
3
3
  Generated by `node ace adula:capabilities`. Read this before proposing a module; anything not listed here is not provided by the kit.
4
4
 
@@ -16,16 +16,17 @@ Generated by `node ace adula:capabilities`. Read this before proposing a module;
16
16
  | json | jsonb |
17
17
  | attachment | attachments row id; per-field accept/maxSize |
18
18
  | belongsTo | foreign key, preloaded, restrict on delete |
19
+ | user | users foreign key, restrict on delete; choices are active members of the record unit or its ancestors; related as { id, fullName } |
19
20
  | hasMany | child resource; inline rows saved with the parent |
20
21
  | lookup | lookups group key |
21
22
 
22
23
  Field options: required, unique (partial, active rows), sortable, searchable (generated tsvector), filterable, permissionLevel, sequence, column.
23
24
 
24
- Resource options: scoped (required), submittable (docStatus, submit/cancel/amend-by-copy), version (optimistic locking, default with submittable), customFields, list / form / show / serialize / hidden, actions, hooks.beforeSave / hooks.afterSave.
25
+ Resource options: scoped (required), scope.from (the unit follows a required belongsTo parent), title (fields that name the record in relations, pickers, tasks and approvals), submittable (docStatus, submit/cancel/amend-by-copy), version (optimistic locking, default with submittable), customFields, list / form / show / serialize / hidden, actions, hooks.beforeSave / hooks.afterSave.
25
26
 
26
- Role rule conditions: $eq, $ne, $in, $lt, $gt, $like on scalar fields; unsupported conditions are refused. Organization scope is always added with AND.
27
+ Role rule conditions: $eq, $ne, $in, $lt, $gt, $like on scalar fields; unsupported conditions are refused. Organization scope is always added with AND. `$actor.id` names the signed-in user on user fields, createdBy and updatedBy ($eq, $ne, $in), bound per request.
27
28
 
28
- Workflow steps: condition, update, notify, approval, delay, http, end.
29
+ Workflow steps: condition, update, notify, approval, decision, delay, http, end. Workflow and step names: use lower-case letters, digits and underscores, starting with a letter (for example release_approval, notify_approved).
29
30
 
30
31
  ## Services
31
32
 
@@ -33,18 +34,22 @@ Workflow steps: condition, update, notify, approval, delay, http, end.
33
34
  - **Records and collaboration:** `RecordCollaboration`, `followerListeners`, `Assignments`, `SavedViews`, `logActivity`
34
35
  - **Documents and workflows:** `defineWorkflow`, `WorkflowEngine`, `workflowListeners`
35
36
  - **Notifications and messages:** `notify`, `notifyWithTemplate`, `MessageTemplates`, `deliverNotificationMail`, `listenForNotifications`, `NotificationsAdmin`
36
- - **Integration:** `Webhooks`, `signWebhook`, `openApiDocument`, `ImportBatches`, `renderPrintHtml`, `htmlToPdf`
37
+ - **Integration:** `Webhooks`, `signWebhook`, `InboundWebhooks`, `openApiDocument`, `ImportBatches`, `renderPrintHtml`, `htmlToPdf`
37
38
  - **Security:** `UserInvitations`, `UsersAdmin`, `RolesAdmin`
38
39
  - **Data and operations:** `sequence`, `Settings`, `SettingsAdmin`, `publishOutbox`, `consumeEvent`, `recordMutation`, `moveOrgUnit`, `migrateStorage`, `verifyBackup`, `runtimeHealth`
39
40
 
40
41
  ## Extension points
41
42
 
42
43
  - Resource hooks (beforeSave, afterSave) inside the save transaction
44
+ - ResourceService.systemSave for module-decided writes (validator, hooks and audit, no role rules); ResourceService.rehome after a parent moves
45
+ - Assignment closing notes (Assignments closeNote: optional or required); managed assignments (managed: true) closed by module code with Assignments.close
43
46
  - Page override: inertia/pages/<resource>/{index,form,show}.tsx replaces the generated page
47
+ - Create links with defaults: /resources/<resource>/create?defaults[field]=value
44
48
  - Domain events <module>.<resource>.{created,updated,deleted,submitted,cancelled,amended} with idempotent listeners
45
49
  - Module workflows (Module.workflows) with versioned definitions
46
50
  - Message templates edited per deployment
47
51
  - Outgoing webhooks and the bearer-token /api/v1 API
52
+ - Signed inbound webhooks raising inbound.<source>.<event> through the outbox
48
53
 
49
54
  ## Outside the kit
50
55
 
@@ -46,7 +46,7 @@ Install missing registry components with `node ace adula:ui add <component>`. Ne
46
46
 
47
47
  Every form and every row/record detail view opens in a shadcn Dialog unless the user explicitly requests another presentation. This includes create/edit forms and custom page overrides. Do not infer a page or Sheet exception from screen size, form length or personal design preference.
48
48
 
49
- For resource routes, use `ResourcePage`, which selects the modal surface by default. For a custom form/detail override, wrap its content in `ResourceSurface` with a title, description and the list's `backHref`. It keeps direct URLs usable and returns to the list when dismissed. Use `presentation="page"` only for a user-requested page exception. Keep embedded line-item fields within their parent's form, not separate nested forms. Search and filter controls may stay in the list toolbar; their form element is submission structure, not a separate record form.
49
+ For resource routes, use `ResourcePage`, which selects the modal surface by default. For a custom form/detail override, wrap its content in `ResourceSurface` with a title, description and the list's `backHref`. It keeps direct URLs usable and returns to the list when dismissed. From a list, open records with `openRecord(resource, id, 'show' | 'edit')` (exported by `resource-surface`): the dialog opens over the mounted list, and closing keeps its query, loaded rows and scroll. Use `presentation="page"` only for a user-requested page exception. Keep embedded line-item fields within their parent's form, not separate nested forms. Search and filter controls may stay in the list toolbar; their form element is submission structure, not a separate record form.
50
50
 
51
51
  Give every Dialog a visible DialogTitle and a useful DialogDescription. Keep long content scrollable within the viewport, Arabic RTL alignment, an accessible close control, focus containment and Escape dismissal. Use shadcn components inside the modal too. Keep validation and conflict messages inside it, preserve typed values on errors, and keep unauthorized actions absent. Do not replace server authorization with UI checks.
52
52
 
@@ -1,9 +1,11 @@
1
1
  import { BaseCommand } from '@adonisjs/core/ace';
2
2
  import { fileURLToPath } from 'node:url';
3
- import { diagnose } from '../src/commands/doctor.js';
3
+ import { diagnose, diagnoseResourceSnapshots, diagnoseOutbox, diagnoseWorkflowRoles, } from '../src/commands/doctor.js';
4
+ import { runtimeHealth } from '../src/core/health.js';
4
5
  import { diagnoseAttachments } from '../src/attachments/doctor.js';
5
6
  import { Settings } from '../src/services/settings.js';
6
7
  import { MigrationRunner } from '@adonisjs/lucid/migration';
8
+ import { readFile } from 'node:fs/promises';
7
9
  export default class Doctor extends BaseCommand {
8
10
  static commandName = 'adula:doctor';
9
11
  static description = 'Check kit ownership, installation and backup readiness';
@@ -12,6 +14,7 @@ export default class Doctor extends BaseCommand {
12
14
  const { default: db } = await import('@adonisjs/lucid/services/db');
13
15
  const findings = await diagnose(fileURLToPath(this.app.appRoot), new Settings(db.connection().getWriteClient()), this.app.inProduction, process.env);
14
16
  findings.push(await diagnoseAttachments(db.connection().getWriteClient()));
17
+ findings.push(diagnoseOutbox(await runtimeHealth(db.connection().getWriteClient())));
15
18
  const migrations = await new MigrationRunner(db, this.app, { direction: 'up' }).getList();
16
19
  const missing = migrations.filter((entry) => entry.status === 'corrupt');
17
20
  const pending = migrations.filter((entry) => entry.status === 'pending');
@@ -24,6 +27,39 @@ export default class Doctor extends BaseCommand {
24
27
  ? `${pending.length} pending migrations; run migration:run after the deployment backup`
25
28
  : 'Migration files and database history agree',
26
29
  });
30
+ const sources = [];
31
+ for (const entry of pending)
32
+ for (const extension of ['.ts', '.js'])
33
+ try {
34
+ const file = `${entry.name}${extension}`;
35
+ sources.push({ file, source: await readFile(this.app.makePath(file), 'utf8') });
36
+ break;
37
+ }
38
+ catch { }
39
+ let registry;
40
+ try {
41
+ ;
42
+ ({ registry } = await this.app.import('#start/modules'));
43
+ }
44
+ catch { }
45
+ if (registry) {
46
+ // Before the 1.1 migrations run, roles have no key column yet (an unfinished upgrade).
47
+ const keyed = await db.connection().getWriteClient().schema.hasColumn('roles', 'key');
48
+ const roles = await db.from('roles').select(keyed ? ['key', 'name'] : ['name']);
49
+ findings.push(diagnoseWorkflowRoles(registry.workflows(), roles.map((role) => ({
50
+ key: role.key ? String(role.key) : null,
51
+ name: String(role.name),
52
+ }))));
53
+ }
54
+ if (registry)
55
+ findings.push(diagnoseResourceSnapshots(sources, (name) => {
56
+ try {
57
+ return registry.get(name);
58
+ }
59
+ catch {
60
+ return undefined;
61
+ }
62
+ }));
27
63
  for (const finding of findings)
28
64
  this.logger.log(`${finding.status.toUpperCase()} ${finding.check}: ${finding.message}`);
29
65
  if (findings.some((finding) => finding.status === 'fail'))
@@ -10,7 +10,7 @@ import { gapReport } from '../src/commands/gap_report.js';
10
10
  /** Collects KIT_GAPS.md for reporting; nothing leaves the machine without explicit confirmation. */
11
11
  export default class Gaps extends BaseCommand {
12
12
  static commandName = 'adula:gaps';
13
- static description = 'Show the project gap report with private names masked before it is sent';
13
+ static description = 'Show the project gap report with private names masked, and links that open the kit gap form prefilled';
14
14
  static options = { startApp: false };
15
15
  async run() {
16
16
  if (this.action !== 'report')
@@ -23,16 +23,26 @@ export default class Gaps extends BaseCommand {
23
23
  catch {
24
24
  throw new Error('KIT_GAPS.md does not exist; adula:install creates it');
25
25
  }
26
- const { masked, titles } = gapReport(content);
27
- this.logger.info(`${titles.length} gap(s) recorded in KIT_GAPS.md`);
28
- for (const title of titles)
29
- this.logger.log(` ${title}`);
26
+ const { masked, gaps } = gapReport(content);
27
+ this.logger.info(`${gaps.length} gap(s) recorded in KIT_GAPS.md`);
28
+ for (const gap of gaps)
29
+ this.logger.log(` ${gap.title}${gap.issue ? ` (reported: ${gap.issue})` : ''}`);
30
30
  if (!this.yes) {
31
- const confirmed = await this.prompt.confirm('Show the full masked report? Nothing is sent anywhere by this command.');
31
+ const confirmed = await this.prompt.confirm('Show the full masked report and the issue links? Nothing is sent anywhere by this command.');
32
32
  if (!confirmed)
33
33
  return;
34
34
  }
35
35
  this.logger.log(masked);
36
+ const pending = gaps.filter((gap) => gap.url);
37
+ if (!pending.length)
38
+ return;
39
+ this.logger.info('Review each link in a browser, submit the form, then write the issue number in the entry as "Issue: #<number>".');
40
+ for (const gap of pending) {
41
+ this.logger.log(`\n${gap.title}`);
42
+ if (gap.missing.length)
43
+ this.logger.warning(`Complete before submitting: ${gap.missing.join(', ')}`);
44
+ this.logger.log(gap.url);
45
+ }
36
46
  }
37
47
  }
38
48
  __decorate([
@@ -1,7 +1,9 @@
1
1
  import { BaseCommand } from '@adonisjs/core/ace';
2
2
  import { readFile, writeFile, access } from 'node:fs/promises';
3
3
  import { Settings } from '../src/services/settings.js';
4
+ import { seedModules } from '../src/core/module_seed.js';
4
5
  import { syncAgentAssets } from '../src/commands/agent_assets.js';
6
+ import { GAPS_TEMPLATE } from '../src/commands/gap_report.js';
5
7
  import { KIT_VERSION } from '../src/version.js';
6
8
  import { fileURLToPath } from 'node:url';
7
9
  export default class Install extends BaseCommand {
@@ -32,10 +34,12 @@ export default class Install extends BaseCommand {
32
34
  let role = await trx('roles').where('name', 'administrator').first();
33
35
  if (!role) {
34
36
  const [created] = await trx('roles')
35
- .insert({ name: 'administrator', permission_level: 1 })
37
+ .insert({ name: 'administrator', key: 'administrator', permission_level: 1 })
36
38
  .returning('*');
37
39
  role = created;
38
40
  }
41
+ else if (!role.key && !(await trx('roles').where('key', 'administrator').first()))
42
+ await trx('roles').where('id', role.id).update({ key: 'administrator' });
39
43
  // Repair a missing bootstrap grant even when the role already exists.
40
44
  // The transaction lock makes repeated/concurrent installation idempotent.
41
45
  if (!(await trx('role_rules')
@@ -55,12 +59,22 @@ export default class Install extends BaseCommand {
55
59
  .ignore();
56
60
  await new Settings(trx).set('kit.version', KIT_VERSION);
57
61
  });
62
+ // Module lookups and default roles: added when missing, never overwritten.
63
+ let registry;
64
+ try {
65
+ ;
66
+ ({ registry } = await this.app.import('#start/modules'));
67
+ }
68
+ catch (error) {
69
+ this.logger.warning(`Module defaults skipped: ${error.message}`);
70
+ }
71
+ if (registry) {
72
+ const seeded = await seedModules(knex, registry, user.id);
73
+ this.logger.info(`Module defaults: ${seeded.lookups} lookup rows added; roles created: ${seeded.created.join(', ') || 'none'}; adopted by name: ${seeded.adopted.join(', ') || 'none'}`);
74
+ }
58
75
  for (const [name, content] of [
59
76
  ['CLAUDE.md', 'Read and follow AGENTS.md.\nThe project rules are maintained there.\n'],
60
- [
61
- 'KIT_GAPS.md',
62
- '# Kit gaps\n\nRecord Needed by, Tried, Blocked because, Proposed kit change, Workaround.\n',
63
- ],
77
+ ['KIT_GAPS.md', GAPS_TEMPLATE],
64
78
  ]) {
65
79
  const path = this.app.makePath(name);
66
80
  try {
@@ -1,4 +1,5 @@
1
1
  import Resource from './resource.js';
2
+ import ResourceSnapshot from './resource_snapshot.js';
2
3
  import Doctor from './doctor.js';
3
4
  import Install from './install.js';
4
5
  import Capabilities from './capabilities.js';
@@ -9,6 +10,7 @@ import StorageMigrate from './storage_migrate.js';
9
10
  import Gaps from './gaps.js';
10
11
  const commands = [
11
12
  Resource,
13
+ ResourceSnapshot,
12
14
  Doctor,
13
15
  Install,
14
16
  Capabilities,
@@ -7,7 +7,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  import { BaseCommand, args, flags } from '@adonisjs/core/ace';
8
8
  import { mkdir, readFile, writeFile, access } from 'node:fs/promises';
9
9
  import { identifier } from '../src/resource/define_resource.js';
10
- import { appendMarkedItem } from '../src/commands/source_markers.js';
10
+ import { appendMarkedItem, moduleSource } from '../src/commands/source_markers.js';
11
11
  export default class ModuleAdd extends BaseCommand {
12
12
  static commandName = 'adula:module:add';
13
13
  static description = 'Create and register an application-owned module';
@@ -44,7 +44,7 @@ export default class ModuleAdd extends BaseCommand {
44
44
  'tests',
45
45
  ])
46
46
  await mkdir(this.app.makePath('app/modules', this.name, directory), { recursive: true });
47
- await writeFile(this.app.makePath('app/modules', this.name, 'module.ts'), `import type { Module } from '@adula/kit'\n// adula:imports\nexport default { name: '${this.name}', label: { ar: '${this.name}', en: '${this.name}' }, reference: ${Boolean(this.reference)}, dependsOn: [], resources: [/* adula:resources */] } satisfies Module\n`, { flag: 'wx' });
47
+ await writeFile(this.app.makePath('app/modules', this.name, 'module.ts'), moduleSource(this.name, { reference: Boolean(this.reference), typed: true }), { flag: 'wx' });
48
48
  await writeFile(indexPath, next);
49
49
  this.logger.success(`Created ${this.name}. Fill bilingual labels and declare dependencies before adding resources.`);
50
50
  }
@@ -18,7 +18,7 @@ export default class Resource extends BaseCommand {
18
18
  }
19
19
  const files = await generateResource(fileURLToPath(this.app.appRoot), this.name, this.module);
20
20
  files.forEach((file) => this.logger.success(file));
21
- this.logger.info('Set bilingual labels, fill the resource definition, migrate, and run the generated security contract.');
21
+ this.logger.info('Set bilingual labels (label: the plural list heading; recordLabel: the singular record noun) and fill the resource definition. The generated migration embeds the definition as scaffolded: after filling the definition, run `node ace adula:resource:snapshot <name>` to refresh the pending migration, and update the model, factory and contract fixture, before migration:run. adula:doctor reports a pending migration that differs. Then run the generated security contract.');
22
22
  }
23
23
  }
24
24
  __decorate([
@@ -0,0 +1,15 @@
1
+ import { BaseCommand } from '@adonisjs/core/ace';
2
+ /**
3
+ * Rewrites the definition embedded in a resource's generated create-migration from the
4
+ * current resource, while that migration has not run yet (#20). A migration that already
5
+ * ran is never edited: change the table with a new expand migration instead.
6
+ */
7
+ export default class ResourceSnapshot extends BaseCommand {
8
+ static commandName: string;
9
+ static description: string;
10
+ static options: {
11
+ startApp: boolean;
12
+ };
13
+ name: string;
14
+ run(): Promise<void>;
15
+ }
@@ -0,0 +1,61 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { BaseCommand, args } from '@adonisjs/core/ace';
8
+ import { readFile, writeFile } from 'node:fs/promises';
9
+ import { MigrationRunner } from '@adonisjs/lucid/migration';
10
+ import { embeddedResource, resourceSnapshot, rewriteResourceSnapshot, } from '../src/commands/snapshot.js';
11
+ /**
12
+ * Rewrites the definition embedded in a resource's generated create-migration from the
13
+ * current resource, while that migration has not run yet (#20). A migration that already
14
+ * ran is never edited: change the table with a new expand migration instead.
15
+ */
16
+ export default class ResourceSnapshot extends BaseCommand {
17
+ static commandName = 'adula:resource:snapshot';
18
+ static description = "Refresh a pending generated create-migration from the resource's current definition";
19
+ static options = { startApp: true };
20
+ async run() {
21
+ const { default: db } = await import('@adonisjs/lucid/services/db');
22
+ const { registry } = (await this.app.import('#start/modules'));
23
+ let resource;
24
+ try {
25
+ resource = registry.get(this.name);
26
+ }
27
+ catch {
28
+ this.logger.error(`Unknown resource: ${this.name}. Register it in its module first.`);
29
+ this.exitCode = 1;
30
+ return;
31
+ }
32
+ const migrations = await new MigrationRunner(db, this.app, { direction: 'up' }).getList();
33
+ const matches = [];
34
+ for (const entry of migrations)
35
+ for (const extension of ['.ts', '.js'])
36
+ try {
37
+ const file = `${entry.name}${extension}`;
38
+ const source = await readFile(this.app.makePath(file), 'utf8');
39
+ if (embeddedResource(source) === this.name)
40
+ matches.push({ file, source, pending: entry.status === 'pending' });
41
+ break;
42
+ }
43
+ catch { }
44
+ const pending = matches.filter((match) => match.pending);
45
+ if (!pending.length) {
46
+ this.logger.error(matches.length
47
+ ? `${matches[0].file} already ran; never edit it. Add an expand migration for the new columns instead.`
48
+ : `No generated create-migration embeds ${this.name}.`);
49
+ this.exitCode = 1;
50
+ return;
51
+ }
52
+ for (const match of pending) {
53
+ await writeFile(this.app.makePath(match.file), rewriteResourceSnapshot(match.source, resourceSnapshot(resource)));
54
+ this.logger.success(match.file);
55
+ }
56
+ this.logger.info('Update the model, factory and contract fixture to the same fields, then run migration:run.');
57
+ }
58
+ }
59
+ __decorate([
60
+ args.string({ description: 'Resource name' })
61
+ ], ResourceSnapshot.prototype, "name", void 0);
@@ -0,0 +1,5 @@
1
+ import { BaseSchema } from '@adonisjs/lucid/schema';
2
+ export default class KitManagedAssignments extends BaseSchema {
3
+ up(): Promise<void>;
4
+ down(): Promise<void>;
5
+ }
@@ -0,0 +1,10 @@
1
+ import { BaseSchema } from '@adonisjs/lucid/schema';
2
+ import { createManagedAssignmentsSchema } from '../../src/database/schema.js';
3
+ export default class KitManagedAssignments extends BaseSchema {
4
+ async up() {
5
+ await createManagedAssignmentsSchema(this.db.getWriteClient());
6
+ }
7
+ async down() {
8
+ throw new Error('Kit migrations are additive. Restore a tested backup instead of rolling back.');
9
+ }
10
+ }
@@ -0,0 +1,5 @@
1
+ import { BaseSchema } from '@adonisjs/lucid/schema';
2
+ export default class KitRoleKeys extends BaseSchema {
3
+ up(): Promise<void>;
4
+ down(): Promise<void>;
5
+ }
@@ -0,0 +1,10 @@
1
+ import { BaseSchema } from '@adonisjs/lucid/schema';
2
+ import { createRoleKeysSchema } from '../../src/database/schema.js';
3
+ export default class KitRoleKeys extends BaseSchema {
4
+ async up() {
5
+ await createRoleKeysSchema(this.db.getWriteClient());
6
+ }
7
+ async down() {
8
+ throw new Error('Kit migrations are additive. Restore a tested backup instead of rolling back.');
9
+ }
10
+ }
@@ -0,0 +1,5 @@
1
+ import { BaseSchema } from '@adonisjs/lucid/schema';
2
+ export default class KitNotificationTargets extends BaseSchema {
3
+ up(): Promise<void>;
4
+ down(): Promise<void>;
5
+ }
@@ -0,0 +1,10 @@
1
+ import { BaseSchema } from '@adonisjs/lucid/schema';
2
+ import { createNotificationTargetsSchema } from '../../src/database/schema.js';
3
+ export default class KitNotificationTargets extends BaseSchema {
4
+ async up() {
5
+ await createNotificationTargetsSchema(this.db.getWriteClient());
6
+ }
7
+ async down() {
8
+ throw new Error('Kit migrations are additive. Restore a tested backup instead of rolling back.');
9
+ }
10
+ }
@@ -0,0 +1,5 @@
1
+ import { BaseSchema } from '@adonisjs/lucid/schema';
2
+ export default class KitUploadGrants extends BaseSchema {
3
+ up(): Promise<void>;
4
+ down(): Promise<void>;
5
+ }
@@ -0,0 +1,10 @@
1
+ import { BaseSchema } from '@adonisjs/lucid/schema';
2
+ import { createUploadGrantsSchema } from '../../src/database/schema.js';
3
+ export default class KitUploadGrants extends BaseSchema {
4
+ async up() {
5
+ await createUploadGrantsSchema(this.db.getWriteClient());
6
+ }
7
+ async down() {
8
+ throw new Error('Kit migrations are additive. Restore a tested backup instead of rolling back.');
9
+ }
10
+ }
@@ -0,0 +1,5 @@
1
+ import { BaseSchema } from '@adonisjs/lucid/schema';
2
+ export default class KitInboundWebhooks extends BaseSchema {
3
+ up(): Promise<void>;
4
+ down(): Promise<void>;
5
+ }
@@ -0,0 +1,10 @@
1
+ import { BaseSchema } from '@adonisjs/lucid/schema';
2
+ import { createInboundWebhooksSchema } from '../../src/database/schema.js';
3
+ export default class KitInboundWebhooks extends BaseSchema {
4
+ async up() {
5
+ await createInboundWebhooksSchema(this.db.getWriteClient());
6
+ }
7
+ async down() {
8
+ throw new Error('Kit migrations are additive. Restore a tested backup instead of rolling back.');
9
+ }
10
+ }
package/build/index.d.ts CHANGED
@@ -11,34 +11,41 @@ export { packedResourceRules } from './src/auth/packed_rules.js';
11
11
  export type { Actor, Rule, KitAbility } from './src/auth/ability.js';
12
12
  export { ActorStore } from './src/auth/actor_store.js';
13
13
  export { accessibleBy, authorizationSql, conditionSql } from './src/auth/sql.js';
14
- export { ResourceService } from './src/admin/resource_service.js';
14
+ export { ACTOR_ID, resolveActorConditions } from './src/auth/conditions.js';
15
+ export { ResourceService, canQueryField } from './src/admin/resource_service.js';
16
+ export type { AggregateOptions, AggregateRow, ListOptions } from './src/admin/resource_service.js';
15
17
  export { serialize, writableInput, selectedFields } from './src/admin/contracts.js';
16
18
  export { KitError } from './src/admin/errors.js';
17
- export { createCoreSchema, createResourceTable, createAttachmentsSchema, createSavedViewsSchema, createCollaborationSchema, createAssignmentsSchema, createMessagingSchema, createWebhooksSchema, createImportsSchema, createWorkflowSchema, } from './src/database/schema.js';
19
+ export { createCoreSchema, createResourceTable, createAttachmentsSchema, createSavedViewsSchema, createCollaborationSchema, createAssignmentsSchema, createMessagingSchema, createWebhooksSchema, createImportsSchema, createWorkflowSchema, createManagedAssignmentsSchema, createRoleKeysSchema, createNotificationTargetsSchema, createUploadGrantsSchema, createInboundWebhooksSchema, } from './src/database/schema.js';
18
20
  export { defineWorkflow, nextStep } from './src/workflows/define_workflow.js';
19
- export type { WorkflowDefinition, WorkflowInput, WorkflowStep, WorkflowEvent, StepContext, Recipients, } from './src/workflows/define_workflow.js';
21
+ export type { WorkflowDefinition, WorkflowInput, WorkflowStep, WorkflowEvent, StepContext, Recipients, DecisionOutcome, } from './src/workflows/define_workflow.js';
20
22
  export { WorkflowEngine, workflowListeners } from './src/workflows/engine.js';
21
- export type { WorkflowRun, WorkflowRunStatus, WorkflowOptions } from './src/workflows/engine.js';
23
+ export type { WorkflowRun, WorkflowRunStatus, WorkflowOptions, WorkflowDecisionForm, } from './src/workflows/engine.js';
22
24
  export { ImportBatches, importCell, IMPORT_ROW_LIMIT } from './src/integrations/imports.js';
23
25
  export type { ImportBatch, ImportStatus, ImportTarget, ActorSource, } from './src/integrations/imports.js';
24
26
  export { openApiDocument } from './src/integrations/openapi.js';
25
27
  export { renderPrintHtml, htmlToPdf } from './src/integrations/print.js';
26
28
  export type { PrintIdentity, PrintInput } from './src/integrations/print.js';
27
29
  export { Webhooks, signWebhook, WEBHOOK_MAX_ATTEMPTS } from './src/integrations/webhooks.js';
30
+ export { InboundWebhooks, inboundEventName, INBOUND_BODY_LIMIT, INBOUND_RETENTION_DAYS, } from './src/integrations/inbound_webhooks.js';
31
+ export type { InboundSource, InboundSourceInput, InboundDelivery, InboundReceipt, } from './src/integrations/inbound_webhooks.js';
28
32
  export type { SecretBox, Webhook, WebhookDelivery, WebhookOptions, HttpPoster, } from './src/integrations/webhooks.js';
29
33
  export { MessageTemplates, DEFAULT_TEMPLATES, renderTemplate, notifyWithTemplate, deliverNotificationMail, listenForNotifications, } from './src/core/message_templates.js';
30
34
  export type { TemplateDefinition, MessageTemplate, RenderedMessage, MailSender, NotificationSignal, } from './src/core/message_templates.js';
31
35
  export { Assignments } from './src/collaboration/assignments.js';
32
- export type { Assignment, AssignmentPage, AssignmentStatus, } from './src/collaboration/assignments.js';
36
+ export type { Assignment, AssignmentPage, AssignmentOptions, CloseNotePolicy, AssignmentStatus, } from './src/collaboration/assignments.js';
33
37
  export { RecordCollaboration, followerListeners } from './src/collaboration/record_collaboration.js';
34
38
  export type { ActorLoader, CommentEntry, FieldChangeEntry, RecordCollaborationState, MentionCandidate, } from './src/collaboration/record_collaboration.js';
35
39
  export type { FieldChange } from './src/events/record_mutation.js';
36
40
  export { registerUpload, findAttachment, loadAttachments, claimAttachment, releaseAttachment, summarizeAttachment, attachmentUrl, isAttachmentId, isRelativeDiskPath, attachmentPolicy, pendingUploadCount, staleUploads, forgetUpload, DEFAULT_ATTACHMENT_EXTENSIONS, PENDING_UPLOAD_LIMIT, UNBOUND_UPLOAD_TTL_MS, } from './src/attachments/attachment_service.js';
37
41
  export type { AttachmentRecord, AttachmentSummary, UploadInput, ClaimInput, } from './src/attachments/attachment_service.js';
42
+ export { grantUpload, redeemUploadGrant, pruneUploadGrants, UPLOAD_GRANT_MAX_TTL_MS, } from './src/attachments/upload_grants.js';
43
+ export type { UploadGrantInput, UploadGrant, RedeemedUploadGrant, } from './src/attachments/upload_grants.js';
38
44
  export { migrateStorage } from './src/attachments/storage_migrate.js';
39
45
  export type { StorageDisk, StorageMigrationOptions, StorageMigrationResult, } from './src/attachments/storage_migrate.js';
40
46
  export { diagnoseAttachments } from './src/attachments/doctor.js';
41
47
  export { Settings, sequence, notify } from './src/services/settings.js';
48
+ export type { NotificationTarget } from './src/services/settings.js';
42
49
  export { uiPreferences, validateUiPreferences, UI_PREFERENCES_KEY, DEFAULT_UI_PREFERENCES, } from './src/core/ui_preferences.js';
43
50
  export type { UiPreferences, CalendarSystem, CalendarPreference, } from './src/core/ui_preferences.js';
44
51
  export { moveOrgUnit } from './src/org/org_service.js';
@@ -51,6 +58,8 @@ export type { BackupObject, BackupStatus } from './src/services/backup.js';
51
58
  export { ActivityAdmin, logActivity } from './src/core/activity.js';
52
59
  export type { ActivityEntry, ActivityFilters, ActivityPage, ActivityRow, } from './src/core/activity.js';
53
60
  export { RolesAdmin, ALL_SUBJECT_LABEL } from './src/core/roles.js';
61
+ export { seedModules } from './src/core/module_seed.js';
62
+ export type { ModuleSeedResult } from './src/core/module_seed.js';
54
63
  export type { RoleSummary, RoleDetail, RoleRule, RuleInput, RoleMatrix, MatrixSubject, MatrixField, } from './src/core/roles.js';
55
64
  export { UsersAdmin } from './src/core/users.js';
56
65
  export { UserInvitations, type InvitationDelivery } from './src/core/user_invitations.js';
package/build/index.js CHANGED
@@ -7,20 +7,23 @@ export { buildAbility, canRecord, inOrgScope } from './src/auth/ability.js';
7
7
  export { packedResourceRules } from './src/auth/packed_rules.js';
8
8
  export { ActorStore } from './src/auth/actor_store.js';
9
9
  export { accessibleBy, authorizationSql, conditionSql } from './src/auth/sql.js';
10
- export { ResourceService } from './src/admin/resource_service.js';
10
+ export { ACTOR_ID, resolveActorConditions } from './src/auth/conditions.js';
11
+ export { ResourceService, canQueryField } from './src/admin/resource_service.js';
11
12
  export { serialize, writableInput, selectedFields } from './src/admin/contracts.js';
12
13
  export { KitError } from './src/admin/errors.js';
13
- export { createCoreSchema, createResourceTable, createAttachmentsSchema, createSavedViewsSchema, createCollaborationSchema, createAssignmentsSchema, createMessagingSchema, createWebhooksSchema, createImportsSchema, createWorkflowSchema, } from './src/database/schema.js';
14
+ export { createCoreSchema, createResourceTable, createAttachmentsSchema, createSavedViewsSchema, createCollaborationSchema, createAssignmentsSchema, createMessagingSchema, createWebhooksSchema, createImportsSchema, createWorkflowSchema, createManagedAssignmentsSchema, createRoleKeysSchema, createNotificationTargetsSchema, createUploadGrantsSchema, createInboundWebhooksSchema, } from './src/database/schema.js';
14
15
  export { defineWorkflow, nextStep } from './src/workflows/define_workflow.js';
15
16
  export { WorkflowEngine, workflowListeners } from './src/workflows/engine.js';
16
17
  export { ImportBatches, importCell, IMPORT_ROW_LIMIT } from './src/integrations/imports.js';
17
18
  export { openApiDocument } from './src/integrations/openapi.js';
18
19
  export { renderPrintHtml, htmlToPdf } from './src/integrations/print.js';
19
20
  export { Webhooks, signWebhook, WEBHOOK_MAX_ATTEMPTS } from './src/integrations/webhooks.js';
21
+ export { InboundWebhooks, inboundEventName, INBOUND_BODY_LIMIT, INBOUND_RETENTION_DAYS, } from './src/integrations/inbound_webhooks.js';
20
22
  export { MessageTemplates, DEFAULT_TEMPLATES, renderTemplate, notifyWithTemplate, deliverNotificationMail, listenForNotifications, } from './src/core/message_templates.js';
21
23
  export { Assignments } from './src/collaboration/assignments.js';
22
24
  export { RecordCollaboration, followerListeners } from './src/collaboration/record_collaboration.js';
23
25
  export { registerUpload, findAttachment, loadAttachments, claimAttachment, releaseAttachment, summarizeAttachment, attachmentUrl, isAttachmentId, isRelativeDiskPath, attachmentPolicy, pendingUploadCount, staleUploads, forgetUpload, DEFAULT_ATTACHMENT_EXTENSIONS, PENDING_UPLOAD_LIMIT, UNBOUND_UPLOAD_TTL_MS, } from './src/attachments/attachment_service.js';
26
+ export { grantUpload, redeemUploadGrant, pruneUploadGrants, UPLOAD_GRANT_MAX_TTL_MS, } from './src/attachments/upload_grants.js';
24
27
  export { migrateStorage } from './src/attachments/storage_migrate.js';
25
28
  export { diagnoseAttachments } from './src/attachments/doctor.js';
26
29
  export { Settings, sequence, notify } from './src/services/settings.js';
@@ -32,6 +35,7 @@ export { recordMutation } from './src/events/record_mutation.js';
32
35
  export { assessBackup, verifyBackup } from './src/services/backup.js';
33
36
  export { ActivityAdmin, logActivity } from './src/core/activity.js';
34
37
  export { RolesAdmin, ALL_SUBJECT_LABEL } from './src/core/roles.js';
38
+ export { seedModules } from './src/core/module_seed.js';
35
39
  export { UsersAdmin } from './src/core/users.js';
36
40
  export { UserInvitations } from './src/core/user_invitations.js';
37
41
  export { OrgUnitsAdmin } from './src/core/org_units.js';
@@ -10,3 +10,10 @@ export declare function serialize(resource: Resource, record: RecordData, abilit
10
10
  export declare function writableInput(resource: Resource, input: RecordData): {
11
11
  [k: string]: unknown;
12
12
  };
13
+ /**
14
+ * Values module code may write through ResourceService.systemSave: every stored field
15
+ * except sequences and inline children, plus the organization unit when it is not inherited.
16
+ */
17
+ export declare function systemInput(resource: Resource, input: RecordData): {
18
+ [k: string]: unknown;
19
+ };
@@ -13,7 +13,7 @@ export function selectedFields(resource, ability, options = {}) {
13
13
  ...(options.write
14
14
  ? Object.keys(resource.fields)
15
15
  : (resource.serialize ?? [...resource.list, ...resource.show])),
16
- ...Object.keys(resource.fields).filter((k) => resource.fields[k].type === 'belongsTo'),
16
+ ...Object.keys(resource.fields).filter((k) => ['belongsTo', 'user'].includes(resource.fields[k].type)),
17
17
  ...(options.extra ?? []),
18
18
  ...conditionFields(ability, resource),
19
19
  ]),
@@ -92,7 +92,7 @@ export function serialize(resource, record, ability, actor) {
92
92
  export function writableInput(resource, input) {
93
93
  const allowed = new Set([
94
94
  ...resource.form,
95
- ...(resource.scoped ? ['orgUnitId'] : []),
95
+ ...(resource.scoped && !resource.scope ? ['orgUnitId'] : []),
96
96
  ...(resource.version ? ['version'] : []),
97
97
  ]);
98
98
  for (const key of Object.keys(input))
@@ -100,3 +100,16 @@ export function writableInput(resource, input) {
100
100
  throw new KitError(422, 'E_FIELD_NOT_WRITABLE', `Field is not writable: ${key}`);
101
101
  return Object.fromEntries(Object.entries(input).filter(([key]) => resource.form.includes(key)));
102
102
  }
103
+ /**
104
+ * Values module code may write through ResourceService.systemSave: every stored field
105
+ * except sequences and inline children, plus the organization unit when it is not inherited.
106
+ */
107
+ export function systemInput(resource, input) {
108
+ for (const key of Object.keys(input)) {
109
+ const field = resource.fields[key];
110
+ const unit = key === 'orgUnitId' && resource.scoped && !resource.scope;
111
+ if (!unit && (!field || field.type === 'hasMany' || field.sequence))
112
+ throw new KitError(422, 'E_FIELD_NOT_WRITABLE', `Field is not writable: ${key}`);
113
+ }
114
+ return Object.fromEntries(Object.entries(input).filter(([key]) => key in resource.fields));
115
+ }
@@ -26,6 +26,7 @@ export interface ResourceController {
26
26
  create(ctx: HttpContext): Promise<unknown>;
27
27
  edit(ctx: HttpContext): Promise<unknown>;
28
28
  options(ctx: HttpContext): Promise<unknown>;
29
+ aggregate(ctx: HttpContext): Promise<unknown>;
29
30
  }
30
31
  /** The host supplies its authenticated actor; ResourceService authorizes every action. */
31
32
  export declare function createResourceController(resolveRuntime: (ctx: HttpContext) => Promise<ResourceRuntime>, renderList?: (ctx: HttpContext, resource: Resource, result: ResourceList) => unknown, renderForm?: (ctx: HttpContext, resource: Resource, editor: ResourceEditor) => unknown, renderShow?: (ctx: HttpContext, resource: Resource, result: ResourceShow) => unknown): new () => ResourceController;