@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.
- package/guide/agent/app-extras/assistant.md +14 -2
- package/guide/agent/app-extras/realtime.md +38 -1
- package/guide/agent/app-setup/authentication.md +64 -14
- package/guide/agent/app-setup/console.md +8 -0
- package/guide/agent/app-setup/database-layer.md +23 -103
- package/guide/agent/app-setup/existing-application-migration.md +105 -72
- package/guide/agent/app-setup/multi-homing.md +94 -32
- package/guide/agent/app-setup/quickstart.md +1 -1
- package/guide/agent/app-setup/run-an-accounts-hub.md +1 -1
- package/guide/agent/app-setup/upgrading-jskit.md +8 -3
- package/guide/agent/app-setup/users.md +8 -5
- package/guide/agent/framework/application-operations.md +1 -1
- package/guide/agent/framework/crud-operations.md +63 -45
- package/guide/agent/framework/data-access.md +1031 -0
- package/guide/agent/framework/sanity-checks.md +26 -27
- package/guide/agent/framework/typed-features.md +114 -247
- package/guide/agent/index.md +2 -0
- package/package.json +6 -3
- package/patterns/feature-package/PATTERN.md +25 -17
- package/patterns/feature-package/example/booking-engine/package.json +1 -1
- package/patterns/feature-package/example/variations/InvoiceRollupFeature.js +6 -7
- package/patterns/feature-package/example/variations/invoiceResource.js +15 -0
- package/patterns/feature-package/example/variations/invoiceRollupActions.js +4 -4
- package/patterns/feature-package/example/variations/ownedTableRepository.js +27 -0
- package/patterns/feature-package/example/variations/types.js +1 -1
- package/patterns/minimal-foundation/example/AGENTS.md +4 -0
- package/patterns/minimal-foundation/example/package.json +4 -4
- package/patterns/minimal-foundation/example/server.js +4 -1
- package/patterns/shell-foundation/example/AGENTS.md +4 -0
- package/patterns/shell-foundation/example/package.json +5 -5
- package/patterns/shell-foundation/example/packages/main/package.json +1 -1
- package/patterns/shell-foundation/example/server.js +4 -1
- package/reference/autogen/KERNEL_MAP.md +2 -1
- package/reference/autogen/PATTERN_INDEX.md +70 -59
- package/reference/autogen/README.md +1 -1
- package/reference/autogen/packages/agent-docs.md +7 -3
- package/reference/autogen/packages/agent-runtime.md +48 -11
- package/reference/autogen/packages/architecture-inspector.md +6 -2
- package/reference/autogen/packages/assistant-core.md +4 -4
- package/reference/autogen/packages/assistant-runtime.md +33 -13
- package/reference/autogen/packages/auth-provider-local-core.md +3 -1
- package/reference/autogen/packages/auth-provider-local-db-core.md +65 -14
- package/reference/autogen/packages/auth-provider-oidc-core.md +5 -1
- package/reference/autogen/packages/auth-provider-supabase-core.md +1 -1
- package/reference/autogen/packages/connectors-core.md +22 -2
- package/reference/autogen/packages/console-core.md +9 -5
- package/reference/autogen/packages/crud-core.md +38 -173
- package/reference/autogen/packages/database-runtime.md +8 -32
- package/reference/autogen/packages/http-runtime.md +3 -3
- package/reference/autogen/packages/http-web.md +12 -0
- package/reference/autogen/packages/kernel.md +25 -2
- package/reference/autogen/packages/oidc-issuer-core.md +4 -0
- package/reference/autogen/packages/payments-core.md +29 -6
- package/reference/autogen/packages/realtime.md +15 -6
- package/reference/autogen/packages/resource-core.md +30 -5
- package/reference/autogen/packages/resource-crud-core.md +30 -34
- package/reference/autogen/packages/resource-runtime.md +198 -0
- package/reference/autogen/packages/rewarded-core.md +16 -10
- package/reference/autogen/packages/shell-web.md +1 -0
- package/reference/autogen/packages/users-core.md +26 -14
- package/reference/autogen/packages/workspaces-core.md +17 -10
- package/reference/autogen/tooling/config-eslint.md +69 -0
- package/reference/autogen/tooling/jskit-catalog.md +40 -1
- package/reference/autogen/tooling/jskit-release.md +3 -0
- package/skills/jskit/SKILL.md +4 -1
- package/skills/jskit/references/app-operations.md +1 -1
- package/skills/jskit/references/crud-operations.md +62 -44
- package/skills/jskit/references/data-access.md +1026 -0
- package/skills/jskit/references/database-layer.md +411 -0
- package/skills/jskit/references/existing-application-migration.md +105 -72
- package/skills/jskit/references/heuristics/INDEX.md +6 -4
- package/skills/jskit/references/heuristics/crud-authoring.md +22 -14
- package/skills/jskit/references/heuristics/data-access.md +19 -0
- package/skills/jskit/references/heuristics/filters.md +6 -6
- package/skills/jskit/references/heuristics/row-policies.md +40 -16
- package/skills/jskit/references/heuristics/server-search.md +30 -42
- package/skills/jskit/references/heuristics/table-resource-fields.md +78 -0
- package/skills/jskit/references/heuristics/typed-features.md +3 -3
- package/skills/jskit/references/multi-homing.md +94 -32
- package/skills/jskit/references/pattern-index.md +70 -59
- package/skills/jskit/references/patterns/app/minimal-foundation/example/AGENTS.md +4 -0
- package/skills/jskit/references/patterns/app/minimal-foundation/example/package.json +4 -4
- package/skills/jskit/references/patterns/app/minimal-foundation/example/server.js +4 -1
- package/skills/jskit/references/patterns/app/shell-foundation/example/AGENTS.md +4 -0
- package/skills/jskit/references/patterns/app/shell-foundation/example/package.json +5 -5
- package/skills/jskit/references/patterns/app/shell-foundation/example/packages/main/package.json +1 -1
- package/skills/jskit/references/patterns/app/shell-foundation/example/server.js +4 -1
- package/skills/jskit/references/patterns/assistant/assistant-surface/PATTERN.md +5 -2
- package/skills/jskit/references/patterns/auth/accounts-hub/example/package.json +3 -2
- package/skills/jskit/references/patterns/auth/auth-surface/PATTERN.md +15 -0
- package/skills/jskit/references/patterns/auth/oidc-sign-in/example/package.json +2 -2
- package/skills/jskit/references/patterns/auth/supabase-auth/example/package.json +1 -1
- package/skills/jskit/references/patterns/connectors/calendar-cli/example/package.json +4 -4
- package/skills/jskit/references/patterns/crud/crud-contract/PATTERN.md +133 -0
- package/skills/jskit/references/patterns/crud/crud-contract/example/bookResource.js +74 -0
- package/skills/jskit/references/patterns/crud/{json-api-resource-package → crud-feature-package}/PATTERN.md +69 -37
- package/skills/jskit/references/patterns/crud/{json-api-resource-package → crud-feature-package}/example/packages/books/package.json +5 -4
- package/skills/jskit/references/patterns/crud/crud-feature-package/example/packages/books/src/server/BooksFeature.js +13 -0
- package/skills/jskit/references/patterns/crud/crud-feature-package/example/packages/books/src/shared/bookResource.js +74 -0
- package/skills/jskit/references/patterns/crud/crud-feature-package/example/packages/books/src/shared/index.js +1 -0
- package/skills/jskit/references/patterns/crud/crud-screen-set/PATTERN.md +26 -8
- package/skills/jskit/references/patterns/crud/crud-screen-set/example/books/BookEditPage.vue +2 -2
- package/skills/jskit/references/patterns/crud/crud-screen-set/example/books/BookListPage.vue +2 -2
- package/skills/jskit/references/patterns/crud/crud-screen-set/example/books/BookNewPage.vue +2 -2
- package/skills/jskit/references/patterns/crud/crud-screen-set/example/books/BookViewPage.vue +3 -3
- package/skills/jskit/references/patterns/database/mysql-application/PATTERN.md +9 -4
- package/skills/jskit/references/patterns/database/mysql-application/example/package.json +2 -1
- package/skills/jskit/references/patterns/database/mysql-application/example/scripts/prepare-database.js +8 -0
- package/skills/jskit/references/patterns/database/postgres-application/PATTERN.md +8 -4
- package/skills/jskit/references/patterns/database/postgres-application/example/package.json +2 -1
- package/skills/jskit/references/patterns/database/postgres-application/example/scripts/prepare-database.js +8 -0
- package/skills/jskit/references/patterns/database/table-resource/PATTERN.md +117 -0
- package/skills/jskit/references/patterns/database/table-resource/example/orderResource.js +25 -0
- package/skills/jskit/references/patterns/realtime/realtime-application/PATTERN.md +7 -0
- package/skills/jskit/references/patterns/realtime/realtime-application/example/package.json +2 -2
- package/skills/jskit/references/patterns/rewards/google-rewarded/PATTERN.md +1 -1
- package/skills/jskit/references/patterns/server/feature-package/PATTERN.md +25 -17
- package/skills/jskit/references/patterns/server/feature-package/example/booking-engine/package.json +1 -1
- package/skills/jskit/references/patterns/server/feature-package/example/variations/InvoiceRollupFeature.js +6 -7
- package/skills/jskit/references/patterns/server/feature-package/example/variations/invoiceResource.js +15 -0
- package/skills/jskit/references/patterns/server/feature-package/example/variations/invoiceRollupActions.js +4 -4
- package/skills/jskit/references/patterns/server/feature-package/example/variations/ownedTableRepository.js +27 -0
- package/skills/jskit/references/patterns/server/feature-package/example/variations/types.js +1 -1
- package/skills/jskit/references/patterns/users/user-administration-server/PATTERN.md +59 -10
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/package.json +5 -4
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/src/server/UsersFeature.js +7 -5
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/src/shared/index.js +1 -1
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/src/shared/userCrud.js +21 -0
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/package.json +11 -5
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/server/UsersWorkspaceFeature.js +14 -5
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/server/workspaceMembersOnly.js +52 -0
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/shared/index.js +1 -1
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/shared/userCrud.js +21 -0
- package/skills/jskit/references/patterns/workspaces/workspace-server/PATTERN.md +9 -9
- package/skills/jskit/references/patterns/workspaces/workspace-surfaces/PATTERN.md +16 -0
- package/skills/jskit/references/patterns/workspaces/workspace-surfaces/example/src/pages/home/workspaces/index.vue +2 -0
- package/skills/jskit/references/sanity-checks.md +27 -28
- package/skills/jskit/references/typed-features.md +114 -247
- package/patterns/feature-package/example/variations/customKnexRepository.js +0 -40
- package/reference/autogen/packages/json-rest-api-core.md +0 -66
- package/skills/jskit/references/heuristics/crud-repository-mapping.md +0 -140
- package/skills/jskit/references/patterns/crud/json-api-resource-package/example/packages/books/src/server/BooksFeature.js +0 -13
- package/skills/jskit/references/patterns/crud/json-api-resource-package/example/packages/books/src/shared/bookResource.js +0 -83
- package/skills/jskit/references/patterns/crud/json-api-resource-package/example/packages/books/src/shared/index.js +0 -1
- package/skills/jskit/references/patterns/crud/resource-contract/PATTERN.md +0 -155
- package/skills/jskit/references/patterns/crud/resource-contract/example/bookResource.js +0 -87
- package/skills/jskit/references/patterns/server/feature-package/example/variations/customKnexRepository.js +0 -40
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/src/shared/userResource.js +0 -69
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/shared/userResource.js +0 -56
- /package/skills/jskit/references/patterns/crud/{json-api-resource-package → crud-feature-package}/example/migrations/20260815000000_books.cjs +0 -0
- /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
|
|
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:
|
|
18
|
+
id: ACTION_READ_MONTHLY_TOTALS,
|
|
19
19
|
input: emptyInputValidator,
|
|
20
20
|
output: null,
|
|
21
|
-
execute: (input, context) => invoiceRollup.
|
|
21
|
+
execute: (input, context) => invoiceRollup.totalsByMonth(input, { context })
|
|
22
22
|
})
|
|
23
23
|
]);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
export {
|
|
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("./
|
|
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
|
|
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
|
-
`
|
|
31
|
-
and
|
|
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
|
-
|
|
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`,
|
|
58
|
-
service contract. Each package's
|
|
59
|
-
|
|
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
|
-
-
|
|
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 `
|
|
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
|
-
"@jskit-ai/resource-crud-core": "0.1.
|
|
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
|
-
"
|
|
27
|
+
"users.profiles"
|
|
27
28
|
]
|
|
28
29
|
},
|
|
29
30
|
"runtime": {
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { defineCrudFeature } from "@jskit-ai/crud-core/server/defineCrudFeature";
|
|
2
|
+
import { userCrud } from "../shared/userCrud.js";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
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 {
|
|
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
|
-
"@jskit-ai/resource-crud-core": "0.1.
|
|
15
|
-
"@jskit-ai/
|
|
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
|
-
"
|
|
32
|
+
"users.profiles",
|
|
33
|
+
"workspaces.memberships"
|
|
28
34
|
]
|
|
29
35
|
},
|
|
30
36
|
"runtime": {
|
|
@@ -1,13 +1,22 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
3
|
+
import { userCrud } from "../shared/userCrud.js";
|
|
4
|
+
import { workspaceMembersOnly } from "./workspaceMembersOnly.js";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
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 {
|
|
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 },
|
|
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.
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
or
|
|
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 `
|
|
82
|
-
|
|
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
|
-
|
|
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,
|
|
@@ -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
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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 `
|
|
82
|
-
`@jskit-ai/
|
|
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
|
-
([
|
|
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
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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: "
|
|
95
|
-
back, so a write that discards its result passes `returning: "
|
|
96
|
-
a batch
|
|
97
|
-
([
|
|
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](
|
|
115
|
+
databases](database-layer.md#integration-test-databases)).
|
|
117
116
|
|
|
118
117
|
### Inputs and access
|
|
119
118
|
|