@jskit-ai/agent-docs 0.1.257 → 0.1.259
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-conversation.md +88 -35
- package/guide/agent/app-extras/assistant.md +28 -28
- package/guide/agent/app-setup/authentication.md +36 -17
- package/guide/agent/app-setup/database-layer.md +116 -17
- package/guide/agent/app-setup/existing-application-migration.md +29 -25
- package/guide/agent/app-setup/multi-homing.md +94 -48
- package/guide/agent/app-setup/run-an-accounts-hub.md +6 -4
- package/guide/agent/framework/client-features.md +496 -0
- package/guide/agent/framework/crud-operations.md +5 -5
- package/guide/agent/framework/translation.md +4 -3
- package/guide/agent/framework/typed-features.md +1104 -0
- package/guide/agent/framework/ui-operations.md +2 -2
- package/guide/agent/index.md +6 -1
- package/package.json +1 -1
- package/patterns/feature-package/PATTERN.md +104 -10
- package/patterns/feature-package/example/booking-engine/package.json +9 -4
- package/patterns/feature-package/example/booking-engine/src/server/BookingEngineFeature.js +24 -0
- package/patterns/feature-package/example/booking-engine/src/server/bookingAvailability.js +10 -0
- package/patterns/feature-package/example/booking-engine/src/server/bookingEngine.js +29 -0
- package/patterns/feature-package/example/booking-engine/src/server/bookingEngineActions.js +26 -0
- package/patterns/feature-package/example/booking-engine/src/server/inputSchemas.js +1 -2
- package/patterns/feature-package/example/booking-engine/src/server/types.js +12 -0
- package/patterns/feature-package/example/variations/AvailabilityEngineFeature.js +19 -0
- package/patterns/feature-package/example/variations/InvoiceRollupFeature.js +23 -0
- package/patterns/feature-package/example/variations/availabilityEngineActions.js +26 -0
- package/patterns/feature-package/example/variations/customKnexRepository.js +32 -26
- package/patterns/feature-package/example/variations/invoiceRollupActions.js +26 -0
- package/patterns/feature-package/example/variations/orchestratorService.js +24 -19
- package/patterns/feature-package/example/variations/types.js +7 -0
- package/patterns/minimal-foundation/PATTERN.md +30 -1
- package/patterns/minimal-foundation/example/AGENTS.md +14 -0
- package/patterns/minimal-foundation/example/bin/develop.js +10 -5
- package/patterns/minimal-foundation/example/eslint.config.mjs +21 -2
- package/patterns/minimal-foundation/example/package.json +5 -4
- package/patterns/minimal-foundation/example/playwright.config.mjs +1 -1
- package/patterns/minimal-foundation/example/server/lib/envFile.js +19 -0
- package/patterns/minimal-foundation/example/server/lib/loggerOptions.js +15 -0
- package/patterns/minimal-foundation/example/server/lib/runtimeEnv.js +17 -17
- package/patterns/minimal-foundation/example/server.js +32 -29
- package/patterns/minimal-foundation/example/tests/server/smoke.test.js +2 -1
- package/patterns/shell-foundation/PATTERN.md +29 -0
- package/patterns/shell-foundation/example/AGENTS.md +14 -0
- package/patterns/shell-foundation/example/bin/develop.js +10 -5
- package/patterns/shell-foundation/example/eslint.config.mjs +21 -2
- package/patterns/shell-foundation/example/package.json +6 -5
- package/patterns/shell-foundation/example/packages/main/package.json +1 -1
- package/patterns/shell-foundation/example/packages/main/src/client/providers/MainClientProvider.js +1 -0
- package/patterns/shell-foundation/example/playwright.config.mjs +1 -1
- package/patterns/shell-foundation/example/server/lib/envFile.js +19 -0
- package/patterns/shell-foundation/example/server/lib/loggerOptions.js +15 -0
- package/patterns/shell-foundation/example/server/lib/runtimeEnv.js +17 -17
- package/patterns/shell-foundation/example/server.js +32 -29
- package/patterns/shell-foundation/example/tests/server/smoke.test.js +2 -1
- package/reference/autogen/KERNEL_MAP.md +14 -8
- package/reference/autogen/PATTERN_INDEX.md +37 -37
- package/reference/autogen/README.md +2 -0
- package/reference/autogen/packages/agent-docs.md +70 -26
- package/reference/autogen/packages/agent-runtime.md +129 -0
- package/reference/autogen/packages/agent.md +149 -0
- package/reference/autogen/packages/architecture-inspector.md +97 -44
- package/reference/autogen/packages/assistant-core.md +786 -292
- package/reference/autogen/packages/assistant-runtime.md +182 -57
- package/reference/autogen/packages/assistant-voice-sherpa.md +4 -0
- package/reference/autogen/packages/assistant-voice.md +54 -8
- package/reference/autogen/packages/auth-core.md +44 -13
- package/reference/autogen/packages/auth-provider-local-core.md +86 -34
- package/reference/autogen/packages/auth-provider-local-db-core.md +1 -2
- package/reference/autogen/packages/auth-provider-oidc-core.md +10 -4
- package/reference/autogen/packages/auth-provider-supabase-core.md +52 -66
- package/reference/autogen/packages/auth-web.md +19 -11
- package/reference/autogen/packages/connectors-catalog.md +71 -43
- package/reference/autogen/packages/connectors-core.md +42 -4
- package/reference/autogen/packages/console-core.md +27 -14
- package/reference/autogen/packages/crud-core.md +178 -170
- package/reference/autogen/packages/database-runtime-mysql.md +0 -2
- package/reference/autogen/packages/database-runtime-postgres.md +0 -2
- package/reference/autogen/packages/database-runtime.md +39 -10
- package/reference/autogen/packages/http-runtime.md +79 -57
- package/reference/autogen/packages/json-rest-api-core.md +11 -8
- package/reference/autogen/packages/kernel.md +144 -83
- package/reference/autogen/packages/oidc-issuer-core.md +49 -20
- package/reference/autogen/packages/payments-core.md +20 -3
- package/reference/autogen/packages/realtime.md +47 -22
- package/reference/autogen/packages/resource-core.md +1 -1
- package/reference/autogen/packages/resource-crud-core.md +64 -26
- package/reference/autogen/packages/rewarded-core.md +47 -36
- package/reference/autogen/packages/shell-web.md +7 -10
- package/reference/autogen/packages/storage-runtime.md +6 -2
- package/reference/autogen/packages/uploads-runtime.md +10 -2
- package/reference/autogen/packages/users-core.md +73 -71
- package/reference/autogen/packages/workspaces-core.md +121 -106
- package/reference/autogen/packages/workspaces-web.md +1 -2
- package/reference/autogen/tooling/config-eslint.md +302 -0
- package/reference/autogen/tooling/jskit-catalog.md +28 -18
- package/reference/autogen/tooling/jskit-release.md +35 -20
- package/reference/autogen/tooling/testUtils.md +3 -1
- package/skills/jskit/SKILL.md +9 -8
- package/skills/jskit/references/app-operations.md +1 -1
- package/skills/jskit/references/client-features.md +491 -0
- package/skills/jskit/references/crud-form-actions.md +1 -1
- package/skills/jskit/references/crud-operations.md +7 -7
- package/skills/jskit/references/existing-application-migration.md +31 -27
- package/skills/jskit/references/heuristics/INDEX.md +5 -1
- package/skills/jskit/references/heuristics/child-cruds.md +1 -1
- package/skills/jskit/references/heuristics/client-features.md +66 -0
- package/skills/jskit/references/heuristics/client-requests.md +1 -1
- package/skills/jskit/references/heuristics/crud-authoring.md +12 -2
- package/skills/jskit/references/heuristics/crud-links.md +1 -1
- package/skills/jskit/references/heuristics/crud-repository-mapping.md +3 -3
- package/skills/jskit/references/heuristics/filters.md +1 -1
- package/skills/jskit/references/heuristics/live-actions.md +1 -1
- package/skills/jskit/references/heuristics/page-redirects.md +1 -1
- package/skills/jskit/references/heuristics/page-scaffolding.md +1 -1
- package/skills/jskit/references/heuristics/placements.md +1 -1
- package/skills/jskit/references/heuristics/row-policies.md +1 -1
- package/skills/jskit/references/heuristics/server-search.md +1 -1
- package/skills/jskit/references/heuristics/surfaces.md +1 -1
- package/skills/jskit/references/heuristics/typed-features.md +100 -0
- package/skills/jskit/references/heuristics/ui-contract.md +1 -1
- package/skills/jskit/references/heuristics/ui-testing.md +4 -4
- package/skills/jskit/references/material-3.md +2 -2
- package/skills/jskit/references/multi-homing.md +96 -50
- package/skills/jskit/references/pattern-index.md +37 -37
- package/skills/jskit/references/patterns/app/minimal-foundation/PATTERN.md +30 -1
- package/skills/jskit/references/patterns/app/minimal-foundation/example/AGENTS.md +14 -0
- package/skills/jskit/references/patterns/app/minimal-foundation/example/bin/develop.js +10 -5
- package/skills/jskit/references/patterns/app/minimal-foundation/example/eslint.config.mjs +21 -2
- package/skills/jskit/references/patterns/app/minimal-foundation/example/package.json +5 -4
- package/skills/jskit/references/patterns/app/minimal-foundation/example/playwright.config.mjs +1 -1
- package/skills/jskit/references/patterns/app/minimal-foundation/example/server/lib/envFile.js +19 -0
- package/skills/jskit/references/patterns/app/minimal-foundation/example/server/lib/loggerOptions.js +15 -0
- package/skills/jskit/references/patterns/app/minimal-foundation/example/server/lib/runtimeEnv.js +17 -17
- package/skills/jskit/references/patterns/app/minimal-foundation/example/server.js +32 -29
- package/skills/jskit/references/patterns/app/minimal-foundation/example/tests/server/smoke.test.js +2 -1
- package/skills/jskit/references/patterns/app/shell-foundation/PATTERN.md +29 -0
- package/skills/jskit/references/patterns/app/shell-foundation/example/AGENTS.md +14 -0
- package/skills/jskit/references/patterns/app/shell-foundation/example/bin/develop.js +10 -5
- package/skills/jskit/references/patterns/app/shell-foundation/example/eslint.config.mjs +21 -2
- package/skills/jskit/references/patterns/app/shell-foundation/example/package.json +6 -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/packages/main/src/client/providers/MainClientProvider.js +1 -0
- package/skills/jskit/references/patterns/app/shell-foundation/example/playwright.config.mjs +1 -1
- package/skills/jskit/references/patterns/app/shell-foundation/example/server/lib/envFile.js +19 -0
- package/skills/jskit/references/patterns/app/shell-foundation/example/server/lib/loggerOptions.js +15 -0
- package/skills/jskit/references/patterns/app/shell-foundation/example/server/lib/runtimeEnv.js +17 -17
- package/skills/jskit/references/patterns/app/shell-foundation/example/server.js +32 -29
- package/skills/jskit/references/patterns/app/shell-foundation/example/tests/server/smoke.test.js +2 -1
- package/skills/jskit/references/patterns/assistant/assistant-surface/example/src/ApplicationAiFeature.js +4 -2
- package/skills/jskit/references/patterns/auth/accounts-hub/PATTERN.md +2 -1
- package/skills/jskit/references/patterns/auth/accounts-hub/example/package.json +2 -2
- package/skills/jskit/references/patterns/auth/accounts-hub/example/server/hubPaths.js +9 -3
- package/skills/jskit/references/patterns/auth/auth-surface/PATTERN.md +25 -12
- package/skills/jskit/references/patterns/auth/auth-surface/example/tools/preview-identity +7 -1
- 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/ai-connections/PATTERN.md +16 -1
- package/skills/jskit/references/patterns/connectors/ai-connections/example/ai-model.js +12 -3
- package/skills/jskit/references/patterns/connectors/ai-connections/example/perplexity-answer.js +15 -4
- package/skills/jskit/references/patterns/connectors/api-key-connection/PATTERN.md +9 -1
- package/skills/jskit/references/patterns/connectors/api-key-connection/example/connections.js +5 -2
- package/skills/jskit/references/patterns/connectors/aws-storage-queries/PATTERN.md +13 -3
- package/skills/jskit/references/patterns/connectors/aws-storage-queries/example/s3-transfer.js +14 -3
- package/skills/jskit/references/patterns/connectors/calendar-cli/PATTERN.md +8 -0
- package/skills/jskit/references/patterns/connectors/calendar-cli/example/knexfile.js +1 -1
- package/skills/jskit/references/patterns/connectors/calendar-cli/example/package.json +4 -4
- package/skills/jskit/references/patterns/connectors/calendar-cli/example/scripts/calendar.js +8 -4
- package/skills/jskit/references/patterns/connectors/event-delivery/PATTERN.md +1 -1
- package/skills/jskit/references/patterns/connectors/firebase-messaging/PATTERN.md +10 -0
- package/skills/jskit/references/patterns/connectors/firebase-messaging/example/server/notifications.js +8 -0
- package/skills/jskit/references/patterns/connectors/google-ads-search/PATTERN.md +18 -7
- package/skills/jskit/references/patterns/connectors/google-ads-search/example/ads-setup.js +13 -5
- package/skills/jskit/references/patterns/connectors/oauth-connection/PATTERN.md +1 -1
- package/skills/jskit/references/patterns/connectors/paddle-catalogue/PATTERN.md +11 -2
- package/skills/jskit/references/patterns/connectors/paddle-catalogue/example/create-products.js +5 -2
- package/skills/jskit/references/patterns/connectors/public-image/PATTERN.md +10 -1
- package/skills/jskit/references/patterns/connectors/public-image/example/logo-url.js +4 -1
- package/skills/jskit/references/patterns/connectors/redshift-queries/PATTERN.md +1 -1
- package/skills/jskit/references/patterns/connectors/source-scanning/PATTERN.md +13 -2
- package/skills/jskit/references/patterns/connectors/source-scanning/example/source-scanner.js +9 -0
- package/skills/jskit/references/patterns/crud/json-api-resource-package/PATTERN.md +102 -23
- package/skills/jskit/references/patterns/crud/json-api-resource-package/example/packages/books/package.json +3 -2
- package/skills/jskit/references/patterns/crud/json-api-resource-package/example/packages/books/src/server/types.js +5 -0
- package/skills/jskit/references/patterns/crud/resource-contract/PATTERN.md +54 -1
- package/skills/jskit/references/patterns/database/mysql-application/PATTERN.md +16 -5
- package/skills/jskit/references/patterns/database/mysql-application/example/knexfile.js +2 -2
- package/skills/jskit/references/patterns/database/mysql-application/example/package.json +1 -1
- package/skills/jskit/references/patterns/database/mysql-application/example/scripts/prepare-database.js +1 -1
- package/skills/jskit/references/patterns/database/postgres-application/PATTERN.md +16 -5
- package/skills/jskit/references/patterns/database/postgres-application/example/knexfile.js +2 -2
- package/skills/jskit/references/patterns/database/postgres-application/example/package.json +1 -1
- package/skills/jskit/references/patterns/database/postgres-application/example/scripts/prepare-database.js +1 -1
- package/skills/jskit/references/patterns/realtime/realtime-application/example/package.json +2 -2
- package/skills/jskit/references/patterns/rewards/google-rewarded/example/GoogleRewardedDeliveryProvider.js +1 -1
- package/skills/jskit/references/patterns/rewards/google-rewarded/example/googlePublisherTag.js +5 -11
- package/skills/jskit/references/patterns/server/feature-package/PATTERN.md +104 -10
- package/skills/jskit/references/patterns/server/feature-package/example/booking-engine/package.json +9 -4
- package/skills/jskit/references/patterns/server/feature-package/example/booking-engine/src/server/BookingEngineFeature.js +24 -0
- package/skills/jskit/references/patterns/server/feature-package/example/booking-engine/src/server/bookingAvailability.js +10 -0
- package/skills/jskit/references/patterns/server/feature-package/example/booking-engine/src/server/bookingEngine.js +29 -0
- package/skills/jskit/references/patterns/server/feature-package/example/booking-engine/src/server/bookingEngineActions.js +26 -0
- package/skills/jskit/references/patterns/server/feature-package/example/booking-engine/src/server/inputSchemas.js +1 -2
- package/skills/jskit/references/patterns/server/feature-package/example/booking-engine/src/server/types.js +12 -0
- package/skills/jskit/references/patterns/server/feature-package/example/variations/AvailabilityEngineFeature.js +19 -0
- package/skills/jskit/references/patterns/server/feature-package/example/variations/InvoiceRollupFeature.js +23 -0
- package/skills/jskit/references/patterns/server/feature-package/example/variations/availabilityEngineActions.js +26 -0
- package/skills/jskit/references/patterns/server/feature-package/example/variations/customKnexRepository.js +32 -26
- package/skills/jskit/references/patterns/server/feature-package/example/variations/invoiceRollupActions.js +26 -0
- package/skills/jskit/references/patterns/server/feature-package/example/variations/orchestratorService.js +24 -19
- package/skills/jskit/references/patterns/server/feature-package/example/variations/types.js +7 -0
- package/skills/jskit/references/patterns/users/user-administration-server/PATTERN.md +16 -1
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/package.json +3 -2
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/src/server/types.js +5 -0
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/package.json +4 -3
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/server/UsersWorkspaceFeature.js +2 -9
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/server/types.js +5 -0
- package/skills/jskit/references/patterns/workspaces/workspace-server/PATTERN.md +42 -17
- package/skills/jskit/references/patterns/workspaces/workspace-server/example/packages/main/src/server/WorkspaceInvitationsFeature.js +27 -0
- package/skills/jskit/references/patterns/workspaces/workspace-server/example/packages/main/src/server/email/workspaceInviteEmail.js +11 -10
- package/skills/jskit/references/patterns/workspaces/workspace-server/example/packages/main/src/server/email/workspaceInviteMailer.js +27 -0
- package/skills/jskit/references/translation.md +5 -4
- package/skills/jskit/references/typed-features.md +1099 -0
- package/skills/jskit/references/ui-operations.md +3 -3
- package/patterns/feature-package/example/booking-engine/src/server/BookingEngineProvider.js +0 -33
- package/patterns/feature-package/example/booking-engine/src/server/actions.js +0 -24
- package/patterns/feature-package/example/variations/AvailabilityEngineProvider.js +0 -31
- package/patterns/feature-package/example/variations/InvoiceRollupProvider.js +0 -36
- package/skills/jskit/references/patterns/server/feature-package/example/booking-engine/src/server/BookingEngineProvider.js +0 -33
- package/skills/jskit/references/patterns/server/feature-package/example/booking-engine/src/server/actions.js +0 -24
- package/skills/jskit/references/patterns/server/feature-package/example/variations/AvailabilityEngineProvider.js +0 -31
- package/skills/jskit/references/patterns/server/feature-package/example/variations/InvoiceRollupProvider.js +0 -36
|
@@ -0,0 +1,1099 @@
|
|
|
1
|
+
<!-- Generated from `packages/agent-docs/site/guide/framework/typed-features.md`. -->
|
|
2
|
+
|
|
3
|
+
# Typed features
|
|
4
|
+
|
|
5
|
+
Read this page before writing or reviewing JSKIT server code: a feature,
|
|
6
|
+
provider, action, service, repository, route, error or test. Each rule says
|
|
7
|
+
what to do, why in one line, and shows good and bad code. `npm run lint`
|
|
8
|
+
reports the rules a machine can see ([checks](#checks)); the others are
|
|
9
|
+
checked by reading. The `server/feature-package` pattern in the
|
|
10
|
+
[bundled source pattern index](pattern-index.md) is the smallest complete feature to copy.
|
|
11
|
+
|
|
12
|
+
The [typed features heuristic](heuristics/typed-features.md)
|
|
13
|
+
maps keywords and lint findings to the section that fixes them. Client code
|
|
14
|
+
(composables, components, client factories and providers) follows the same
|
|
15
|
+
rule in [client features](client-features.md).
|
|
16
|
+
|
|
17
|
+
## The rule
|
|
18
|
+
|
|
19
|
+
> Every collaborator a feature uses arrives through its declared JSKIT
|
|
20
|
+
> capabilities, and is typed where it is received and required, never probed,
|
|
21
|
+
> so each call has exactly one visible destination for a reader and for the
|
|
22
|
+
> code map.
|
|
23
|
+
|
|
24
|
+
- A **capability** is a value a feature `provides`, `requires` or may use
|
|
25
|
+
(`optional`), named by a **token** such as `"feature.invoices"`.
|
|
26
|
+
- A **collaborator** is an object or function with behaviour that code calls:
|
|
27
|
+
a service, store, repository, runtime, router, publisher, catalogue, logger
|
|
28
|
+
or clock. Plain data (an input, a record, a list, an options bag) is not one.
|
|
29
|
+
- A **slot** is a place that holds one value someone else supplies.
|
|
30
|
+
- A **refusal** is an error meant for the caller: bad input, not found,
|
|
31
|
+
forbidden, conflict.
|
|
32
|
+
|
|
33
|
+
Following a call then takes one step: the parameter names its type, the type
|
|
34
|
+
names its file. The architecture inspector's code map takes the same step, so
|
|
35
|
+
it reports the call as exact instead of guessing from the method name.
|
|
36
|
+
|
|
37
|
+
## Features
|
|
38
|
+
|
|
39
|
+
### Feature files
|
|
40
|
+
|
|
41
|
+
**A `defineFeature` lives in `<Name>Feature.js`, its keys in one order, one
|
|
42
|
+
per line: `id, domain, requires, optional, provides, setup, actions, boot,
|
|
43
|
+
shutdown`.** A `defineProvider` lives in `<Name>Provider.js`.
|
|
44
|
+
|
|
45
|
+
- `setup` builds each provided value with a named `create<Name>` factory from
|
|
46
|
+
a module of its own, never inline.
|
|
47
|
+
- `actions` is a method that returns `create<Name>Actions({ … })`.
|
|
48
|
+
- The hooks take `capabilities`, destructured and typed.
|
|
49
|
+
|
|
50
|
+
Why: every feature reads the same way, top to bottom.
|
|
51
|
+
|
|
52
|
+
```js
|
|
53
|
+
// good
|
|
54
|
+
const InvoicesFeature = defineFeature({
|
|
55
|
+
id: "feature.invoices",
|
|
56
|
+
requires: {
|
|
57
|
+
database: "runtime.database"
|
|
58
|
+
},
|
|
59
|
+
provides: {
|
|
60
|
+
invoices: "feature.invoices"
|
|
61
|
+
},
|
|
62
|
+
/** @param {{ database: import("@jskit-ai/database-runtime/server/types").RuntimeDatabase }} capabilities */
|
|
63
|
+
setup({ database }) {
|
|
64
|
+
return { invoices: createInvoices({ knex: database.knex }) };
|
|
65
|
+
},
|
|
66
|
+
actions({ invoices }) {
|
|
67
|
+
return createInvoiceActions({ invoices });
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// bad: the value built inline, the actions an inline array
|
|
72
|
+
setup: ({ database }) => ({ invoices: { list: () => database.knex("invoices") } }),
|
|
73
|
+
actions: ({ invoices }) => [{ id: "invoices.list", execute: () => invoices.list() }]
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Capability types
|
|
77
|
+
|
|
78
|
+
**Each provided token has one `@typedef` in its package's `src/server/types.js`,
|
|
79
|
+
exported as `./server/types`. A type used in two or more files is named once
|
|
80
|
+
there.**
|
|
81
|
+
|
|
82
|
+
- Derive it from the factory, `ReturnType<typeof createInvoices>`, and name it
|
|
83
|
+
after the token in PascalCase (`feature.invoices` is `Invoices`). Each token
|
|
84
|
+
gets its own name, even when it aliases another type.
|
|
85
|
+
- Import a type from the package that owns it; never re-alias it through
|
|
86
|
+
another module. A JSDoc `import("…")` is type-only, so a package cycle is no
|
|
87
|
+
reason to hand-write one.
|
|
88
|
+
- Hand-write a contract only for a token with several implementations (such as
|
|
89
|
+
`auth.service`), in its owning package, with optional operations marked
|
|
90
|
+
optional.
|
|
91
|
+
- JSKIT's names: `RuntimeKnex`, `RuntimeKnexTransaction`,
|
|
92
|
+
`RuntimeKnexQueryBuilder`, `RuntimeRouter`, `RuntimeLogger`, `RuntimeEnv`,
|
|
93
|
+
`CrudService`, `CrudRepository`.
|
|
94
|
+
|
|
95
|
+
Why: a typed parameter tells a reader and the code map which file a call
|
|
96
|
+
reaches.
|
|
97
|
+
|
|
98
|
+
```js
|
|
99
|
+
// good: src/server/types.js
|
|
100
|
+
/** @typedef {ReturnType<typeof import("./invoices.js").createInvoices>} Invoices */
|
|
101
|
+
export {};
|
|
102
|
+
// a consumer in another package
|
|
103
|
+
/** @param {{ invoices: import("@local/invoices/server/types").Invoices }} dependencies */
|
|
104
|
+
|
|
105
|
+
// bad: spelled out at every use, or a hand-written interface of `any`
|
|
106
|
+
/** @param {{ invoices: ReturnType<typeof import("../../invoices/src/server/invoices.js").createInvoices> }} dependencies */
|
|
107
|
+
/** @typedef {{ list: (...args: any[]) => Promise<any> }} InvoiceReads */
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Typed collaborators
|
|
111
|
+
|
|
112
|
+
**Every factory, constructor, hook and helper that calls a collaborator
|
|
113
|
+
destructures it in its signature and types it with a JSDoc `@param`.**
|
|
114
|
+
|
|
115
|
+
- That includes `setup()`, `boot()` and `shutdown()`, route registrars,
|
|
116
|
+
formatters and repository helpers. A hook that only passes capabilities on
|
|
117
|
+
needs no type.
|
|
118
|
+
- Pure helpers (validators, mappers, id builders) are imported, never passed
|
|
119
|
+
in.
|
|
120
|
+
|
|
121
|
+
Why: the type is what turns a call into one step.
|
|
122
|
+
|
|
123
|
+
```js
|
|
124
|
+
// good
|
|
125
|
+
/**
|
|
126
|
+
* The invoices the feature provides.
|
|
127
|
+
* @param {{ knex: import("@jskit-ai/database-runtime/server/types").RuntimeKnex }} dependencies
|
|
128
|
+
*/
|
|
129
|
+
function createInvoices({ knex }) {
|
|
130
|
+
|
|
131
|
+
// bad: untyped, read from a bag in the body
|
|
132
|
+
function createInvoices(dependencies) {
|
|
133
|
+
const knex = dependencies.knex;
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Required collaborators
|
|
137
|
+
|
|
138
|
+
**A collaborator is required: no `= null`, `= {}` or no-op default, no `typeof
|
|
139
|
+
x.method === "function"` probe, no `x?.method?.()` and no "x is required"
|
|
140
|
+
guard.**
|
|
141
|
+
|
|
142
|
+
- A default that is the production implementation stays: `fetchImpl =
|
|
143
|
+
globalThis.fetch`, `now = Date.now`. One clock per factory.
|
|
144
|
+
- An *optional operation* is a contract method some real implementer lacks.
|
|
145
|
+
An operation every implementer has, including one that refuses while its
|
|
146
|
+
feature is switched off, is required and never probed.
|
|
147
|
+
- A per-call callback in an options bag is data: resolve it once with a named
|
|
148
|
+
no-op, `const { onProgress = reportNoProgress } = options`.
|
|
149
|
+
- A public entry point that checks what an application hands it, such as
|
|
150
|
+
`defineFeature()` checking a definition, is input validation and stays, with
|
|
151
|
+
its [exception comment](#exceptions).
|
|
152
|
+
|
|
153
|
+
Why: the capability runtime already refuses to start without a required
|
|
154
|
+
token; a probe gives each call a second, invisible outcome.
|
|
155
|
+
|
|
156
|
+
```js
|
|
157
|
+
// good
|
|
158
|
+
function createReminders({ mailer, now = Date.now }) {
|
|
159
|
+
async function send(invoice) {
|
|
160
|
+
await mailer.send(invoice.email, { sentAt: now() });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// bad
|
|
164
|
+
function createReminders({ mailer = null, onSent }) {
|
|
165
|
+
async function send(invoice) {
|
|
166
|
+
if (typeof mailer?.send !== "function") return;
|
|
167
|
+
await mailer.send(invoice.email);
|
|
168
|
+
onSent?.(invoice);
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### One shape per value
|
|
173
|
+
|
|
174
|
+
**A value arrives in one shape. Never accept `capability.service ||
|
|
175
|
+
capability` or `x?.value ?? x`.**
|
|
176
|
+
|
|
177
|
+
Why: production takes one branch and the tests take the other, so a reader
|
|
178
|
+
follows two.
|
|
179
|
+
|
|
180
|
+
```js
|
|
181
|
+
// good: a CRUD capability's service, and a test double with the same shape
|
|
182
|
+
const audits = auditsCapability.service;
|
|
183
|
+
createAuditing({ auditsCapability: { service: auditsDouble } });
|
|
184
|
+
|
|
185
|
+
// bad
|
|
186
|
+
const audits = auditsCapability.service || auditsCapability;
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Environment from the caller
|
|
190
|
+
|
|
191
|
+
**Code reads the environment its caller passes, typed `RuntimeEnv`. Only the
|
|
192
|
+
application's entry point reads `process.env`, and passes it on.**
|
|
193
|
+
|
|
194
|
+
- A third-party client is a named default resolved once in the factory that
|
|
195
|
+
owns it: `mailClient = createMailClient(config)`.
|
|
196
|
+
- An application loads `.env` once per process, in one module every entry
|
|
197
|
+
point calls.
|
|
198
|
+
|
|
199
|
+
Why: a function that falls back to the process environment hides a caller
|
|
200
|
+
that forgot to pass one, and every test inherits the developer's shell.
|
|
201
|
+
|
|
202
|
+
```js
|
|
203
|
+
// good
|
|
204
|
+
/** @param {{ env: import("@jskit-ai/kernel/server/types").RuntimeEnv }} options */
|
|
205
|
+
function createMailer({ env }) {
|
|
206
|
+
// server.js, the entry point
|
|
207
|
+
const mailer = createMailer({ env: resolveRuntimeEnv() });
|
|
208
|
+
|
|
209
|
+
// bad
|
|
210
|
+
function createMailer({ env = process.env } = {}) {
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Optional tokens
|
|
214
|
+
|
|
215
|
+
**A slot is an `optional` token with a named default, resolved once in
|
|
216
|
+
`setup()` with `??`.**
|
|
217
|
+
|
|
218
|
+
- The declaring package names the default beside the slot's type
|
|
219
|
+
(`sequentialNumbering`).
|
|
220
|
+
- `X | null` appears only at the receiving hook; what `setup()` builds
|
|
221
|
+
receives the value as required. A disabled implementation is a named value
|
|
222
|
+
(`DISABLED_MAILER`), never `null`.
|
|
223
|
+
- Anything that holds one value is a slot, however it is named (`set*`,
|
|
224
|
+
`register*`, `attach*`). Catalogues stay catalogues: routes on
|
|
225
|
+
`http.router`, contributors on `runtime.bootstrap`, listeners on
|
|
226
|
+
`runtime.events`, actions and placements.
|
|
227
|
+
- When the slot's supplier requires the slot's owner, the two form a cycle.
|
|
228
|
+
Split the part of the owner the supplier needs into its own feature. Until
|
|
229
|
+
then the setter stays, with a `cycle:` [exception comment](#exceptions), and
|
|
230
|
+
is cleared at shutdown.
|
|
231
|
+
|
|
232
|
+
Why: a slot filled later makes a call's destination depend on what ran
|
|
233
|
+
before.
|
|
234
|
+
|
|
235
|
+
```js
|
|
236
|
+
// good: the owner declares the slot and resolves it once
|
|
237
|
+
optional: {
|
|
238
|
+
numbering: "feature.invoices.numbering"
|
|
239
|
+
},
|
|
240
|
+
/** @param {{ numbering: import("./types.js").InvoiceNumbering | null }} capabilities */
|
|
241
|
+
setup({ numbering }) {
|
|
242
|
+
return { invoices: createInvoices({ numbering: numbering ?? sequentialNumbering }) };
|
|
243
|
+
},
|
|
244
|
+
// another package fills it by providing the token
|
|
245
|
+
const YearlyNumberingFeature = defineFeature({
|
|
246
|
+
id: "feature.yearly-numbering",
|
|
247
|
+
provides: {
|
|
248
|
+
numbering: "feature.invoices.numbering"
|
|
249
|
+
},
|
|
250
|
+
setup: () => ({ numbering: yearlyNumber })
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// bad
|
|
254
|
+
let numbering = sequentialNumbering;
|
|
255
|
+
function setNumbering(next) {
|
|
256
|
+
numbering = next;
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Owned state
|
|
261
|
+
|
|
262
|
+
**A capability owns its state: built in `setup()`, started in `boot()`,
|
|
263
|
+
stopped in `shutdown()`. No module-level registry, and no module state
|
|
264
|
+
filled after construction.**
|
|
265
|
+
|
|
266
|
+
- A private memo cache that nothing fills after construction is fine, and so
|
|
267
|
+
is a private concurrency guard over a process-wide resource, with a comment
|
|
268
|
+
saying so.
|
|
269
|
+
- No late-bound closures between two tokens of one feature: the consumer
|
|
270
|
+
requires the other token.
|
|
271
|
+
|
|
272
|
+
Why: module state is shared by every runtime in the process and outlives
|
|
273
|
+
`shutdown()`.
|
|
274
|
+
|
|
275
|
+
```js
|
|
276
|
+
// good
|
|
277
|
+
setup() {
|
|
278
|
+
return { jobs: createJobQueue() };
|
|
279
|
+
},
|
|
280
|
+
boot({ jobs }) {
|
|
281
|
+
jobs.start();
|
|
282
|
+
},
|
|
283
|
+
|
|
284
|
+
// bad
|
|
285
|
+
const handlers = new Map();
|
|
286
|
+
export function registerHandler(name, handler) {
|
|
287
|
+
handlers.set(name, handler);
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Shutdown mirrors setup
|
|
292
|
+
|
|
293
|
+
**`shutdown()` takes the same early returns as `setup()` and releases exactly
|
|
294
|
+
what `setup()` registered, with no `?.()` or `?? []` guard.**
|
|
295
|
+
|
|
296
|
+
- When a `register()` returns its release, keep it and call it at shutdown.
|
|
297
|
+
|
|
298
|
+
Why: a guarded release hides a setup that never ran, or one that ran twice.
|
|
299
|
+
|
|
300
|
+
```js
|
|
301
|
+
// good
|
|
302
|
+
setup({ events }) {
|
|
303
|
+
if (!config.remindersEnabled) return {};
|
|
304
|
+
return { reminders: createReminders({ events }) };
|
|
305
|
+
},
|
|
306
|
+
shutdown({ reminders }) {
|
|
307
|
+
if (!config.remindersEnabled) return;
|
|
308
|
+
return reminders.stop();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// bad
|
|
312
|
+
shutdown({ reminders }) {
|
|
313
|
+
return reminders?.stop?.();
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### Loggers
|
|
318
|
+
|
|
319
|
+
**The logger is required, typed `RuntimeLogger`, and called pino-style:
|
|
320
|
+
`logger.warn({ error, ...fields }, "Sentence.")`.**
|
|
321
|
+
|
|
322
|
+
- Only where a real caller may omit it does it default to the kernel's
|
|
323
|
+
`silentLogger` (`@jskit-ai/kernel/server/runtime/logger`). Never `logger =
|
|
324
|
+
null` with `if (!logger) return`, and never `logger = console`.
|
|
325
|
+
- The foundations' logger options serialize `error` like `err`, so the message
|
|
326
|
+
and stack are kept.
|
|
327
|
+
|
|
328
|
+
Why: one shape keeps every log line searchable by its fields.
|
|
329
|
+
|
|
330
|
+
```js
|
|
331
|
+
// good
|
|
332
|
+
logger.warn({ error, invoiceId }, "The reminder failed; the next run retries it.");
|
|
333
|
+
|
|
334
|
+
// bad
|
|
335
|
+
logger?.warn?.(`Reminder failed: ${error.message}`);
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## Actions
|
|
339
|
+
|
|
340
|
+
### Action creators
|
|
341
|
+
|
|
342
|
+
**A feature's actions come from one `create<Name>Actions({ <capability> })`,
|
|
343
|
+
destructured, in `<name>Actions.js`. Each definition is a frozen literal
|
|
344
|
+
inside it, and so is the list.**
|
|
345
|
+
|
|
346
|
+
- Each id is a string literal or an `ACTION_*` module constant, exported for
|
|
347
|
+
the routes that run it.
|
|
348
|
+
- Actions of another concern live in a file named after that concern.
|
|
349
|
+
- The capability keeps the provider's name (`invoices`, not
|
|
350
|
+
`invoicesService`); only a second version of one capability gets a role name
|
|
351
|
+
(`unauditedInvoices`).
|
|
352
|
+
|
|
353
|
+
Why: a reader finds every action, and the method it reaches, in one place.
|
|
354
|
+
|
|
355
|
+
```js
|
|
356
|
+
// good
|
|
357
|
+
const ACTION_LIST_INVOICES = "invoices.list";
|
|
358
|
+
|
|
359
|
+
/** @param {{ invoices: import("./types.js").Invoices }} dependencies */
|
|
360
|
+
function createInvoiceActions({ invoices }) {
|
|
361
|
+
return Object.freeze([
|
|
362
|
+
Object.freeze({
|
|
363
|
+
...QUERY_ACTION,
|
|
364
|
+
id: ACTION_LIST_INVOICES,
|
|
365
|
+
input: listInvoicesInput,
|
|
366
|
+
execute: (input, context) => invoices.list(input, { context })
|
|
367
|
+
})
|
|
368
|
+
]);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export { ACTION_LIST_INVOICES, createInvoiceActions };
|
|
372
|
+
|
|
373
|
+
// bad: positional, and built by a helper
|
|
374
|
+
function createInvoiceActions(invoices) {
|
|
375
|
+
return [queryAction("invoices.list", invoices.list)];
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### One-line execute
|
|
380
|
+
|
|
381
|
+
**`execute: (input, context) => service.method(…)` makes exactly one call:
|
|
382
|
+
no `async` or `await`, no body, no envelope, no helper that builds or strips
|
|
383
|
+
it, and no `input || {}`.**
|
|
384
|
+
|
|
385
|
+
- The service returns the final result, its JSON:API document included.
|
|
386
|
+
- A key that must not reach the service is destructured in the parameters:
|
|
387
|
+
`execute: ({ workspaceSlug, ...changes }, context) => invoices.update(changes, { context })`.
|
|
388
|
+
- Flags go into the input or the context, never into a third argument.
|
|
389
|
+
- A CRUD custom action keeps its declaration's own
|
|
390
|
+
`execute: ({ input, context, service }) => service.method(…)`, and its map
|
|
391
|
+
key names it; never write an id equal to the default crud-core builds.
|
|
392
|
+
|
|
393
|
+
Why: each action shows the one method it reaches.
|
|
394
|
+
|
|
395
|
+
```js
|
|
396
|
+
// good
|
|
397
|
+
execute: (input, context) => invoices.send(input, { context })
|
|
398
|
+
|
|
399
|
+
// bad
|
|
400
|
+
execute: async (input = {}, context) => {
|
|
401
|
+
const invoice = await invoices.send(input || {}, { context });
|
|
402
|
+
return { data: invoice };
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### Shared action fields
|
|
407
|
+
|
|
408
|
+
**Fields many actions share (`version`, `kind`, `channels`, `surfaces`) come
|
|
409
|
+
from one frozen constant per kind, spread first into each literal. The other
|
|
410
|
+
keys follow in this order: `id, input, output, permission, events, execute`.**
|
|
411
|
+
|
|
412
|
+
- An application keeps one query and one command constant in its shared
|
|
413
|
+
package; a package defines its own only when its values differ.
|
|
414
|
+
- An empty `events` is left out.
|
|
415
|
+
|
|
416
|
+
Why: each literal reads complete, and one edit changes every action's
|
|
417
|
+
channels.
|
|
418
|
+
|
|
419
|
+
```js
|
|
420
|
+
// good
|
|
421
|
+
const QUERY_ACTION = Object.freeze({
|
|
422
|
+
version: 1,
|
|
423
|
+
kind: "query",
|
|
424
|
+
channels: ["api", "automation", "internal"],
|
|
425
|
+
surfaces: ["app"]
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// bad: half the fields in the feature's actionDefaults, half in each literal
|
|
429
|
+
defineFeature({ actionDefaults: { channels: ["api"] }, … });
|
|
430
|
+
Object.freeze({ id: ACTION_LIST_INVOICES, kind: "query", … });
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
### Strict action inputs
|
|
434
|
+
|
|
435
|
+
**An action's input is validated in create mode, the default: `required` is
|
|
436
|
+
enforced and defaults are applied. Only an action that updates part of a
|
|
437
|
+
record declares `mode: "patch"`; a command never does.**
|
|
438
|
+
|
|
439
|
+
- Patch mode skips `required`, so a service keeps its guard for a field a
|
|
440
|
+
patch input lists as required.
|
|
441
|
+
|
|
442
|
+
Why: a command whose required field is never enforced reaches the service
|
|
443
|
+
without it.
|
|
444
|
+
|
|
445
|
+
```js
|
|
446
|
+
// good
|
|
447
|
+
input: { schema: sendInvoiceSchema } // a command: invoiceId is enforced
|
|
448
|
+
input: { schema: invoiceChangesSchema, mode: "patch" } // a partial update
|
|
449
|
+
|
|
450
|
+
// bad: a command in patch mode, so its required invoiceId is never enforced
|
|
451
|
+
input: { schema: sendInvoiceSchema, mode: "patch" }
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### No idempotency
|
|
455
|
+
|
|
456
|
+
**An action declares no idempotency; the kernel refuses the field at startup.
|
|
457
|
+
An action that must tolerate a repeated request handles it in its own domain
|
|
458
|
+
(a unique key, an upsert, a state check) and says so in a comment where that
|
|
459
|
+
is not obvious.**
|
|
460
|
+
|
|
461
|
+
Why: a label nothing enforces promises a guarantee nobody keeps.
|
|
462
|
+
|
|
463
|
+
```js
|
|
464
|
+
// good: the service returns an invoice that is already sent unchanged
|
|
465
|
+
execute: (input, context) => invoices.send(input, { context })
|
|
466
|
+
|
|
467
|
+
// bad
|
|
468
|
+
idempotency: "optional",
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
### The action policy
|
|
472
|
+
|
|
473
|
+
**A rule every action of an application obeys (who may run it, the record it
|
|
474
|
+
acts within, the scope it runs in) is data in each definition's
|
|
475
|
+
`extensions`, applied by the application's one `runtime.actions.policy`.
|
|
476
|
+
Never a helper that wraps every definition.**
|
|
477
|
+
|
|
478
|
+
- `input(definition)` runs once, when the action is registered, and returns
|
|
479
|
+
the input schema the action is validated and listed with.
|
|
480
|
+
- `execute({ definition, input, context, run })` runs on every execution,
|
|
481
|
+
after the kernel's channel, surface, permission and input checks, and calls
|
|
482
|
+
`run(input, context)` with what the action should receive. It can refuse,
|
|
483
|
+
adjust the input or context, or run the action inside a scope.
|
|
484
|
+
- The policy comes from a provider (a feature requires `runtime.actions`,
|
|
485
|
+
which would close a cycle), and passes every action whose `extensions` it
|
|
486
|
+
does not recognise straight to `run`.
|
|
487
|
+
|
|
488
|
+
Why: a wrapper makes every definition's `execute` the wrapper's, so neither a
|
|
489
|
+
reader nor the code map sees the call it makes.
|
|
490
|
+
|
|
491
|
+
```js
|
|
492
|
+
// good
|
|
493
|
+
Object.freeze({ ...COMMAND_ACTION, id: ACTION_UPDATE_NOTE, extensions: { records: OWNER_ONLY }, execute: (input) => notes.update(input) })
|
|
494
|
+
const RecordScopePolicyProvider = defineProvider({
|
|
495
|
+
id: "app.action-policy",
|
|
496
|
+
provides: {
|
|
497
|
+
policy: "runtime.actions.policy"
|
|
498
|
+
},
|
|
499
|
+
setup: () => ({ policy: recordScopePolicy })
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
// bad
|
|
503
|
+
withRecordScope({ id: ACTION_UPDATE_NOTE, …, execute: (input) => notes.update(input) }, { ownerOnly: true })
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
### Request context
|
|
507
|
+
|
|
508
|
+
**A package reads the request, the actor and the workspace from an action
|
|
509
|
+
context one way, through the kernel's readers, and its actions pass the
|
|
510
|
+
context one way.**
|
|
511
|
+
|
|
512
|
+
- `resolveRequest(context)`, `resolveActor(context)` and
|
|
513
|
+
`resolveActorId(context)`, from
|
|
514
|
+
`@jskit-ai/kernel/server/actions/actionContributorHelpers`.
|
|
515
|
+
- The workspace comes from the context only. A record's workspace may be
|
|
516
|
+
checked against it, never used as a fallback.
|
|
517
|
+
- `?.` and `|| null` variants count as different forms.
|
|
518
|
+
|
|
519
|
+
Why: each form treats an absent actor differently, so one action behaves
|
|
520
|
+
differently depending on its path.
|
|
521
|
+
|
|
522
|
+
```js
|
|
523
|
+
// good
|
|
524
|
+
const actorId = resolveActorId(context);
|
|
525
|
+
|
|
526
|
+
// bad
|
|
527
|
+
const actorId = context?.actor?.id || options.context?.user?.id || null;
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
## Services and repositories
|
|
531
|
+
|
|
532
|
+
### Validated input as is
|
|
533
|
+
|
|
534
|
+
**A method that receives an action's validated input takes it as it is: no
|
|
535
|
+
`input = {}`, no `input?.x` and no second parse of what the schema checked.**
|
|
536
|
+
|
|
537
|
+
- Destructure with the schema's absent value: `{ name, note = "" }`.
|
|
538
|
+
- A contract operation other callers reach drops `= {}` and `?.` too, since
|
|
539
|
+
the contract types its input, and still validates what it receives.
|
|
540
|
+
|
|
541
|
+
Why: two validators drift apart, and the service seems to accept values the
|
|
542
|
+
action refuses.
|
|
543
|
+
|
|
544
|
+
```js
|
|
545
|
+
// good
|
|
546
|
+
async function rename({ invoiceId, name }, { context }) {
|
|
547
|
+
|
|
548
|
+
// bad
|
|
549
|
+
async function rename(input = {}, options = {}) {
|
|
550
|
+
const name = String(input?.name || "").trim();
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
### Options and defaults
|
|
554
|
+
|
|
555
|
+
**Resolve each default once, at the feature or the public entry point; below
|
|
556
|
+
it, destructure and use.**
|
|
557
|
+
|
|
558
|
+
- An options bag whose fields are optional by contract keeps `options = {}`,
|
|
559
|
+
with no `?.` or second `|| {}` on it. Pass it on as `options`, or
|
|
560
|
+
`{ ...options, trx }` inside a transaction.
|
|
561
|
+
- A parameter or bag no caller passes is dropped and its values written in
|
|
562
|
+
place. A helper's argument takes no default unless a real caller omits it,
|
|
563
|
+
and a default never comes before a required parameter.
|
|
564
|
+
- Two behaviours are two methods: no boolean mode parameter and no `skip*`
|
|
565
|
+
flag. A construction-time setting, and a boolean that is column data
|
|
566
|
+
(`setArchived({ archived })` behind `archive` and `restore`), are fine.
|
|
567
|
+
|
|
568
|
+
Why: a value with two defaults has two meanings, and a mode flag hides a
|
|
569
|
+
second method inside the first.
|
|
570
|
+
|
|
571
|
+
```js
|
|
572
|
+
// good
|
|
573
|
+
async function list(query, options = {}) {
|
|
574
|
+
return resolveRepoClient(knex, options)("invoices").where(query);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// bad
|
|
578
|
+
async function list(query = {}, options = {}) {
|
|
579
|
+
const client = options?.trx || knex;
|
|
580
|
+
}
|
|
581
|
+
async function save(invoice, skipAudit = false) {
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
### Frozen results
|
|
585
|
+
|
|
586
|
+
**Every `create*` returns `Object.freeze({ … })` of named inner functions.
|
|
587
|
+
Action definitions and their lists are frozen too.**
|
|
588
|
+
|
|
589
|
+
Why: a caller cannot replace a member, and each member has a name the code
|
|
590
|
+
map shows.
|
|
591
|
+
|
|
592
|
+
```js
|
|
593
|
+
// good
|
|
594
|
+
function createInvoices({ knex }) {
|
|
595
|
+
async function list(query) { … }
|
|
596
|
+
async function send(input, options) { … }
|
|
597
|
+
return Object.freeze({
|
|
598
|
+
list,
|
|
599
|
+
send
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// bad
|
|
604
|
+
return { list(query) { … }, send: async (input) => { … } };
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
### Named members
|
|
608
|
+
|
|
609
|
+
**A factory exposes another factory's functions by name, never by spreading
|
|
610
|
+
its result.**
|
|
611
|
+
|
|
612
|
+
- Spreading is for a `decorateService` or `decorateRepository` over the JSKIT
|
|
613
|
+
CRUD service or repository, whose members are the contract's, and for data.
|
|
614
|
+
|
|
615
|
+
Why: a spread hides where a member comes from, and a later spread silently
|
|
616
|
+
replaces an earlier member.
|
|
617
|
+
|
|
618
|
+
```js
|
|
619
|
+
// good
|
|
620
|
+
return Object.freeze({
|
|
621
|
+
list: reads.list,
|
|
622
|
+
send: writes.send
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
// bad
|
|
626
|
+
return Object.freeze({ ...createInvoiceReads(dependencies), ...createInvoiceWrites(dependencies) });
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
### One helper per package
|
|
630
|
+
|
|
631
|
+
**Each pure helper exists once per package and is imported. A helper the
|
|
632
|
+
kernel owns is imported from it. A non-pure check two packages make the same
|
|
633
|
+
way (is this user an operator) is a member of its owner's capability.**
|
|
634
|
+
|
|
635
|
+
- The kernel's `@jskit-ai/kernel/shared/support/normalize` holds
|
|
636
|
+
`normalizeText` (every falsy value is empty), `toText` (only `null` and
|
|
637
|
+
`undefined` are), `isRecord` and their siblings; check each call site's
|
|
638
|
+
meaning of 0 and false before replacing a copy.
|
|
639
|
+
- A helper two packages of an application use lives in its shared package,
|
|
640
|
+
never inside one of them.
|
|
641
|
+
|
|
642
|
+
Why: copies drift, and the next reader cannot tell which one is right.
|
|
643
|
+
|
|
644
|
+
```js
|
|
645
|
+
// good
|
|
646
|
+
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
647
|
+
|
|
648
|
+
// bad: a third copy, with its own idea of empty
|
|
649
|
+
function normalizeText(value) {
|
|
650
|
+
return String(value || "").trim();
|
|
651
|
+
}
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
### One reader per fact
|
|
655
|
+
|
|
656
|
+
**A fact that a call's options or context carry is read through its owner's
|
|
657
|
+
helper, never by hand.**
|
|
658
|
+
|
|
659
|
+
| Fact | Reader |
|
|
660
|
+
| --- | --- |
|
|
661
|
+
| the call's transaction, or the client to query through | `resolveTransaction(options)`, `resolveRepoClient(knex, options)` (`@jskit-ai/database-runtime/shared`) |
|
|
662
|
+
| what a service hands its repository | `repositoryCallOptions(options)` (`@jskit-ai/crud-core/server/jsonApiService`), only at the repository call |
|
|
663
|
+
| the signed-in user | `resolveActor(context)`, `resolveActorId(context)` |
|
|
664
|
+
| a tagged result's records, or its document | `simplifyJsonApiResult(result)`, `jsonApiResultValue(result)` (`@jskit-ai/http-runtime/shared`) |
|
|
665
|
+
| a collection's next-page cursor | `jsonApiNextCursor(document)` (`@jskit-ai/http-runtime/shared`) |
|
|
666
|
+
| a created record's id | `resolveCrudJsonApiEntityIdFromResult(result)` (`@jskit-ai/crud-core/server/serviceEvents`) |
|
|
667
|
+
| a CRUD record change's event | `buildCrudRecordChangedEvent({ namespace, operation, entityId, audience })` (`@jskit-ai/crud-core/server/serviceEvents`) |
|
|
668
|
+
| the workspace a request asks for, before any action context | `requestedWorkspaceScope(request)` (`@jskit-ai/workspaces-core/server/support/workspaceRouteInput`) |
|
|
669
|
+
| a context's workspace id | `resolveWorkspaceScopeValue(context)` (`@jskit-ai/json-rest-api-core/server/jsonRestApiHost`) |
|
|
670
|
+
|
|
671
|
+
Why: each hand-written copy treats a missing value its own way.
|
|
672
|
+
|
|
673
|
+
```js
|
|
674
|
+
// good
|
|
675
|
+
const trx = resolveTransaction(options);
|
|
676
|
+
|
|
677
|
+
// bad
|
|
678
|
+
const trx = options?.trx || options?.transaction || null;
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
### Routes
|
|
682
|
+
|
|
683
|
+
**A route does HTTP work only (status, cookies, redirects) and runs an action
|
|
684
|
+
for anything that reads or writes the domain.**
|
|
685
|
+
|
|
686
|
+
- Registration lives in `registerRoutes.js`, or in `<concern>Routes.js`
|
|
687
|
+
exporting `register<Concern>Routes` when there are several.
|
|
688
|
+
- One handler shape: a named async function that sends what
|
|
689
|
+
`request.executeAction({ actionId: ACTION_X, input })` returns.
|
|
690
|
+
- A route that bypasses actions (a provider's callback) gets its own named
|
|
691
|
+
registrar. Summaries are sentences.
|
|
692
|
+
|
|
693
|
+
Why: an action is the one place a domain operation's policy, input and
|
|
694
|
+
events are declared.
|
|
695
|
+
|
|
696
|
+
```js
|
|
697
|
+
// good
|
|
698
|
+
router.register("POST", "/api/invoices/:invoiceId/send", { auth: "required", meta: { summary: "Sends an invoice." } },
|
|
699
|
+
async function sendInvoice(request, reply) {
|
|
700
|
+
reply.code(200).send(await request.executeAction({ actionId: ACTION_SEND_INVOICE, input: request.input.params }));
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
// bad: the route reads storage itself
|
|
704
|
+
router.register("GET", "/api/invoices", {}, async (request, reply) => reply.send(await knex("invoices")));
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
## Errors
|
|
708
|
+
|
|
709
|
+
### Refusals carry their own code
|
|
710
|
+
|
|
711
|
+
**A refusal is `new AppError(status, message, { code, params })`, or a
|
|
712
|
+
subclass of `AppError`, with a code of its own: one code per distinct
|
|
713
|
+
sentence, and `params` for the values in it.**
|
|
714
|
+
|
|
715
|
+
- One code convention per package (`billing.invoice.notFound`); codes are
|
|
716
|
+
stable and never numbered.
|
|
717
|
+
- Where callers branch on a shared code, keep it and add a `messageCode` (and
|
|
718
|
+
`messageParams`) per sentence. Input validation that carries per-field
|
|
719
|
+
`details` may share one code.
|
|
720
|
+
- An error class extends `AppError` and gives each refusal its own code. One
|
|
721
|
+
that takes extra fields destructures them and forwards the rest to `super`
|
|
722
|
+
(`constructor(message, { approval, ...options })`), so each construction
|
|
723
|
+
passes its own code. A project's coded builder, such as
|
|
724
|
+
`billingError(message, code)`, is fine when every call passes its own code. A function converting a caught error is
|
|
725
|
+
`to<What>Error(error, …)`; one returning refusal text is `*Message`.
|
|
726
|
+
- The kernel's `NotFoundError`, `ConflictError` and `DomainError` default to
|
|
727
|
+
one shared code, so each call passes its own.
|
|
728
|
+
|
|
729
|
+
Why: a client tells refusals apart by their code and shows each in the
|
|
730
|
+
user's language ([translation](translation.md)).
|
|
731
|
+
|
|
732
|
+
```js
|
|
733
|
+
// good
|
|
734
|
+
throw new AppError(409, "The invoice is already sent.", { code: "billing.invoice.alreadySent" });
|
|
735
|
+
throw new AppError(422, "The note is too long.", { code: "billing.note.tooLong", params: { max: 500 } });
|
|
736
|
+
|
|
737
|
+
// bad
|
|
738
|
+
throw new AppError(409, "The invoice is already sent.");
|
|
739
|
+
throw new NotFoundError("Invoice not found.");
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
### Internal failures are plain errors
|
|
743
|
+
|
|
744
|
+
**An internal invariant or an unexpected failure is `throw new Error(message)`.
|
|
745
|
+
The caller sees "Internal server error." and the message stays in the log.**
|
|
746
|
+
|
|
747
|
+
- JSKIT's HTTP handler shows an `AppError`'s message at any status, so an
|
|
748
|
+
`AppError` at 5xx is only for a failure the caller can act on.
|
|
749
|
+
|
|
750
|
+
Why: an internal failure dressed as a refusal leaks its details and promises
|
|
751
|
+
the caller a fix it cannot make.
|
|
752
|
+
|
|
753
|
+
```js
|
|
754
|
+
// good
|
|
755
|
+
throw new Error(`Invoice ${invoiceId} has no ledger entry.`);
|
|
756
|
+
|
|
757
|
+
// bad
|
|
758
|
+
throw new AppError(500, `Invoice ${invoiceId} has no ledger entry.`, { code: "billing.ledger.missing" });
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
### One construction per error
|
|
762
|
+
|
|
763
|
+
**An error gets its status and code when it is constructed, never patched on
|
|
764
|
+
afterwards.**
|
|
765
|
+
|
|
766
|
+
Why: a patched `Error` is neither a coded refusal nor a hidden failure, and
|
|
767
|
+
the next edit drops the patch.
|
|
768
|
+
|
|
769
|
+
```js
|
|
770
|
+
// good
|
|
771
|
+
throw new AppError(404, "Invoice was not found.", { code: "billing.invoice.notFound" });
|
|
772
|
+
|
|
773
|
+
// bad
|
|
774
|
+
throw Object.assign(new Error("Invoice was not found."), { statusCode: 404 });
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
### Failures are logged, not swallowed
|
|
778
|
+
|
|
779
|
+
**A best-effort `.catch` around a collaborator call logs the error and
|
|
780
|
+
carries on.**
|
|
781
|
+
|
|
782
|
+
- A silent catch stays only for cleanup on platform objects
|
|
783
|
+
(`response.body.cancel()`), for queue tails whose error reaches the queued
|
|
784
|
+
caller, and for client best effort, each with a short comment saying so.
|
|
785
|
+
|
|
786
|
+
Why: `() => {}` hides a wiring `TypeError` along with the failure.
|
|
787
|
+
|
|
788
|
+
```js
|
|
789
|
+
// good
|
|
790
|
+
publisher.publish(event).catch((error) => logger.warn({ error, eventId: event.id }, "Publishing failed; the record is saved."));
|
|
791
|
+
|
|
792
|
+
// bad
|
|
793
|
+
publisher.publish(event).catch(() => {});
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
## Tests
|
|
797
|
+
|
|
798
|
+
### Doubles use documented seams
|
|
799
|
+
|
|
800
|
+
**A test double replaces an owner only through its documented seams (an
|
|
801
|
+
injected collaborator, a provider client, an option), with the real shape.
|
|
802
|
+
It never copies an internal protocol such as an id format or a stream
|
|
803
|
+
frame.**
|
|
804
|
+
|
|
805
|
+
- Complete a double with the method the code calls, returning its neutral
|
|
806
|
+
value; never shape production code around a double.
|
|
807
|
+
|
|
808
|
+
Why: a double that mirrors internals pins them, so the test breaks on a
|
|
809
|
+
refactor and passes on a real bug.
|
|
810
|
+
|
|
811
|
+
```js
|
|
812
|
+
// good
|
|
813
|
+
const invoices = createInvoices({ knex, mailer: { send: async () => ({ accepted: true }) } });
|
|
814
|
+
|
|
815
|
+
// bad: production code that exists for the double
|
|
816
|
+
if (typeof mailer.send !== "function") return;
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
### Tests are not callers
|
|
820
|
+
|
|
821
|
+
**A parameter, option or default that only tests pass, or a method only test
|
|
822
|
+
doubles lack, is no reason to keep it: drop the seam and complete the
|
|
823
|
+
double.**
|
|
824
|
+
|
|
825
|
+
- One named clock per factory (`now = Date.now`) is a platform default, not a
|
|
826
|
+
test seam.
|
|
827
|
+
|
|
828
|
+
Why: a seam kept for tests is a shape no real caller uses, and a reader
|
|
829
|
+
cannot tell it apart from one that matters.
|
|
830
|
+
|
|
831
|
+
```js
|
|
832
|
+
// good
|
|
833
|
+
function createReminders({ mailer, now = Date.now }) {
|
|
834
|
+
|
|
835
|
+
// bad
|
|
836
|
+
function createReminders({ mailer, now = Date.now, skipDelay = false, testHooks = {} }) {
|
|
837
|
+
```
|
|
838
|
+
|
|
839
|
+
### No test-only exports
|
|
840
|
+
|
|
841
|
+
**A module exports only what other production modules import; an actions
|
|
842
|
+
file exports its creator and its `ACTION_*` ids. Tests reach everything else
|
|
843
|
+
through the feature, and pure helpers through `__testables`.**
|
|
844
|
+
|
|
845
|
+
- `__testables` holds pure helpers only, never one that touches a
|
|
846
|
+
collaborator or reads request facts.
|
|
847
|
+
|
|
848
|
+
Why: a test-only export widens the module's API and invites production
|
|
849
|
+
callers.
|
|
850
|
+
|
|
851
|
+
```js
|
|
852
|
+
// good
|
|
853
|
+
const __testables = Object.freeze({ nextInvoiceNumber });
|
|
854
|
+
export { createInvoices, __testables };
|
|
855
|
+
|
|
856
|
+
// bad
|
|
857
|
+
export function resetInvoiceCacheForTests() {
|
|
858
|
+
cache.clear();
|
|
859
|
+
}
|
|
860
|
+
```
|
|
861
|
+
|
|
862
|
+
## Files and layout
|
|
863
|
+
|
|
864
|
+
### Size limits
|
|
865
|
+
|
|
866
|
+
**Files stay under 750 lines and functions under 600, in all hand-written
|
|
867
|
+
code: server, shared, client (`.vue` and `.js`) and scripts. One concern per
|
|
868
|
+
factory.**
|
|
869
|
+
|
|
870
|
+
Why: a reader holds one concern at a time; a 2,000-line closure holds ten.
|
|
871
|
+
|
|
872
|
+
```text
|
|
873
|
+
good: invoices.js (list, send), invoiceNumbering.js, invoiceReminders.js
|
|
874
|
+
bad: invoices.js, 1,900 lines: listing, sending, numbering, reminders and PDF rendering
|
|
875
|
+
```
|
|
876
|
+
|
|
877
|
+
### Data modules
|
|
878
|
+
|
|
879
|
+
**A generated or static data module (a catalogue, a lookup table) is not
|
|
880
|
+
code. It is exempt from the size limits, and its first line says it is data
|
|
881
|
+
and how it is produced.**
|
|
882
|
+
|
|
883
|
+
Why: the limits measure code a person reads; a table is scanned, not read.
|
|
884
|
+
|
|
885
|
+
```js
|
|
886
|
+
// good
|
|
887
|
+
/* eslint-disable max-lines -- data: the country list, generated by scripts/build-countries.mjs */
|
|
888
|
+
|
|
889
|
+
// bad: a module of functions excused as data
|
|
890
|
+
/* eslint-disable max-lines -- data: invoice helpers */
|
|
891
|
+
```
|
|
892
|
+
|
|
893
|
+
### Formatter
|
|
894
|
+
|
|
895
|
+
**An application has one formatter config, `formatterConfig(globs)`: width
|
|
896
|
+
140, semicolons, double quotes, no trailing commas, one property per line in
|
|
897
|
+
an object that spans lines, braces once an `if` spans lines.** `eslint --fix`
|
|
898
|
+
applies it.
|
|
899
|
+
|
|
900
|
+
- A returned frozen result and a `requires`, `optional` or `provides` map put
|
|
901
|
+
each property on its own line. A short leaf literal of at most three scalar
|
|
902
|
+
properties (`{ type: "string", required: true }`) may stay on one line.
|
|
903
|
+
- Long prose stays inline in its string or template; never split a sentence
|
|
904
|
+
across `+` to fit the width.
|
|
905
|
+
|
|
906
|
+
Why: one layout lets a diff show only what changed.
|
|
907
|
+
|
|
908
|
+
```js
|
|
909
|
+
// good
|
|
910
|
+
return Object.freeze({
|
|
911
|
+
list,
|
|
912
|
+
send
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
// bad
|
|
916
|
+
return Object.freeze({ list, send });
|
|
917
|
+
```
|
|
918
|
+
|
|
919
|
+
### Names
|
|
920
|
+
|
|
921
|
+
**Factories and action creators are `create*`; a pure value builder is
|
|
922
|
+
`build*` or `to*`; a domain operation that creates a thing keeps `create*`
|
|
923
|
+
(`createInvoice`). Files are named for what they hold: `<Name>Feature.js`,
|
|
924
|
+
`<Name>Provider.js`, `<name>Runtime.js`, `<name>Actions.js`,
|
|
925
|
+
`<name>Routes.js`, `types.js`.**
|
|
926
|
+
|
|
927
|
+
- Sibling factories are named alike (`createInvoiceReads`,
|
|
928
|
+
`createInvoiceWrites`). An error builder keeps its `*Error` suffix.
|
|
929
|
+
|
|
930
|
+
Why: a name says what a call returns before anyone opens the file.
|
|
931
|
+
|
|
932
|
+
```js
|
|
933
|
+
// good
|
|
934
|
+
function buildInvoiceTotals(lines) {
|
|
935
|
+
|
|
936
|
+
// bad: a pure builder named like a factory
|
|
937
|
+
function createInvoiceTotals(lines) {
|
|
938
|
+
```
|
|
939
|
+
|
|
940
|
+
### Comments
|
|
941
|
+
|
|
942
|
+
**A function's description is a `/** … */` block, with the description
|
|
943
|
+
inside it. Comments describe the code, never its history or its tests.**
|
|
944
|
+
|
|
945
|
+
- A single-factory file puts its description in the factory's JSDoc, not in a
|
|
946
|
+
`//` file header.
|
|
947
|
+
- No "was", "now" or "until X is converted", no dead probes, and no shim left
|
|
948
|
+
for a removed option.
|
|
949
|
+
|
|
950
|
+
Why: a comment that narrates history is wrong the day after it is written.
|
|
951
|
+
|
|
952
|
+
```js
|
|
953
|
+
// good
|
|
954
|
+
/**
|
|
955
|
+
* Sends an invoice; a repeated send returns it unchanged.
|
|
956
|
+
* @param {{ invoiceId: string }} input
|
|
957
|
+
*/
|
|
958
|
+
|
|
959
|
+
// bad
|
|
960
|
+
// Sends an invoice (moved here from billing.js)
|
|
961
|
+
/** @param {{ invoiceId: string }} input */
|
|
962
|
+
```
|
|
963
|
+
|
|
964
|
+
## Checks
|
|
965
|
+
|
|
966
|
+
### Running the checks
|
|
967
|
+
|
|
968
|
+
`npm run lint` runs ESLint with `@jskit-ai/config-eslint`, and `npm run
|
|
969
|
+
verify` runs `jskit check` (the package graph), lint, the tests and the
|
|
970
|
+
build. The foundations' `eslint.config.mjs` already holds a new
|
|
971
|
+
application's code to every check:
|
|
972
|
+
|
|
973
|
+
```js
|
|
974
|
+
import {
|
|
975
|
+
baseConfig,
|
|
976
|
+
clientFeaturesConfig,
|
|
977
|
+
formatterConfig,
|
|
978
|
+
nodeConfig,
|
|
979
|
+
sizeLimitsConfig,
|
|
980
|
+
typedFeaturesConfig,
|
|
981
|
+
vueConfig,
|
|
982
|
+
webConfig
|
|
983
|
+
} from "@jskit-ai/config-eslint/server";
|
|
984
|
+
|
|
985
|
+
const SERVER_SOURCES = ["packages/*/src/server/**", "packages/*/src/shared/**", "server/**", "server.js"];
|
|
986
|
+
const CLIENT_SOURCES = ["packages/*/src/client/**", "src/**"];
|
|
987
|
+
const HAND_WRITTEN_SOURCES = ["packages/*/src/**", "src/**", "config/**", "server/**", "server.js", "bin/**"];
|
|
988
|
+
|
|
989
|
+
export default [
|
|
990
|
+
...baseConfig,
|
|
991
|
+
...typedFeaturesConfig(SERVER_SOURCES),
|
|
992
|
+
...sizeLimitsConfig(HAND_WRITTEN_SOURCES),
|
|
993
|
+
...vueConfig,
|
|
994
|
+
...clientFeaturesConfig(CLIENT_SOURCES),
|
|
995
|
+
...webConfig,
|
|
996
|
+
...nodeConfig,
|
|
997
|
+
...formatterConfig([...SERVER_SOURCES, "config/**", "bin/**"])
|
|
998
|
+
];
|
|
999
|
+
```
|
|
1000
|
+
|
|
1001
|
+
- `typedFeaturesConfig(globs)` makes every `jskit/*` rule and the size limits
|
|
1002
|
+
errors for the server and shared code the globs select (tests and client
|
|
1003
|
+
directories excluded).
|
|
1004
|
+
- `sizeLimitsConfig(globs)` holds the rest of the hand-written code, client
|
|
1005
|
+
code included, to the size limits.
|
|
1006
|
+
- `formatterConfig(globs)` is the formatter.
|
|
1007
|
+
- `baseConfig` reports the `jskit/*` rules as warnings on any
|
|
1008
|
+
`**/src/server/**`, so an existing application sees them before it lists a
|
|
1009
|
+
package; it lists each package as it converts it.
|
|
1010
|
+
- `clientFeaturesConfig(globs)` holds client code to the same rules; see
|
|
1011
|
+
[client features](client-features.md#running-the-checks).
|
|
1012
|
+
|
|
1013
|
+
Every finding ends with a link to the section of this page that explains it.
|
|
1014
|
+
|
|
1015
|
+
### Lint rules
|
|
1016
|
+
|
|
1017
|
+
| Rule | Reports | Section |
|
|
1018
|
+
| --- | --- | --- |
|
|
1019
|
+
| `jskit/typed-dependencies` | a factory or constructor that receives collaborators without a JSDoc `@param` type | [Typed collaborators](#typed-collaborators) |
|
|
1020
|
+
| `jskit/required-collaborators` | an absent default on a collaborator, a `typeof … === "function"` probe, an optional call (`x.y?.()`, `received?.()`, `receivedService?.y()`) | [Required collaborators](#required-collaborators) |
|
|
1021
|
+
| `jskit/no-dual-shape` | a value accepted in two shapes, `x.service \|\| x` or `x?.value ?? x` | [One shape per value](#one-shape-per-value) |
|
|
1022
|
+
| `jskit/no-env-default` | `env = process.env`, `env \|\| process.env`, `env ?? process.env` | [Environment from the caller](#environment-from-the-caller) |
|
|
1023
|
+
| `jskit/no-slot-setter` | a `set<Name>()` that fills a slot after construction | [Optional tokens](#optional-tokens) |
|
|
1024
|
+
| `jskit/no-module-registry` | a module-level `register<Name>()` that writes module state | [Owned state](#owned-state) |
|
|
1025
|
+
| `jskit/named-repeated-type` | a `ReturnType<…>` or `X["y"]` JSDoc type written in two or more files of a package | [Capability types](#capability-types) |
|
|
1026
|
+
| `jskit/action-creator` | an action definition outside a `create<Name>Actions` creator, or a creator that takes its capabilities positionally | [Action creators](#action-creators) |
|
|
1027
|
+
| `jskit/action-execute-shape` | an `execute` that is not one arrow making one call: a body, `execute: helper(…)`, a built result | [One-line execute](#one-line-execute) |
|
|
1028
|
+
| `jskit/one-line-action` | `input = {}`, `input \|\| {}` or `input?.` in an action, an `async` `execute`, an unfrozen definition or list | [One-line execute](#one-line-execute) |
|
|
1029
|
+
| `jskit/no-idempotency` | an `idempotency` field on an action definition or its shared-field constant | [No idempotency](#no-idempotency) |
|
|
1030
|
+
| `jskit/no-optional-after-default` | `options?.` or a second `options \|\| {}` on a parameter that has a default | [Options and defaults](#options-and-defaults) |
|
|
1031
|
+
| `jskit/frozen-factory-result` | a `create*` result that is not frozen, or a member defined inline in it | [Frozen results](#frozen-results) |
|
|
1032
|
+
| `jskit/no-factory-spread` | a `create*()` result spread into a factory's API or a token value | [Named members](#named-members) |
|
|
1033
|
+
| `jskit/no-duplicate-helper` | a small helper, function or arrow, repeated in another file of the package | [One helper per package](#one-helper-per-package) |
|
|
1034
|
+
| `jskit/app-error-code` | an `AppError`, `DomainError`, `ConflictError` or `NotFoundError` (or a subclass the module declares) without its own code, and one code naming two sentences in a module | [Refusals carry their own code](#refusals-carry-their-own-code) |
|
|
1035
|
+
| `jskit/no-patched-error` | `Object.assign(new Error(…), { statusCode })`, or an error builder's result given its status afterwards | [One construction per error](#one-construction-per-error) |
|
|
1036
|
+
| `jskit/no-test-only-export` | an export whose name says only tests use it (`…ForTests`, `__reset…`; `__testables` aside) | [No test-only exports](#no-test-only-exports) |
|
|
1037
|
+
| `jskit/one-property-per-line` | two properties on one line in a returned `Object.freeze({ … })` or a `requires` / `optional` / `provides` map (`eslint --fix` splits them) | [Formatter](#formatter) |
|
|
1038
|
+
| `jskit/jsdoc-description-inside` | a `//` description directly above a `/** */` block | [Comments](#comments) |
|
|
1039
|
+
| `jskit/allow-comment-form` | a `readable-check: allow` reason without a prefix from the table below, or one that cites tests or the state of a project | [Exceptions](#exceptions) |
|
|
1040
|
+
| `max-lines`, `max-lines-per-function` | files over 750 lines, functions over 600 | [Size limits](#size-limits) |
|
|
1041
|
+
|
|
1042
|
+
The rules see shapes, not intent. Feature files, owned state beyond
|
|
1043
|
+
registries, shutdown, loggers, shared action fields, strict inputs, the action
|
|
1044
|
+
policy, request context, validated input, one reader per fact, routes,
|
|
1045
|
+
internal failures, swallowed failures, test doubles, tests as callers, data
|
|
1046
|
+
modules and names are checked by reading.
|
|
1047
|
+
|
|
1048
|
+
### Exceptions
|
|
1049
|
+
|
|
1050
|
+
A justified exception carries a comment on the line before it, naming the
|
|
1051
|
+
rule or the finding, with a reason that says what is true of the code, never
|
|
1052
|
+
the state of a project or its tests:
|
|
1053
|
+
|
|
1054
|
+
```js
|
|
1055
|
+
// readable-check: allow method-probe - input validation: an application supplies the serializer builder
|
|
1056
|
+
if (typeof buildSerializer !== "function") {
|
|
1057
|
+
```
|
|
1058
|
+
|
|
1059
|
+
| Reason prefix | The exception |
|
|
1060
|
+
| --- | --- |
|
|
1061
|
+
| `input validation:` | a public library entry point checks a value an application passes it, and refuses a wrong one with its own error |
|
|
1062
|
+
| `data:` | probes or defaults data (a route definition, a transport, a parsed record, an options list), not a collaborator; it never excuses a function itself, such as a repeated helper |
|
|
1063
|
+
| `platform object:` | probes a host or library object reached internally, such as `Headers`, a fetch `Response` or a Knex transaction (`platform object: a Knex transaction`) |
|
|
1064
|
+
| `optional operation:` | calls a contract method that some real implementer lacks, which the contract marks optional |
|
|
1065
|
+
| `safety invariant: <what goes wrong>` | keeps a guard that protects an internal invariant, saying what goes wrong without it; the retired `test asserts error:` is reported |
|
|
1066
|
+
| `not a slot:` | matched a name that fills no slot |
|
|
1067
|
+
| `cycle: <supplier> requires <owner>` | keeps a setter because the slot's supplier requires its owner |
|
|
1068
|
+
| `deferred until boot: <reason>` | keeps a capability handed out at setup and filled in `boot()`, such as the deferred `auth.service`: a two-phase value that is neither a cycle nor a setter |
|
|
1069
|
+
| `filled by <repository>; convert with its supplier` | keeps a setter that another repository's code calls |
|
|
1070
|
+
| `cross-repository slot <token>` | keeps a probe for a token another repository supplies |
|
|
1071
|
+
| `shared resource:` | keeps client module state that every instance on the page shares on purpose ([client features](client-features.md#module-state)) |
|
|
1072
|
+
|
|
1073
|
+
One kind of probe gets one prefix and one wording everywhere, and a bare
|
|
1074
|
+
`typeof x` guard of that kind carries the same comment even where no rule
|
|
1075
|
+
reports it. The size limits are ESLint's own rules: excuse them with
|
|
1076
|
+
`// eslint-disable-next-line max-lines-per-function -- <reason>`, or a data
|
|
1077
|
+
module's header.
|
|
1078
|
+
|
|
1079
|
+
## A whole feature
|
|
1080
|
+
|
|
1081
|
+
The `server/feature-package` pattern is a complete feature that passes every
|
|
1082
|
+
check. Its layout:
|
|
1083
|
+
|
|
1084
|
+
```text
|
|
1085
|
+
packages/booking-engine/
|
|
1086
|
+
package.json exports ./server/types; lists the capability it provides
|
|
1087
|
+
src/server/
|
|
1088
|
+
types.js BookingEngine, BookingEngineAvailability
|
|
1089
|
+
BookingEngineFeature.js optional slot, provided engine, setup, actions
|
|
1090
|
+
bookingEngine.js createBookingEngine({ availability }): frozen named functions
|
|
1091
|
+
bookingAvailability.js everyScopeOpen, the slot's named default
|
|
1092
|
+
bookingEngineActions.js BOOKING_QUERY and createBookingEngineActions({ bookingEngine })
|
|
1093
|
+
inputSchemas.js the actions' input validators
|
|
1094
|
+
```
|
|
1095
|
+
|
|
1096
|
+
Following a call takes one step each time: a route names
|
|
1097
|
+
`ACTION_GET_STATUS`; that action's `execute` calls `bookingEngine.readStatus`;
|
|
1098
|
+
`BookingEngine` is `ReturnType<typeof createBookingEngine>`, so the call
|
|
1099
|
+
reaches `bookingEngine.js`.
|