@jskit-ai/agent-docs 0.1.282 → 0.1.284

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 (151) hide show
  1. package/guide/agent/app-extras/assistant.md +14 -2
  2. package/guide/agent/app-extras/realtime.md +38 -1
  3. package/guide/agent/app-setup/authentication.md +64 -14
  4. package/guide/agent/app-setup/console.md +8 -0
  5. package/guide/agent/app-setup/database-layer.md +23 -103
  6. package/guide/agent/app-setup/existing-application-migration.md +105 -72
  7. package/guide/agent/app-setup/multi-homing.md +94 -32
  8. package/guide/agent/app-setup/quickstart.md +1 -1
  9. package/guide/agent/app-setup/run-an-accounts-hub.md +1 -1
  10. package/guide/agent/app-setup/upgrading-jskit.md +8 -3
  11. package/guide/agent/app-setup/users.md +8 -5
  12. package/guide/agent/framework/application-operations.md +1 -1
  13. package/guide/agent/framework/crud-operations.md +63 -45
  14. package/guide/agent/framework/data-access.md +1031 -0
  15. package/guide/agent/framework/sanity-checks.md +26 -27
  16. package/guide/agent/framework/typed-features.md +114 -247
  17. package/guide/agent/index.md +2 -0
  18. package/package.json +6 -3
  19. package/patterns/feature-package/PATTERN.md +25 -17
  20. package/patterns/feature-package/example/booking-engine/package.json +1 -1
  21. package/patterns/feature-package/example/variations/InvoiceRollupFeature.js +6 -7
  22. package/patterns/feature-package/example/variations/invoiceResource.js +15 -0
  23. package/patterns/feature-package/example/variations/invoiceRollupActions.js +4 -4
  24. package/patterns/feature-package/example/variations/ownedTableRepository.js +27 -0
  25. package/patterns/feature-package/example/variations/types.js +1 -1
  26. package/patterns/minimal-foundation/example/AGENTS.md +4 -0
  27. package/patterns/minimal-foundation/example/package.json +4 -4
  28. package/patterns/minimal-foundation/example/server.js +4 -1
  29. package/patterns/shell-foundation/example/AGENTS.md +4 -0
  30. package/patterns/shell-foundation/example/package.json +5 -5
  31. package/patterns/shell-foundation/example/packages/main/package.json +1 -1
  32. package/patterns/shell-foundation/example/server.js +4 -1
  33. package/reference/autogen/KERNEL_MAP.md +2 -1
  34. package/reference/autogen/PATTERN_INDEX.md +70 -59
  35. package/reference/autogen/README.md +1 -1
  36. package/reference/autogen/packages/agent-docs.md +7 -3
  37. package/reference/autogen/packages/agent-runtime.md +48 -11
  38. package/reference/autogen/packages/architecture-inspector.md +6 -2
  39. package/reference/autogen/packages/assistant-core.md +4 -4
  40. package/reference/autogen/packages/assistant-runtime.md +33 -13
  41. package/reference/autogen/packages/auth-provider-local-core.md +3 -1
  42. package/reference/autogen/packages/auth-provider-local-db-core.md +65 -14
  43. package/reference/autogen/packages/auth-provider-oidc-core.md +5 -1
  44. package/reference/autogen/packages/auth-provider-supabase-core.md +1 -1
  45. package/reference/autogen/packages/connectors-core.md +22 -2
  46. package/reference/autogen/packages/console-core.md +9 -5
  47. package/reference/autogen/packages/crud-core.md +38 -173
  48. package/reference/autogen/packages/database-runtime.md +8 -32
  49. package/reference/autogen/packages/http-runtime.md +3 -3
  50. package/reference/autogen/packages/http-web.md +12 -0
  51. package/reference/autogen/packages/kernel.md +25 -2
  52. package/reference/autogen/packages/oidc-issuer-core.md +4 -0
  53. package/reference/autogen/packages/payments-core.md +29 -6
  54. package/reference/autogen/packages/realtime.md +15 -6
  55. package/reference/autogen/packages/resource-core.md +30 -5
  56. package/reference/autogen/packages/resource-crud-core.md +30 -34
  57. package/reference/autogen/packages/resource-runtime.md +198 -0
  58. package/reference/autogen/packages/rewarded-core.md +16 -10
  59. package/reference/autogen/packages/shell-web.md +1 -0
  60. package/reference/autogen/packages/users-core.md +26 -14
  61. package/reference/autogen/packages/workspaces-core.md +17 -10
  62. package/reference/autogen/tooling/config-eslint.md +69 -0
  63. package/reference/autogen/tooling/jskit-catalog.md +40 -1
  64. package/reference/autogen/tooling/jskit-release.md +3 -0
  65. package/skills/jskit/SKILL.md +4 -1
  66. package/skills/jskit/references/app-operations.md +1 -1
  67. package/skills/jskit/references/crud-operations.md +62 -44
  68. package/skills/jskit/references/data-access.md +1026 -0
  69. package/skills/jskit/references/database-layer.md +411 -0
  70. package/skills/jskit/references/existing-application-migration.md +105 -72
  71. package/skills/jskit/references/heuristics/INDEX.md +6 -4
  72. package/skills/jskit/references/heuristics/crud-authoring.md +22 -14
  73. package/skills/jskit/references/heuristics/data-access.md +19 -0
  74. package/skills/jskit/references/heuristics/filters.md +6 -6
  75. package/skills/jskit/references/heuristics/row-policies.md +40 -16
  76. package/skills/jskit/references/heuristics/server-search.md +30 -42
  77. package/skills/jskit/references/heuristics/table-resource-fields.md +78 -0
  78. package/skills/jskit/references/heuristics/typed-features.md +3 -3
  79. package/skills/jskit/references/multi-homing.md +94 -32
  80. package/skills/jskit/references/pattern-index.md +70 -59
  81. package/skills/jskit/references/patterns/app/minimal-foundation/example/AGENTS.md +4 -0
  82. package/skills/jskit/references/patterns/app/minimal-foundation/example/package.json +4 -4
  83. package/skills/jskit/references/patterns/app/minimal-foundation/example/server.js +4 -1
  84. package/skills/jskit/references/patterns/app/shell-foundation/example/AGENTS.md +4 -0
  85. package/skills/jskit/references/patterns/app/shell-foundation/example/package.json +5 -5
  86. package/skills/jskit/references/patterns/app/shell-foundation/example/packages/main/package.json +1 -1
  87. package/skills/jskit/references/patterns/app/shell-foundation/example/server.js +4 -1
  88. package/skills/jskit/references/patterns/assistant/assistant-surface/PATTERN.md +5 -2
  89. package/skills/jskit/references/patterns/auth/accounts-hub/example/package.json +3 -2
  90. package/skills/jskit/references/patterns/auth/auth-surface/PATTERN.md +15 -0
  91. package/skills/jskit/references/patterns/auth/oidc-sign-in/example/package.json +2 -2
  92. package/skills/jskit/references/patterns/auth/supabase-auth/example/package.json +1 -1
  93. package/skills/jskit/references/patterns/connectors/calendar-cli/example/package.json +4 -4
  94. package/skills/jskit/references/patterns/crud/crud-contract/PATTERN.md +133 -0
  95. package/skills/jskit/references/patterns/crud/crud-contract/example/bookResource.js +74 -0
  96. package/skills/jskit/references/patterns/crud/{json-api-resource-package → crud-feature-package}/PATTERN.md +69 -37
  97. package/skills/jskit/references/patterns/crud/{json-api-resource-package → crud-feature-package}/example/packages/books/package.json +5 -4
  98. package/skills/jskit/references/patterns/crud/crud-feature-package/example/packages/books/src/server/BooksFeature.js +13 -0
  99. package/skills/jskit/references/patterns/crud/crud-feature-package/example/packages/books/src/shared/bookResource.js +74 -0
  100. package/skills/jskit/references/patterns/crud/crud-feature-package/example/packages/books/src/shared/index.js +1 -0
  101. package/skills/jskit/references/patterns/crud/crud-screen-set/PATTERN.md +26 -8
  102. package/skills/jskit/references/patterns/crud/crud-screen-set/example/books/BookEditPage.vue +2 -2
  103. package/skills/jskit/references/patterns/crud/crud-screen-set/example/books/BookListPage.vue +2 -2
  104. package/skills/jskit/references/patterns/crud/crud-screen-set/example/books/BookNewPage.vue +2 -2
  105. package/skills/jskit/references/patterns/crud/crud-screen-set/example/books/BookViewPage.vue +3 -3
  106. package/skills/jskit/references/patterns/database/mysql-application/PATTERN.md +9 -4
  107. package/skills/jskit/references/patterns/database/mysql-application/example/package.json +2 -1
  108. package/skills/jskit/references/patterns/database/mysql-application/example/scripts/prepare-database.js +8 -0
  109. package/skills/jskit/references/patterns/database/postgres-application/PATTERN.md +8 -4
  110. package/skills/jskit/references/patterns/database/postgres-application/example/package.json +2 -1
  111. package/skills/jskit/references/patterns/database/postgres-application/example/scripts/prepare-database.js +8 -0
  112. package/skills/jskit/references/patterns/database/table-resource/PATTERN.md +117 -0
  113. package/skills/jskit/references/patterns/database/table-resource/example/orderResource.js +25 -0
  114. package/skills/jskit/references/patterns/realtime/realtime-application/PATTERN.md +7 -0
  115. package/skills/jskit/references/patterns/realtime/realtime-application/example/package.json +2 -2
  116. package/skills/jskit/references/patterns/rewards/google-rewarded/PATTERN.md +1 -1
  117. package/skills/jskit/references/patterns/server/feature-package/PATTERN.md +25 -17
  118. package/skills/jskit/references/patterns/server/feature-package/example/booking-engine/package.json +1 -1
  119. package/skills/jskit/references/patterns/server/feature-package/example/variations/InvoiceRollupFeature.js +6 -7
  120. package/skills/jskit/references/patterns/server/feature-package/example/variations/invoiceResource.js +15 -0
  121. package/skills/jskit/references/patterns/server/feature-package/example/variations/invoiceRollupActions.js +4 -4
  122. package/skills/jskit/references/patterns/server/feature-package/example/variations/ownedTableRepository.js +27 -0
  123. package/skills/jskit/references/patterns/server/feature-package/example/variations/types.js +1 -1
  124. package/skills/jskit/references/patterns/users/user-administration-server/PATTERN.md +59 -10
  125. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/package.json +5 -4
  126. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/src/server/UsersFeature.js +7 -5
  127. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/src/shared/index.js +1 -1
  128. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/src/shared/userCrud.js +21 -0
  129. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/package.json +11 -5
  130. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/server/UsersWorkspaceFeature.js +14 -5
  131. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/server/workspaceMembersOnly.js +52 -0
  132. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/shared/index.js +1 -1
  133. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/shared/userCrud.js +21 -0
  134. package/skills/jskit/references/patterns/workspaces/workspace-server/PATTERN.md +9 -9
  135. package/skills/jskit/references/patterns/workspaces/workspace-surfaces/PATTERN.md +16 -0
  136. package/skills/jskit/references/patterns/workspaces/workspace-surfaces/example/src/pages/home/workspaces/index.vue +2 -0
  137. package/skills/jskit/references/sanity-checks.md +27 -28
  138. package/skills/jskit/references/typed-features.md +114 -247
  139. package/patterns/feature-package/example/variations/customKnexRepository.js +0 -40
  140. package/reference/autogen/packages/json-rest-api-core.md +0 -66
  141. package/skills/jskit/references/heuristics/crud-repository-mapping.md +0 -140
  142. package/skills/jskit/references/patterns/crud/json-api-resource-package/example/packages/books/src/server/BooksFeature.js +0 -13
  143. package/skills/jskit/references/patterns/crud/json-api-resource-package/example/packages/books/src/shared/bookResource.js +0 -83
  144. package/skills/jskit/references/patterns/crud/json-api-resource-package/example/packages/books/src/shared/index.js +0 -1
  145. package/skills/jskit/references/patterns/crud/resource-contract/PATTERN.md +0 -155
  146. package/skills/jskit/references/patterns/crud/resource-contract/example/bookResource.js +0 -87
  147. package/skills/jskit/references/patterns/server/feature-package/example/variations/customKnexRepository.js +0 -40
  148. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/src/shared/userResource.js +0 -69
  149. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/shared/userResource.js +0 -56
  150. /package/skills/jskit/references/patterns/crud/{json-api-resource-package → crud-feature-package}/example/migrations/20260815000000_books.cjs +0 -0
  151. /package/skills/jskit/references/patterns/crud/{json-api-resource-package → crud-feature-package}/example/packages/books/src/server/types.js +0 -0
