@adula/kit 0.2.0-alpha.4 → 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 (120) hide show
  1. package/README.md +12 -2
  2. package/build/agent/AGENTS.template.md +2 -2
  3. package/build/agent/capabilities.md +54 -7
  4. package/build/agent/skills/adula-frontend-design/SKILL.md +1 -1
  5. package/build/agent/skills/idea-review/SKILL.md +26 -1
  6. package/build/agent/skills/module-review/SKILL.md +22 -0
  7. package/build/agent/skills/perf-review/SKILL.md +24 -0
  8. package/build/agent/skills/schema-review/SKILL.md +22 -0
  9. package/build/agent/skills/security-review/SKILL.md +24 -0
  10. package/build/agent/skills/ui-review/SKILL.md +22 -0
  11. package/build/commands/capabilities.d.ts +4 -0
  12. package/build/commands/capabilities.js +35 -4
  13. package/build/commands/doctor.js +37 -1
  14. package/build/commands/gaps.js +16 -6
  15. package/build/commands/install.js +19 -5
  16. package/build/commands/main.d.ts +4 -2
  17. package/build/commands/main.js +2 -0
  18. package/build/commands/module_add.js +2 -2
  19. package/build/commands/resource.js +1 -1
  20. package/build/commands/resource_snapshot.d.ts +15 -0
  21. package/build/commands/resource_snapshot.js +61 -0
  22. package/build/database/migrations/1770000000004_kit_collaboration.d.ts +5 -0
  23. package/build/database/migrations/1770000000004_kit_collaboration.js +10 -0
  24. package/build/database/migrations/1770000000005_kit_assignments.d.ts +5 -0
  25. package/build/database/migrations/1770000000005_kit_assignments.js +10 -0
  26. package/build/database/migrations/1770000000006_kit_messaging.d.ts +5 -0
  27. package/build/database/migrations/1770000000006_kit_messaging.js +10 -0
  28. package/build/database/migrations/1770000000007_kit_webhooks.d.ts +5 -0
  29. package/build/database/migrations/1770000000007_kit_webhooks.js +10 -0
  30. package/build/database/migrations/1770000000008_kit_imports.d.ts +5 -0
  31. package/build/database/migrations/1770000000008_kit_imports.js +10 -0
  32. package/build/database/migrations/1770000000010_kit_workflows.d.ts +5 -0
  33. package/build/database/migrations/1770000000010_kit_workflows.js +10 -0
  34. package/build/database/migrations/1770000000011_kit_managed_assignments.d.ts +5 -0
  35. package/build/database/migrations/1770000000011_kit_managed_assignments.js +10 -0
  36. package/build/database/migrations/1770000000012_kit_role_keys.d.ts +5 -0
  37. package/build/database/migrations/1770000000012_kit_role_keys.js +10 -0
  38. package/build/database/migrations/1770000000013_kit_notification_targets.d.ts +5 -0
  39. package/build/database/migrations/1770000000013_kit_notification_targets.js +10 -0
  40. package/build/database/migrations/1770000000014_kit_upload_grants.d.ts +5 -0
  41. package/build/database/migrations/1770000000014_kit_upload_grants.js +10 -0
  42. package/build/database/migrations/1770000000015_kit_inbound_webhooks.d.ts +5 -0
  43. package/build/database/migrations/1770000000015_kit_inbound_webhooks.js +10 -0
  44. package/build/index.d.ts +31 -2
  45. package/build/index.js +17 -2
  46. package/build/src/admin/contracts.d.ts +7 -0
  47. package/build/src/admin/contracts.js +34 -12
  48. package/build/src/admin/controller.d.ts +2 -0
  49. package/build/src/admin/controller.js +52 -1
  50. package/build/src/admin/presentation.d.ts +6 -0
  51. package/build/src/admin/record_title.d.ts +14 -0
  52. package/build/src/admin/record_title.js +50 -0
  53. package/build/src/admin/resource_service.d.ts +184 -2
  54. package/build/src/admin/resource_service.js +713 -48
  55. package/build/src/attachments/attachment_service.d.ts +12 -0
  56. package/build/src/attachments/attachment_service.js +28 -2
  57. package/build/src/attachments/upload_grants.d.ts +44 -0
  58. package/build/src/attachments/upload_grants.js +105 -0
  59. package/build/src/auth/ability.d.ts +1 -1
  60. package/build/src/auth/ability.js +4 -1
  61. package/build/src/auth/actor_store.js +6 -1
  62. package/build/src/auth/conditions.d.ts +15 -0
  63. package/build/src/auth/conditions.js +36 -0
  64. package/build/src/auth/sql.js +6 -2
  65. package/build/src/collaboration/assignments.d.ts +129 -0
  66. package/build/src/collaboration/assignments.js +333 -0
  67. package/build/src/collaboration/record_collaboration.d.ts +86 -0
  68. package/build/src/collaboration/record_collaboration.js +348 -0
  69. package/build/src/commands/agent_assets.js +5 -0
  70. package/build/src/commands/capabilities.d.ts +19 -0
  71. package/build/src/commands/capabilities.js +179 -0
  72. package/build/src/commands/doctor.d.ts +31 -0
  73. package/build/src/commands/doctor.js +114 -0
  74. package/build/src/commands/gap_report.d.ts +50 -2
  75. package/build/src/commands/gap_report.js +102 -4
  76. package/build/src/commands/generator.js +3 -3
  77. package/build/src/commands/snapshot.d.ts +28 -0
  78. package/build/src/commands/snapshot.js +48 -0
  79. package/build/src/commands/source_markers.d.ts +10 -1
  80. package/build/src/commands/source_markers.js +36 -4
  81. package/build/src/core/administration_guard.js +3 -1
  82. package/build/src/core/message_templates.d.ts +83 -0
  83. package/build/src/core/message_templates.js +293 -0
  84. package/build/src/core/module_seed.d.ts +15 -0
  85. package/build/src/core/module_seed.js +31 -0
  86. package/build/src/core/notifications.d.ts +18 -1
  87. package/build/src/core/notifications.js +27 -1
  88. package/build/src/core/roles.d.ts +27 -1
  89. package/build/src/core/roles.js +133 -5
  90. package/build/src/database/schema.d.ts +45 -0
  91. package/build/src/database/schema.js +290 -0
  92. package/build/src/eslint/index.js +26 -0
  93. package/build/src/events/outbox.d.ts +1 -0
  94. package/build/src/events/outbox.js +1 -1
  95. package/build/src/events/record_mutation.d.ts +11 -0
  96. package/build/src/events/record_mutation.js +15 -2
  97. package/build/src/integrations/imports.d.ts +74 -0
  98. package/build/src/integrations/imports.js +333 -0
  99. package/build/src/integrations/inbound_webhooks.d.ts +94 -0
  100. package/build/src/integrations/inbound_webhooks.js +276 -0
  101. package/build/src/integrations/openapi.d.ts +39 -0
  102. package/build/src/integrations/openapi.js +323 -0
  103. package/build/src/integrations/print.d.ts +37 -0
  104. package/build/src/integrations/print.js +124 -0
  105. package/build/src/integrations/webhooks.d.ts +98 -0
  106. package/build/src/integrations/webhooks.js +298 -0
  107. package/build/src/resource/define_resource.d.ts +1 -0
  108. package/build/src/resource/define_resource.js +21 -1
  109. package/build/src/resource/registry.d.ts +3 -0
  110. package/build/src/resource/registry.js +42 -0
  111. package/build/src/resource/types.d.ts +67 -1
  112. package/build/src/resource/values.js +2 -1
  113. package/build/src/services/settings.d.ts +13 -1
  114. package/build/src/services/settings.js +9 -2
  115. package/build/src/workflows/define_workflow.d.ts +124 -0
  116. package/build/src/workflows/define_workflow.js +123 -0
  117. package/build/src/workflows/engine.d.ts +141 -0
  118. package/build/src/workflows/engine.js +752 -0
  119. package/build/stubs/resource_contract.txt +103 -41
  120. package/package.json +7 -3
@@ -5,7 +5,7 @@ import db from '@adonisjs/lucid/services/db'
5
5
  import { kit } from '#services/kit'
6
6
  import { randomUUID } from 'node:crypto'
7
7
  import { columnName } from '@adula/kit'
8
- import type { Field, RecordData, Resource, SerializedRecord } from '@adula/kit'
8
+ import type { Action, Field, RecordData, Resource, SerializedRecord } from '@adula/kit'
9
9
 
10
10
  export type FixtureContext = { userId: number; orgUnitId: number; unique: string }
11
11
  export type ContractFixture = {
@@ -16,9 +16,12 @@ export type ContractFixture = {
16
16
  stored?: RecordData
17
17
  /** All live child rows after creation, ordered by ID, using public field names. */
18
18
  inline?: Record<string, RecordData[]>
19
- /** A valid update for this same record, including required validator fields. */
20
- update: RecordData
21
- updated: SerializedRecord
19
+ /**
20
+ * A valid update for this same record, including required validator fields. Required
21
+ * when the resource declares the `update` action.
22
+ */
23
+ update?: RecordData
24
+ updated?: SerializedRecord
22
25
  updatedStored?: RecordData
23
26
  updatedInline?: Record<string, RecordData[]>
24
27
  }
@@ -48,8 +51,13 @@ export function resourceContract(name: string, fixture: ResourceFixture) {
48
51
  let resource: Resource
49
52
  const base = `/resources/${name}`
50
53
  const fresh = () => fixture({ userId: writer.id, orgUnitId, unique: randomUUID() })
51
- const input = (values: RecordData) => ({ ...values, ...(resource.scoped ? { orgUnitId } : {}) })
54
+ const input = (values: RecordData) => ({
55
+ ...values,
56
+ ...(resource.scoped && !resource.scope ? { orgUnitId } : {}),
57
+ })
52
58
  const version = (row: SerializedRecord) => (resource.version ? { version: row.version } : {})
59
+ // Actions the resource does not declare are refused even to a role that manages all.
60
+ const allows = (action: Action) => resource.actions.includes(action)
53
61
 
54
62
  group.setup(async () => {
55
63
  const knex = db.connection().getWriteClient()
@@ -124,10 +132,27 @@ export function resourceContract(name: string, fixture: ResourceFixture) {
124
132
  const response = await client.get(base).header('Accept', 'application/json')
125
133
  response.assertStatus(401)
126
134
  })
135
+ test('undeclared actions are refused to a role that manages all resources', async ({
136
+ client,
137
+ }) => {
138
+ for (const [action, method, suffix] of [
139
+ ['create', 'post', ''],
140
+ ['update', 'patch', '/999999'],
141
+ ['delete', 'delete', '/999999'],
142
+ ] as const) {
143
+ if (allows(action)) continue
144
+ const response = await client[method](`${base}${suffix}`)
145
+ .loginAs(writer)
146
+ .withCsrfToken()
147
+ .header('Accept', 'application/json')
148
+ response.assertStatus(403)
149
+ }
150
+ })
127
151
  test('existing records enforce scope on reads and writes; central resources stay shared', async ({
128
152
  client,
129
153
  assert,
130
154
  }) => {
155
+ if (!allows('create')) return
131
156
  const values = await fresh()
132
157
  const created = await client
133
158
  .post(base)
@@ -159,21 +184,21 @@ export function resourceContract(name: string, fixture: ResourceFixture) {
159
184
  .get(`${base}/${row.id}/edit`)
160
185
  .loginAs(outsider)
161
186
  .header('Accept', 'application/json')
162
- edit.assertStatus(404)
187
+ edit.assertStatus(allows('update') ? 404 : 403)
163
188
  const changed = await client
164
189
  .patch(`${base}/${row.id}`)
165
190
  .loginAs(outsider)
166
191
  .withCsrfToken()
167
192
  .header('Accept', 'application/json')
168
- .json({ ...values.update, ...version(row) })
169
- changed.assertStatus(404)
193
+ .json({ ...(values.update ?? values.input), ...version(row) })
194
+ changed.assertStatus(allows('update') ? 404 : 403)
170
195
  const deleted = await client
171
196
  .delete(`${base}/${row.id}`)
172
197
  .loginAs(outsider)
173
198
  .withCsrfToken()
174
199
  .header('Accept', 'application/json')
175
200
  .json(version(row))
176
- deleted.assertStatus(404)
201
+ deleted.assertStatus(allows('delete') ? 404 : 403)
177
202
  const after = await client
178
203
  .get(`${base}/${row.id}`)
179
204
  .loginAs(writer)
@@ -185,6 +210,7 @@ export function resourceContract(name: string, fixture: ResourceFixture) {
185
210
  client,
186
211
  assert,
187
212
  }) => {
213
+ if (!allows('create')) return
188
214
  const values = await fresh()
189
215
  for (const key of resource.form)
190
216
  assert.property(values.input, key, `Missing ${name}.${key} fixture`)
@@ -268,64 +294,99 @@ export function resourceContract(name: string, fixture: ResourceFixture) {
268
294
  if (field.permissionLevel || resource.hidden?.includes(key))
269
295
  assert.notProperty(shown.body().data, key)
270
296
  }
271
- for (const key of ['createdBy', 'updatedBy', 'deletedAt', 'orgPath', 'searchVector']) {
272
- const invalid = await client
297
+ const stored = async () => db.connection().getWriteClient()(name).where('id', row.id).first()
298
+ const inserted = await stored()
299
+ assert.equal(inserted.created_by, writer.id)
300
+ if (!allows('update')) {
301
+ const refused = await client
273
302
  .patch(`${base}/${row.id}`)
274
303
  .loginAs(writer)
275
304
  .withCsrfToken()
276
305
  .header('Accept', 'application/json')
277
- .json({ ...values.update, ...version(row), [key]: writer.id })
278
- invalid.assertStatus(422)
279
- assert.equal(invalid.body().error.code, 'E_FIELD_NOT_WRITABLE')
306
+ .json({ ...values.input, ...version(row) })
307
+ refused.assertStatus(403)
280
308
  }
281
- const updated = await client
282
- .patch(`${base}/${row.id}`)
283
- .loginAs(writer)
284
- .withCsrfToken()
285
- .header('Accept', 'application/json')
286
- .json({ ...values.update, ...version(row) })
287
- updated.assertStatus(200)
288
- for (const [key, value] of Object.entries(values.updated))
289
- assert.deepEqual(updated.body().data[key], value, key)
290
- await assertStored(values.update, values.updated, values.updatedStored, values.updatedInline)
291
- if (resource.version) {
292
- assert.equal(updated.body().data.version, Number(row.version) + 1)
293
- const stale = await client
294
- .patch(`${base}/${row.id}`)
309
+ const current = allows('update') ? await updateRecord() : row
310
+ if (!allows('delete')) {
311
+ const refused = await client
312
+ .delete(`${base}/${row.id}`)
295
313
  .loginAs(writer)
296
314
  .withCsrfToken()
297
315
  .header('Accept', 'application/json')
298
- .json({ ...values.update, ...version(row) })
299
- stale.assertStatus(409)
316
+ .json(version(current))
317
+ refused.assertStatus(403)
318
+ const kept = await client
319
+ .get(`${base}/${row.id}`)
320
+ .loginAs(writer)
321
+ .header('Accept', 'application/json')
322
+ kept.assertStatus(200)
323
+ assert.deepEqual(kept.body().data, current)
324
+ return
300
325
  }
301
- const persisted = await client
302
- .get(`${base}/${row.id}`)
303
- .loginAs(writer)
304
- .header('Accept', 'application/json')
305
- persisted.assertStatus(200)
306
- assert.deepEqual(persisted.body().data, updated.body().data)
307
326
  const removed = await client
308
327
  .delete(`${base}/${row.id}`)
309
328
  .loginAs(writer)
310
329
  .withCsrfToken()
311
330
  .header('Accept', 'application/json')
312
- .json(version(updated.body().data))
331
+ .json(version(current))
313
332
  removed.assertStatus(200)
314
- const stored = await db.connection().getWriteClient()(name).where('id', row.id).first()
315
- assert.exists(stored.deleted_at)
316
- assert.equal(stored.created_by, writer.id)
333
+ const deleted = await stored()
334
+ assert.exists(deleted.deleted_at)
317
335
  const missing = await client
318
336
  .get(`${base}/${row.id}`)
319
337
  .loginAs(writer)
320
338
  .header('Accept', 'application/json')
321
339
  missing.assertStatus(404)
340
+
341
+ async function updateRecord() {
342
+ assert.exists(values.update, `Missing ${name} update fixture`)
343
+ assert.exists(values.updated, `Missing ${name} updated fixture`)
344
+ const update = values.update!
345
+ for (const key of ['createdBy', 'updatedBy', 'deletedAt', 'orgPath', 'searchVector']) {
346
+ const invalid = await client
347
+ .patch(`${base}/${row.id}`)
348
+ .loginAs(writer)
349
+ .withCsrfToken()
350
+ .header('Accept', 'application/json')
351
+ .json({ ...update, ...version(row), [key]: writer.id })
352
+ invalid.assertStatus(422)
353
+ assert.equal(invalid.body().error.code, 'E_FIELD_NOT_WRITABLE')
354
+ }
355
+ const updated = await client
356
+ .patch(`${base}/${row.id}`)
357
+ .loginAs(writer)
358
+ .withCsrfToken()
359
+ .header('Accept', 'application/json')
360
+ .json({ ...update, ...version(row) })
361
+ updated.assertStatus(200)
362
+ for (const [key, value] of Object.entries(values.updated!))
363
+ assert.deepEqual(updated.body().data[key], value, key)
364
+ await assertStored(update, values.updated!, values.updatedStored, values.updatedInline)
365
+ if (resource.version) {
366
+ assert.equal(updated.body().data.version, Number(row.version) + 1)
367
+ const stale = await client
368
+ .patch(`${base}/${row.id}`)
369
+ .loginAs(writer)
370
+ .withCsrfToken()
371
+ .header('Accept', 'application/json')
372
+ .json({ ...update, ...version(row) })
373
+ stale.assertStatus(409)
374
+ }
375
+ const persisted = await client
376
+ .get(`${base}/${row.id}`)
377
+ .loginAs(writer)
378
+ .header('Accept', 'application/json')
379
+ persisted.assertStatus(200)
380
+ assert.deepEqual(persisted.body().data, updated.body().data)
381
+ return updated.body().data as SerializedRecord
382
+ }
322
383
  })
323
384
  test('unique constraints reject duplicates and allow reuse after soft deletion', async ({
324
385
  client,
325
386
  assert,
326
387
  }) => {
327
388
  const keys = Object.entries(resource.fields).filter(([, field]) => field.unique)
328
- if (!keys.length) return
389
+ if (!keys.length || !allows('create')) return
329
390
  const values = await fresh()
330
391
  const created = await client
331
392
  .post(base)
@@ -369,6 +430,7 @@ export function resourceContract(name: string, fixture: ResourceFixture) {
369
430
  rejected.assertStatus(409)
370
431
  assert.equal(rejected.body().error.code, 'E_DUPLICATE')
371
432
  }
433
+ if (!allows('delete')) return
372
434
  const removed = await client
373
435
  .delete(`${base}/${row.id}`)
374
436
  .loginAs(writer)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@adula/kit",
3
3
  "description": "Resource-driven business applications for AdonisJS 7",
4
- "version": "0.2.0-alpha.4",
4
+ "version": "1.2.0",
5
5
  "engines": {
6
6
  "node": ">=24.0.0"
7
7
  },
@@ -45,6 +45,7 @@
45
45
  "@jrmc/adonis-mcp": "2.0.0",
46
46
  "@poppinss/ts-exec": "1.4.4",
47
47
  "@types/node": "26.2.0",
48
+ "@types/papaparse": "5.5.2",
48
49
  "eslint": "10.9.0",
49
50
  "pg": "8.16.3",
50
51
  "typescript": "6.0.3"
@@ -74,7 +75,9 @@
74
75
  "prettier": "@adonisjs/prettier-config",
75
76
  "dependencies": {
76
77
  "@casl/ability": "7.0.1",
77
- "knex": "3.3.0"
78
+ "knex": "3.3.0",
79
+ "papaparse": "5.7.0",
80
+ "xstate": "5.33.2"
78
81
  },
79
82
  "repository": {
80
83
  "type": "git",
@@ -89,6 +92,7 @@
89
92
  "build": "node ../../scripts/clean-kit-build.mjs && tsc -p tsconfig.build.json && node ../../scripts/copy-kit-assets.mjs",
90
93
  "typecheck": "tsc --noEmit",
91
94
  "test": "node --import=@poppinss/ts-exec bin/test.ts",
92
- "lint": "eslint src providers commands database tests index.ts"
95
+ "lint": "eslint src providers commands database tests index.ts",
96
+ "capabilities": "node --import=@poppinss/ts-exec bin/generate_capabilities.ts"
93
97
  }
94
98
  }