@@ -0,0 +1,15 @@
1
+ import { defineTableResource } from "@jskit-ai/resource-core/shared/tableResource";
2
+
3
+ /** The invoices table: its fields, and the workspace that owns each row. Shared with the client. */
4
+ const invoiceResource = defineTableResource({
5
+ name: "invoices",
6
+ tableName: "invoices",
7
+ ownership: "workspace",
8
+ schema: {
9
+ status: { type: "string", required: true, search: true },
10
+ amount: { type: "number", required: true },
11
+ issuedAt: { type: "dateTime", required: true }
12
+ }
13
+ });
14
+
15
+ export { invoiceResource };
@@ -1,6 +1,6 @@
1
1
  import { emptyInputValidator } from "@jskit-ai/kernel/server/actions/actionContributorHelpers";
2
2
 
3
- const ACTION_GET_ROLLUP_STATUS = "invoices.rollup.status.read";
3
+ const ACTION_READ_MONTHLY_TOTALS = "invoices.rollup.monthly-totals.read";
4
4
 
5
5
  /** The fields every invoice rollup query shares, spread first into each definition: they run on the admin surface. */
6
6
  const ROLLUP_QUERY = Object.freeze({
@@ -15,12 +15,12 @@ function createInvoiceRollupActions({ invoiceRollup }) {
15
15
  return Object.freeze([
16
16
  Object.freeze({
17
17
  ...ROLLUP_QUERY,
18
- id: ACTION_GET_ROLLUP_STATUS,
18
+ id: ACTION_READ_MONTHLY_TOTALS,
19
19
  input: emptyInputValidator,
20
20
  output: null,
21
- execute: (input, context) => invoiceRollup.getStatus(input, { context })
21
+ execute: (input, context) => invoiceRollup.totalsByMonth(input, { context })
22
22
  })
23
23
  ]);
24
24
  }
25
25
 
26
- export { ACTION_GET_ROLLUP_STATUS, createInvoiceRollupActions };
26
+ export { ACTION_READ_MONTHLY_TOTALS, createInvoiceRollupActions };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The invoice rollup's persistence: the queries of the invoices table its
3
+ * feature owns, on that table's handle. The handle adds the workspace to every
4
+ * statement, so no query names it.
5
+ * @param {{ invoices: import("@jskit-ai/resource-runtime/server/types").ResourceHandle }} dependencies
6
+ */
7
+ function createInvoiceRollup({ invoices }) {
8
+ /**
9
+ * How many invoices of each status the workspace issued each month, and their total.
10
+ * @param {Record<string, never>} input
11
+ * @param {import("@jskit-ai/resource-runtime/server/types").ResourceCallOptions} options
12
+ */
13
+ function totalsByMonth(input, options) {
14
+ return invoices.aggregate({
15
+ groupBy: ["status", { field: "issuedAt", bucket: "month", as: "month" }],
16
+ measures: { invoices: { count: "*" }, amount: { sum: "amount" } },
17
+ sort: ["month"]
18
+ }, options);
19
+ }
20
+
21
+ return Object.freeze({
22
+ withTransaction: invoices.withTransaction,
23
+ totalsByMonth
24
+ });
25
+ }
26
+
27
+ export { createInvoiceRollup };
@@ -2,6 +2,6 @@
2
2
  // capabilities, for features that require them.
3
3
 
4
4
  /** @typedef {ReturnType<typeof import("./orchestratorService.js").createAvailabilityEngine>} AvailabilityEngine */
5
- /** @typedef {ReturnType<typeof import("./customKnexRepository.js").createInvoiceRollup>} InvoiceRollup */
5
+ /** @typedef {ReturnType<typeof import("./ownedTableRepository.js").createInvoiceRollup>} InvoiceRollup */
6
6
 
7
7
  export {};
@@ -22,21 +22,66 @@ credentials, or an application that needs no administrative user listing.
22
22
 
23
23
  Choose global versus workspace scope, visible and editable fields, permission
24
24
  policy, route surface, filters, and whether account deletion is part of the
25
- product. Do not expose password/session data through the resource.
25
+ product. Do not expose password, session or sign-in identity data through the
26
+ CRUD contract.
26
27
 
27
28
  ## Framework APIs
28
29
 
29
30
  Use `@jskit-ai/users-core` for profile persistence and policies,
30
- `defineCrudJsonApiFeature()` from `@jskit-ai/crud-core` for standard mechanics,
31
- and the resource contracts from `@jskit-ai/resource-crud-core`.
31
+ `defineCrudFeature()` from `@jskit-ai/crud-core` for standard mechanics,
32
+ and `defineCrudContract()` from `@jskit-ai/resource-crud-core`.
33
+
34
+ users-core is the `users` table's one owner, so an administration feature does
35
+ not own it: `defineCrudFeature({ crud: userCrud, ownerRepository: "users.profiles", ... })`
36
+ serves the CRUD contract through users-core's repository. The workspace
37
+ variant also names `visibility: "workspace"`: its routes are a workspace's,
38
+ so they resolve the caller's workspace and check its `crud.users.*`
39
+ permissions. The `users` table is public, every user's, so the variant also
40
+ narrows the repository to the workspace's members with `decorateRepository`
41
+ (`src/server/workspaceMembersOnly.js`): a list reads only the active members
42
+ from `workspaces.memberships`' `activeMemberIds`, and anyone else is a missing
43
+ record, so a workspace admin never learns who else has an account. The narrowed
44
+ repository answers only `query`, `get` and `pageLimits`: users.profiles' other
45
+ methods (`findByEmail`, `countUsers`, the writes) read or write every user, so
46
+ they never pass through it.
47
+
48
+ The `users` table has one table resource, users-core's `userProfileTableResource`
49
+ (`@jskit-ai/users-core/shared/resources/userProfileResource`). An
50
+ administration package never defines a second one; `jskit check` fails a table
51
+ with two. It builds its CRUD contract on that resource,
52
+ `defineCrudContract(userProfileTableResource, { namespace, operations, fields })`,
53
+ which chooses the operations, their namespace and the fields each returns; a
54
+ field it does not name, such as the sign-in identity, is never shown. The
55
+ list searches with the resource's `q` filter: every word in the display name,
56
+ email or username.
57
+
58
+ Other code that reads or writes the users' records (a decorated service, a
59
+ hook, a job) requires their owners' capabilities, never the `users` or
60
+ `user_settings` tables:
61
+
62
+ - `users.profiles`: `findById`, `findByEmail`, `findByIdentity`,
63
+ `listByEmails(emails, options)` (one read), `upsert`,
64
+ `createWithSettings({ profile, settings }, options)` (the profile and its
65
+ settings in one transaction; settings start from the defaults),
66
+ `createWithId(profile, options)` (a profile keeping an earlier table's id),
67
+ `createManyIgnoringExisting(profiles, options)` (a re-runnable bulk import,
68
+ system context only), `changeUsername(userId, username, options)` and
69
+ `countUsers({ excludeIds, createdSince, authProvider }, options)`.
70
+ - `users.settings`: `findByUserId`, `ensureForUserId` and the settings updates.
71
+
72
+ Their types are `UsersProfiles` and `UsersSettings` in
73
+ `@jskit-ai/users-core/server/types`. Every method takes `options` last and
74
+ joins the caller's transaction: inside `withTransaction(options, async
75
+ (txOptions) => …)`, pass `txOptions` to every call.
32
76
 
33
77
  ## Invariants
34
78
 
35
79
  - The public user id is the resource identity.
36
80
  - Workspace operations are scoped before repository access.
37
81
  - Permission checks happen before records or record existence are disclosed.
38
- - Shared profile persistence remains owned by `@jskit-ai/users-core`.
39
- - The resource is the field contract; the feature projects its repository,
82
+ - Shared profile persistence remains owned by `@jskit-ai/users-core`, and so
83
+ does the `users` table's one table resource.
84
+ - The CRUD contract is the field contract; the feature projects its repository,
40
85
  service, actions, and routes without app-local ceremony.
41
86
  - Each package exports the type of the capability it provides:
42
87
  `src/server/types.js` declares `AppUsers` (for `app.users`) or
@@ -54,9 +99,12 @@ and the resource contracts from `@jskit-ai/resource-crud-core`.
54
99
 
55
100
  `example/packages/users/` is a global administration package.
56
101
  `example/packages/users-workspace/` demonstrates the workspace route/action
57
- boundary, with workspaces-core's `WORKSPACE_CRUD_SCOPE`, while reusing the same
58
- service contract. Each package's
59
- `src/server/types.js` declares the type of the capability it provides. Copy only the composition
102
+ boundary, with workspaces-core's `WORKSPACE_CRUD_SCOPE`, and lists only the
103
+ workspace's members through the same service contract. Each package's `src/shared/userCrud.js` builds its CRUD
104
+ contract on users-core's `userProfileTableResource`, its `package.json` requires
105
+ `users.profiles` (users-core, which owns the table, must be installed), and its
106
+ `src/server/types.js` declares the type of the capability it provides; the
107
+ workspace variant also requires `workspaces.memberships`. Copy only the composition
60
108
  that matches the product and adapt it as ordinary application source.
61
109
 
62
110
  ## Variation points
@@ -71,9 +119,10 @@ isolation, missing records, null/cleared fields, filtering, and pagination.
71
119
 
72
120
  ## Avoid
73
121
 
74
- - copying authentication credential tables into the user resource
122
+ - a second table resource on `users` or `user_settings`
123
+ - copying authentication credential tables into the CRUD contract
75
124
  - trusting route scope without repository scope
76
- - adding app-local mechanics already provided by `defineCrudJsonApiFeature()`
125
+ - adding app-local mechanics already provided by `defineCrudFeature()`
77
126
  - generator metadata or field questionnaires
78
127
  - probing a collaborator, defaulting one to `null`, `{}` or a no-op, setters
79
128
  that fill a slot after construction, and module-level registries
@@ -10,8 +10,9 @@
10
10
  "./shared": "./src/shared/index.js"
11
11
  },
12
12
  "dependencies": {
13
- "@jskit-ai/crud-core": "0.2.14",
14
- "@jskit-ai/resource-crud-core": "0.1.204"
13
+ "@jskit-ai/crud-core": "0.2.15",
14
+ "@jskit-ai/resource-crud-core": "0.1.205",
15
+ "@jskit-ai/users-core": "0.2.16"
15
16
  },
16
17
  "jskit": {
17
18
  "kind": "runtime",
@@ -20,10 +21,10 @@
20
21
  "app.users"
21
22
  ],
22
23
  "requires": [
24
+ "crud.contracts",
23
25
  "runtime.http",
24
- "runtime.json-rest-api",
25
26
  "runtime.actions",
26
- "runtime.database"
27
+ "users.profiles"
27
28
  ]
28
29
  },
29
30
  "runtime": {
@@ -1,12 +1,14 @@
1
- import { defineCrudJsonApiFeature } from "@jskit-ai/crud-core/server/defineCrudJsonApiFeature";
2
- import { resource } from "../shared/userResource.js";
1
+ import { defineCrudFeature } from "@jskit-ai/crud-core/server/defineCrudFeature";
2
+ import { userCrud } from "../shared/userCrud.js";
3
3
 
4
- const UsersFeature = defineCrudJsonApiFeature({
5
- resource,
4
+ // users-core owns the users table; this feature serves the administration
5
+ // CRUD contract through its owner's repository.
6
+ const UsersFeature = defineCrudFeature({
7
+ crud: userCrud,
8
+ ownerRepository: "users.profiles",
6
9
  id: "app.users",
7
10
  capability: "app.users",
8
11
  surface: "home",
9
- ownershipFilter: "public",
10
12
  relativePath: "/users"
11
13
  });
12
14
 
@@ -1 +1 @@
1
- export { resource } from "./userResource.js";
1
+ export { userCrud } from "./userCrud.js";
@@ -0,0 +1,21 @@
1
+ import { defineCrudContract } from "@jskit-ai/resource-crud-core/shared/crudContract";
2
+ import { userProfileTableResource } from "@jskit-ai/users-core/shared/resources/userProfileResource";
3
+
4
+ // The users list and view. The users table has one table resource, users-core's
5
+ // userProfileTableResource; this contract chooses the operations and the fields they
6
+ // return, and the list searches with the resource's q (display name, email and
7
+ // username). It shows only the fields it names, so the sign-in identity and the
8
+ // avatar's storage stay out of administration.
9
+ const userCrud = defineCrudContract(userProfileTableResource, {
10
+ namespace: "users",
11
+ operations: ["list", "view"],
12
+ fields: {
13
+ displayName: { output: "optional" },
14
+ email: { output: "optional" },
15
+ username: { output: "optional" },
16
+ createdAt: { output: "required" },
17
+ updatedAt: { output: "required" }
18
+ }
19
+ });
20
+
21
+ export { userCrud };
@@ -10,9 +10,14 @@
10
10
  "./shared": "./src/shared/index.js"
11
11
  },
12
12
  "dependencies": {
13
- "@jskit-ai/crud-core": "0.2.14",
14
- "@jskit-ai/resource-crud-core": "0.1.204",
15
- "@jskit-ai/workspaces-core": "0.2.15"
13
+ "@jskit-ai/crud-core": "0.2.15",
14
+ "@jskit-ai/resource-crud-core": "0.1.205",
15
+ "@jskit-ai/users-core": "0.2.16",
16
+ "@jskit-ai/workspaces-core": "0.2.16"
17
+ },
18
+ "peerDependencies": {
19
+ "@jskit-ai/kernel": "0.2.14",
20
+ "@jskit-ai/resource-runtime": "0.2.12"
16
21
  },
17
22
  "jskit": {
18
23
  "kind": "runtime",
@@ -21,10 +26,11 @@
21
26
  "app.users-workspace"
22
27
  ],
23
28
  "requires": [
29
+ "crud.contracts",
24
30
  "runtime.http",
25
- "runtime.json-rest-api",
26
31
  "runtime.actions",
27
- "runtime.database"
32
+ "users.profiles",
33
+ "workspaces.memberships"
28
34
  ]
29
35
  },
30
36
  "runtime": {
@@ -1,13 +1,22 @@
1
- import { defineCrudJsonApiFeature } from "@jskit-ai/crud-core/server/defineCrudJsonApiFeature";
1
+ import { defineCrudFeature } from "@jskit-ai/crud-core/server/defineCrudFeature";
2
2
  import { WORKSPACE_CRUD_SCOPE } from "@jskit-ai/workspaces-core/server/support/workspaceCrud";
3
- import { resource } from "../shared/userResource.js";
3
+ import { userCrud } from "../shared/userCrud.js";
4
+ import { workspaceMembersOnly } from "./workspaceMembersOnly.js";
4
5
 
5
- const UsersWorkspaceFeature = defineCrudJsonApiFeature({
6
- resource,
6
+ // users-core owns the users table; this feature serves the administration
7
+ // CRUD contract on a workspace's admin surface through its owner's
8
+ // repository. The routes are a workspace's, so they resolve the caller's
9
+ // workspace and check its crud.users.* permissions, and the repository is
10
+ // narrowed to that workspace's members: the users table is every user's.
11
+ const UsersWorkspaceFeature = defineCrudFeature({
12
+ crud: userCrud,
13
+ ownerRepository: "users.profiles",
14
+ requires: { memberships: "workspaces.memberships" },
15
+ decorateRepository: ({ repository, memberships }) => workspaceMembersOnly(repository, memberships),
16
+ visibility: "workspace",
7
17
  id: "app.users-workspace",
8
18
  capability: "app.users-workspace",
9
19
  surface: "admin",
10
- ownershipFilter: "workspace",
11
20
  relativePath: "/users",
12
21
  scope: WORKSPACE_CRUD_SCOPE
13
22
  });
@@ -0,0 +1,52 @@
1
+ import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
2
+ import { passedOn, resolveWorkspaceScopeValue } from "@jskit-ai/resource-runtime/server";
3
+
4
+ /**
5
+ * users-core's users repository narrowed to the caller's workspace: a list reads only the workspace's active members,
6
+ * and anyone else is a missing record, so a workspace admin never learns who else has an account. The users table is
7
+ * public, so its handle alone would list every user; the narrowing reads the workspace's memberships first. It answers
8
+ * only the reads it narrows (query and get, with pageLimits): the rest of users.profiles, such as findByEmail or
9
+ * countUsers, never passes through this feature's repository, so nothing reaches users past the narrowing through it.
10
+ * @param {import("@jskit-ai/crud-core/server/types").CrudRepository} repository
11
+ * @param {import("@jskit-ai/workspaces-core/server/types").WorkspaceMembershipsRepository} memberships
12
+ */
13
+ function workspaceMembersOnly(repository, memberships) {
14
+ // The caller's workspace's active members; none when the context names no workspace. The memberships get only the
15
+ // context and transaction: the list's own settings (its format, missing) are for the users read.
16
+ function memberIds(options) {
17
+ return memberships.activeMemberIds(resolveWorkspaceScopeValue(options.context), passedOn(options));
18
+ }
19
+
20
+ /**
21
+ * One page of the workspace's members the params select.
22
+ * @param {Record<string, any>} params
23
+ * @param {{ context?: object }} options
24
+ */
25
+ async function query(params, options) {
26
+ const members = await memberIds(options);
27
+ const requested = params.filters?.id;
28
+ // An empty list matches no row: a workspace with no members lists nobody.
29
+ const id = requested === undefined ? members : [requested].flat().map(String).filter((memberId) => members.includes(memberId));
30
+ return repository.query({ ...params, filters: { ...params.filters, id } }, options);
31
+ }
32
+
33
+ /**
34
+ * A member of the workspace, or a missing record for anyone else.
35
+ * @param {unknown} id
36
+ * @param {Record<string, any>} params
37
+ * @param {{ context?: object, missing?: "skip" }} options
38
+ */
39
+ async function get(id, params, options) {
40
+ if ((await memberIds(options)).includes(String(id))) {
41
+ return repository.get(id, params, options);
42
+ }
43
+ if (options.missing === "skip") {
44
+ return null;
45
+ }
46
+ throw new AppError(404, "Document not found.", { code: "crud.document.notFound" });
47
+ }
48
+
49
+ return Object.freeze({ query, get, pageLimits: repository.pageLimits });
50
+ }
51
+
52
+ export { workspaceMembersOnly };
@@ -1 +1 @@
1
- export { resource } from "./userResource.js";
1
+ export { userCrud } from "./userCrud.js";
@@ -0,0 +1,21 @@
1
+ import { defineCrudContract } from "@jskit-ai/resource-crud-core/shared/crudContract";
2
+ import { userProfileTableResource } from "@jskit-ai/users-core/shared/resources/userProfileResource";
3
+
4
+ // The users list and view. The users table has one table resource, users-core's
5
+ // userProfileTableResource; this contract chooses the operations and the fields they
6
+ // return, and the list searches with the resource's q (display name, email and
7
+ // username). It shows only the fields it names, so the sign-in identity and the
8
+ // avatar's storage stay out of administration.
9
+ const userCrud = defineCrudContract(userProfileTableResource, {
10
+ namespace: "users",
11
+ operations: ["list", "view"],
12
+ fields: {
13
+ displayName: { output: "optional" },
14
+ email: { output: "optional" },
15
+ username: { output: "optional" },
16
+ createdAt: { output: "required" },
17
+ updatedAt: { output: "required" }
18
+ }
19
+ });
20
+
21
+ export { userCrud };
@@ -66,20 +66,20 @@ catalogue entries. Updating labels requires no database migration.
66
66
  Provide one function as the optional `workspaces.invite-acceptance-participant`
67
67
  capability, from a small application feature that requires only what its writes
68
68
  need (it cannot require `workspaces.core`, which receives it). The built-in
69
- `workspace.invite.redeem` action invokes `fn({ invite, user }, { trx, context })`
69
+ `workspace.invite.redeem` action invokes `fn({ invite, user }, txOptions)`
70
70
  after validating the recipient, state and expiry and activating membership in the
71
- uncommitted transaction. Forward `trx` to every application owner and propagate
72
- failures. JSKIT marks accepted and commits after the function succeeds. Keep
71
+ uncommitted transaction. Pass `txOptions` to every application owner's calls and
72
+ propagate failures. JSKIT marks accepted and commits after the function succeeds. Keep
73
73
  external effects out of it. Without the capability, acceptance activates the
74
74
  membership only. Allow ordinary invitations; reject stale, revoked, conflicting
75
75
  or unexpectedly missing application associations.
76
76
 
77
- Use `members.prepareInvite(workspace, actor,
78
- payload, { trx, context })` in `repositories.workspaceInvites.withTransaction`
79
- or the same JSON REST API's managed `transaction`. It returns
77
+ Call `members.prepareInvite(workspace, actor, payload, txOptions)` inside
78
+ `repositories.workspaceInvites.withTransaction(options, async (txOptions) => …)`
79
+ or another resource handle's `withTransaction`. It returns
80
80
  `{ createdInviteId, inviteTokenPreview }` without delivery. Lock the workspace
81
- with `repositories.workspaceInvites.lockWorkspaceForInvitations(workspace.id,
82
- { trx })` before application draft/receipt rows, and save the exact invitation
81
+ with `lock(workspace.id, txOptions)` of the `workspaces` capability before
82
+ application draft/receipt rows, and save the exact invitation
83
83
  association and retry receipt before commit. Failure restores the previous
84
84
  pending invitation. Keep access drafts separate: preparation creates a real token.
85
85
 
@@ -92,7 +92,7 @@ failure. A provider may accept a message before failing; the application owns
92
92
  receipts and retries. Keep tokens/URLs out of logs and use controlled mailers in tests.
93
93
 
94
94
  `createInvite`, acceptance and refusal own their managed transactions and
95
- reject a borrowed `options.trx`. Use preparation followed by delivery after commit for
95
+ refuse a call made inside one (`withTransaction({ ...options, outermost: true }, work)`). Use preparation followed by delivery after commit for
96
96
  transactional creation; put related acceptance writes in the participant. Revocation may join
97
97
  a managed transaction. Raw Knex/savepoint transactions cannot own resource
98
98
  writes. Invitation mutations lock workspace then invitation rows, revalidate
@@ -54,6 +54,22 @@ the **Your workspaces** page at `src/pages/home/workspaces/index.vue`, member
54
54
  and settings routes, not-found state, app-owned invitation widgets, and the
55
55
  components that an application's client provider registers.
56
56
 
57
+ `example/src/surfaces/<surface>/` and `example/src/pages/admin/` are each
58
+ surface's own pages; the app puts them under the surface's `pagesRoot`. With
59
+ `pagesRoot: "w/[workspaceSlug]"` for `app` and `"w/[workspaceSlug]/admin"` for
60
+ `admin`:
61
+
62
+ | Example file | App file |
63
+ | --- | --- |
64
+ | `src/surfaces/app/root.vue` | `src/pages/w/[workspaceSlug].vue` |
65
+ | `src/surfaces/app/index.vue` | `src/pages/w/[workspaceSlug]/index.vue` |
66
+ | `src/surfaces/admin/root.vue` | `src/pages/w/[workspaceSlug]/admin.vue` |
67
+ | `src/surfaces/admin/index.vue` | `src/pages/w/[workspaceSlug]/admin/index.vue` |
68
+ | `src/pages/admin/...` | `src/pages/w/[workspaceSlug]/admin/...` |
69
+
70
+ The surface definitions and access policies they need are in the human
71
+ guide's **Workspace tenancy → Surfaces and where `/` lands**.
72
+
57
73
  ## Variation points
58
74
 
59
75
  Change surfaces, routes, section owners, labels, icons, placement order,
@@ -4,4 +4,6 @@
4
4
 
5
5
  <script setup>
6
6
  import WorkspacesClientElement from "@jskit-ai/workspaces-web/client/components/WorkspacesClientElement";
7
+
8
+ definePage({ meta: { jskit: { parent: "/" } } });
7
9
  </script>
@@ -3,10 +3,10 @@
3
3
  # Sanity checks
4
4
 
5
5
  Before calling a list, a save, a test, a refactor, a screen or a deployment
6
- done, measure what it actually costs and does, judge whether that is sane, and
7
- fix the cause when it is not. This page is the JSKIT part of that: the
8
- pitfalls JSKIT's own packages cause, then which JSKIT call, helper or setting
9
- applies each check, with one line on what the check catches and why.
6
+ done, measure what it costs and does, judge whether that is sane, and fix the
7
+ cause when it is not. This page holds JSKIT's part: the pitfalls JSKIT's own
8
+ packages cause, then the JSKIT call, helper or setting that applies each
9
+ check, with one line on what it catches and why.
10
10
 
11
11
  ## JSKIT's own pitfalls
12
12
 
@@ -66,36 +66,35 @@ expects the relationship id or an unselected field never gets it.
66
66
 
67
67
  ### Every row
68
68
 
69
- - An unpaged JSON:API query returns the first 20 rows, and a page above the
70
- resource's `queryMaxLimit` (100 unless it sets one) is cut to that size,
71
- both without an error; CRUD `queryDocuments()` refuses such a `limit`. A
72
- read its callers treat as complete uses `queryAllJsonRestCollectionRows()`
73
- or the repository's `queryEveryDocument()`, because the missing rows
74
- otherwise vanish from every picker and delivery built on it
75
- ([Every row](typed-features.md#every-row)).
76
- - A lookup by a unique key matches at most one row, so it may read `rows[0]`
77
- of a plain query.
69
+ - A handle's `query()` answers one page and refuses a missing or oversized
70
+ `page.size`, because json-rest-api would otherwise cut the list without an
71
+ error. A read its callers treat as complete uses `queryAll()`, because the
72
+ missing rows otherwise vanish from every picker and delivery built on it
73
+ ([Reads](data-access.md#reads-shaped-for-the-screen)).
74
+ - A lookup by a unique key matches at most one row: `get(id)`, or a `query()`
75
+ of page size 1.
78
76
 
79
77
  ### Query counts and batches
80
78
 
81
- - Count a path's queries with `countQueries()` from
82
- `@jskit-ai/database-runtime/server/testDatabases` at a few rows and at a
79
+ - Count a path's queries with `expectFlatQueries()` from
80
+ `@jskit-ai/resource-runtime/test`, or `countQueries()` from
81
+ `@jskit-ai/database-runtime/server/testDatabases`, at a few rows and at a
83
82
  realistic number; the count should follow tables and pages, not rows
84
- ([Query counts at realistic
85
- sizes](typed-features.md#query-counts-at-realistic-sizes)).
83
+ ([Tests](data-access.md#tests)).
86
84
  - A JSON:API read adds a few lookup and visibility queries per page and per
87
- relationship field. That follows pages, so it is expected.
88
- - `getDocumentById()`, `queryDocuments()` or a service `get` inside a loop is
89
- the usual per-row read; batch it with an id-list `searchSchema` filter or
90
- `include`, grouped with `Map.groupBy()` ([Lists read related rows in
91
- batches](typed-features.md#lists-read-related-rows-in-batches)).
85
+ relationship field, and json-rest-api reads each relationship's related ids
86
+ in one statement however many rows the page has. Both follow pages, not
87
+ rows, so `expectFlatQueries` passes them.
88
+ - A handle's `get()` or `query()`, or a service `get`, inside a loop is the
89
+ usual per-row read; batch it with an id-list filter through `queryAll()` or
90
+ `include`, grouped with `Map.groupBy()`
91
+ ([Reads](data-access.md#reads-shaped-for-the-screen)).
92
92
  - A `rowPolicy` adds one correlated predicate to the query it is given, so
93
93
  its cost follows the rows read, not the subject's history. It runs on every
94
- single-record read, including a write's default `returning: "full"` read
95
- back, so a write that discards its result passes `returning: "minimal"` and
96
- a batch of creates uses `createDocuments()`, in the owner's transaction
97
- ([Bounded access
98
- checks](typed-features.md#bounded-access-checks)).
94
+ single-record read, including a write's default `returning: "record"` read
95
+ back, so a write that discards its result passes `returning: "id"` or
96
+ `"none"`, and a batch uses the many-row writes (`createMany()`,
97
+ `patchMany()`) ([Layer 1](data-access.md#layer-1-the-table-resource)).
99
98
 
100
99
  ### Tests
101
100
 
@@ -113,7 +112,7 @@ expects the relationship id or an unselected field never gets it.
113
112
  duration, so slow preparation shows ([Test databases are prepared
114
113
  once](typed-features.md#test-databases-are-prepared-once),
115
114
  [integration test
116
- databases](/guide/app-setup/database-layer#integration-test-databases)).
115
+ databases](database-layer.md#integration-test-databases)).
117
116
 
118
117
  ### Inputs and access
119
118