@nilejs/cli 0.0.7 → 1.0.1
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/README.md +56 -121
- package/dist/index.js +87 -58
- package/package.json +8 -6
- package/template/Dockerfile +1 -2
- package/template/README.md +33 -21
- package/template/SKILL.md +343 -0
- package/template/docker-compose.dev.yml +0 -1
- package/template/docker-compose.yml +4 -24
- package/template/gitignore +16 -0
- package/template/package.json +9 -8
- package/template/src/db/models/tasks.ts +2 -2
- package/template/src/index.ts +40 -9
- package/template/src/services/services.config.ts +3 -20
- package/template/src/services/tasks/{create.ts → actions/create.ts} +12 -8
- package/template/src/services/tasks/actions/delete.ts +27 -0
- package/template/src/services/tasks/{get.ts → actions/get.ts} +10 -6
- package/template/src/services/tasks/{list.ts → actions/list.ts} +5 -3
- package/template/src/services/tasks/{update.ts → actions/update.ts} +15 -8
- package/template/src/services/tasks/index.ts +18 -0
- package/template/tsconfig.json +17 -5
- package/template/src/services/tasks/delete.ts +0 -23
package/template/README.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# {{projectName}}
|
|
2
2
|
|
|
3
|
-
Built with [Nile](https://github.
|
|
3
|
+
Built with [Nile Js](https://nile-js.github.io/nile/). Read the [getting started guide](https://nile-js.github.io/nile/guide/start/getting-started/) for a walkthrough of this starter.
|
|
4
|
+
|
|
5
|
+
The server runs on Bun's native HTTP server (`runtime: "bun", native: true` in `src/index.ts`). REST settings live under `rest`; custom routes and middleware are available on `server.nativeRest`.
|
|
6
|
+
|
|
7
|
+
The included `SKILL.md` gives coding assistants the action pattern, service layout and current error-handling rules. After a Nile upgrade, use `node_modules/@nilejs/nile/SKILL.md` as the current reference.
|
|
4
8
|
|
|
5
9
|
## Setup
|
|
6
10
|
|
|
@@ -10,7 +14,7 @@ cp .env.example .env
|
|
|
10
14
|
bun run dev
|
|
11
15
|
```
|
|
12
16
|
|
|
13
|
-
The server starts at `http://localhost:8000`.
|
|
17
|
+
The server starts at `http://localhost:8000`. PGlite creates an embedded Postgres database in `data/` on first start, so no database server is required. The first start can take up to 30 seconds.
|
|
14
18
|
|
|
15
19
|
## Scripts
|
|
16
20
|
|
|
@@ -24,13 +28,13 @@ The server starts at `http://localhost:8000`. PGLite creates an embedded Postgre
|
|
|
24
28
|
| `bun run db:push` | Push schema changes directly to the database |
|
|
25
29
|
| `bun run db:studio` | Open Drizzle Studio to browse your data |
|
|
26
30
|
|
|
27
|
-
## Project
|
|
31
|
+
## Project structure
|
|
28
32
|
|
|
29
33
|
```
|
|
30
34
|
src/
|
|
31
35
|
index.ts # Server entry point
|
|
32
36
|
db/
|
|
33
|
-
client.ts #
|
|
37
|
+
client.ts # PGlite + Drizzle client
|
|
34
38
|
schema.ts # Drizzle table definitions
|
|
35
39
|
types.ts # Inferred types from schema
|
|
36
40
|
index.ts # Barrel export
|
|
@@ -40,16 +44,18 @@ src/
|
|
|
40
44
|
services/
|
|
41
45
|
services.config.ts # Service registry
|
|
42
46
|
tasks/
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
index.ts # Registers the tasks service
|
|
48
|
+
actions/
|
|
49
|
+
create.ts # Create task action
|
|
50
|
+
list.ts # List tasks action
|
|
51
|
+
get.ts # Get task by ID action
|
|
52
|
+
update.ts # Update task action
|
|
53
|
+
delete.ts # Delete task action
|
|
48
54
|
```
|
|
49
55
|
|
|
50
56
|
## Usage
|
|
51
57
|
|
|
52
|
-
|
|
58
|
+
HTTP action calls go through a single POST endpoint. The `intent` field determines the operation.
|
|
53
59
|
|
|
54
60
|
### Explore available services
|
|
55
61
|
|
|
@@ -75,7 +81,13 @@ curl -X POST http://localhost:8000/api/services \
|
|
|
75
81
|
-d '{"intent":"schema","service":"tasks","action":"*","payload":{}}'
|
|
76
82
|
```
|
|
77
83
|
|
|
78
|
-
|
|
84
|
+
Responses have exactly `status`, `message` and `data`. The default success message is `Action completed.` Validation failures contain Zod’s readable message and `{ errorCode, issues }` in `data`. Unexpected failures use a generated diagnostic reference linked to the log’s `log_id`; internal `raw` causes are never sent to the frontend.
|
|
85
|
+
|
|
86
|
+
## Optional features
|
|
87
|
+
|
|
88
|
+
Keep business logic in actions and ordinary functions. Configure other features when the product needs them: [typed Socket.IO topics](https://nile-js.github.io/nile/guide/features/websockets/), [cron jobs](https://nile-js.github.io/nile/guide/features/cron-jobs/), [rate limits](https://nile-js.github.io/nile/guide/features/rate-limiting/), [queues](https://nile-js.github.io/nile/guide/features/queues-debounce-throttle/) and [telemetry](https://nile-js.github.io/nile/guide/features/telemetry/). The frontend package provides both `createNileClient` and a separate `createNileSocketClient`.
|
|
89
|
+
|
|
90
|
+
## Adding services
|
|
79
91
|
|
|
80
92
|
Generate a new service with the CLI:
|
|
81
93
|
|
|
@@ -83,9 +95,9 @@ Generate a new service with the CLI:
|
|
|
83
95
|
bun run gen service users
|
|
84
96
|
```
|
|
85
97
|
|
|
86
|
-
|
|
98
|
+
The generator offers to register the service in `src/services/services.config.ts`. To add one by hand, create a folder under `src/services/` with an `actions/` folder and create an `index.ts` that registers its actions with `createService`. Import that service into `services.config.ts`. Nile checks this layout at startup and fails on violations by default. Set `project.structure` to `"warn"` explicitly if you need to migrate an older layout gradually.
|
|
87
99
|
|
|
88
|
-
## Adding
|
|
100
|
+
## Adding actions
|
|
89
101
|
|
|
90
102
|
Generate a new action in an existing service:
|
|
91
103
|
|
|
@@ -93,9 +105,9 @@ Generate a new action in an existing service:
|
|
|
93
105
|
bun run gen action users get-user
|
|
94
106
|
```
|
|
95
107
|
|
|
96
|
-
Each action file exports
|
|
108
|
+
This creates `src/services/users/actions/get-user.ts`. Each action file exports one action created with `createAction`: a named Zod schema, an input type inferred from it and a named handler that returns `Ok(data)` or `Err(message)`. Import the action in `src/services/users/index.ts` and add it to the service's `actions` list.
|
|
97
109
|
|
|
98
|
-
## Generating
|
|
110
|
+
## Generating schemas and types
|
|
99
111
|
|
|
100
112
|
Extract Zod validation schemas from your actions and generate TypeScript types:
|
|
101
113
|
|
|
@@ -105,8 +117,8 @@ bun run gen schema
|
|
|
105
117
|
|
|
106
118
|
This auto-detects `src/services/services.config.ts`, reads validation schemas from all actions, and outputs two files:
|
|
107
119
|
|
|
108
|
-
- `src/generated/schemas.ts
|
|
109
|
-
- `src/generated/types.ts
|
|
120
|
+
- `src/generated/schemas.ts`: named Zod schema exports
|
|
121
|
+
- `src/generated/types.ts`: input TypeScript types via `z.input`
|
|
110
122
|
|
|
111
123
|
Options:
|
|
112
124
|
|
|
@@ -119,14 +131,14 @@ Actions without a validation schema are skipped and listed in the CLI output.
|
|
|
119
131
|
|
|
120
132
|
## Database
|
|
121
133
|
|
|
122
|
-
This project ships with [
|
|
134
|
+
This project ships with [PGlite](https://electric-sql.com/product/pglite) for zero-setup local development. For production, you can swap it for Postgres, MySQL, SQLite, or any other database supported by Drizzle. Update `src/db/client.ts` with your connection and driver.
|
|
123
135
|
|
|
124
136
|
See the [Drizzle getting started guide](https://orm.drizzle.team/docs/get-started) for setup instructions with each supported database.
|
|
125
137
|
|
|
126
|
-
## Tech
|
|
138
|
+
## Tech stack
|
|
127
139
|
|
|
128
140
|
- **Runtime:** [Bun](https://bun.sh)
|
|
129
|
-
- **Framework:** [
|
|
130
|
-
- **Database:** [
|
|
141
|
+
- **Framework:** [Nile Js](https://nile-js.github.io/nile/) (`@nilejs/nile`)
|
|
142
|
+
- **Database:** [PGlite](https://electric-sql.com/product/pglite) (embedded Postgres)
|
|
131
143
|
- **ORM:** [Drizzle](https://orm.drizzle.team)
|
|
132
144
|
- **Validation:** [Zod](https://zod.dev)
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nile
|
|
3
|
+
description: Work in a Nile Js backend (@nilejs/nile 1.x). Organize business operations as services and actions with Zod validation, Result returns, hooks, JWT protection and typed clients. Use when adding or changing actions, services, hooks, uploads, topics, schedules or the @nilejs/client calls in a Nile Js project.
|
|
4
|
+
license: MIT
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Working in a Nile Js application
|
|
8
|
+
|
|
9
|
+
Nile Js organizes a backend into **services** (business areas) and **actions** (one operation each). Read the project's `src/services/services.config.ts` first: it is the registry of every action the server exposes. Then read the domain’s `index.ts`, a neighboring action, its model and any global policy hooks. Follow one operation from registration to persistence before changing it.
|
|
10
|
+
|
|
11
|
+
Use Nile’s conventions to make business operations easy to find. Prefer named functions, explicit inputs and early Result branches. Choose FP helpers when they make absence, failure, transformation or async control clearer; ordinary functions and straightforward `if` statements remain useful.
|
|
12
|
+
|
|
13
|
+
Documentation: https://nile-js.github.io/nile/guide/
|
|
14
|
+
Feature pages: https://nile-js.github.io/nile/guide/features/
|
|
15
|
+
Examples: https://nile-js.github.io/nile/guide/examples/
|
|
16
|
+
Every guide page is also listed in https://nile-js.github.io/nile/llms.txt
|
|
17
|
+
|
|
18
|
+
## Project structure
|
|
19
|
+
|
|
20
|
+
Service and action entry points are the enforced core. The rest of this tree shows where supporting code goes when needed. Add optional layers when a real feature needs them; do not generate empty layers up front.
|
|
21
|
+
|
|
22
|
+
```text
|
|
23
|
+
src/
|
|
24
|
+
index.ts listener and process lifecycle
|
|
25
|
+
server.ts optional server composition
|
|
26
|
+
config.ts optional environment parsing in one place
|
|
27
|
+
services/
|
|
28
|
+
services.config.ts explicit service registry
|
|
29
|
+
projects/
|
|
30
|
+
index.ts createService and public actions
|
|
31
|
+
actions/
|
|
32
|
+
create.ts a small action
|
|
33
|
+
archive/
|
|
34
|
+
archive.ts a larger action's entry point
|
|
35
|
+
index.ts exports archiveAction
|
|
36
|
+
modules/ archive's private helpers
|
|
37
|
+
modules/ helpers shared within this domain
|
|
38
|
+
db/
|
|
39
|
+
client.ts connection and driver
|
|
40
|
+
models/ persistence operations
|
|
41
|
+
hooks/ shared action policies
|
|
42
|
+
business/ optional reusable domain functions
|
|
43
|
+
generated/ client payload/schema contracts
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Create a project with `bunx @nilejs/cli new my-app`. Start it with `bun run dev`. Generate code with `bun run gen service <name>` and `bun run gen action <service> <action>`. Generate Zod schemas and accepted payload types from validations with `bun run gen schema`. Handler types describe parsed output (`z.infer`); client payload types describe input (`z.input`), so fields with defaults can be omitted by callers.
|
|
47
|
+
|
|
48
|
+
Structure violations fail startup by default. An application needs `src/index.ts` or `src/server.ts`, `src/services/services.config.ts`, and each service’s `index.ts` and `actions/`. For a growing action, use `actions/archive/archive.ts`, an `index.ts` export and private helpers in `actions/archive/modules/`. Action entry points above 400 lines are reported for refactoring. `project: { root: ".", structure: "warn" }` reports violations without stopping; `false` disables inspection. Source-less bundles skip inspection unless a root is explicit.
|
|
49
|
+
|
|
50
|
+
## The action pattern
|
|
51
|
+
|
|
52
|
+
Every action uses a named schema, an inferred input type, a separately written named handler and `createAction<T>`:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { createAction } from "@nilejs/nile";
|
|
56
|
+
import { Err, Ok } from "@nilejs/nile/fp";
|
|
57
|
+
import { z } from "zod";
|
|
58
|
+
|
|
59
|
+
const createProjectSchema = z.object({
|
|
60
|
+
name: z.string().trim().min(3, "Name must have at least 3 characters"),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
type CreateProjectInput = z.infer<typeof createProjectSchema>;
|
|
64
|
+
|
|
65
|
+
const createProjectHandler = (input: CreateProjectInput) => {
|
|
66
|
+
if (input.name === "system") {
|
|
67
|
+
return Err("Choose a different project name");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return Ok({ project: { name: input.name } });
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const createProjectAction = createAction<CreateProjectInput>({
|
|
74
|
+
name: "create",
|
|
75
|
+
description: "Create a project",
|
|
76
|
+
validation: createProjectSchema,
|
|
77
|
+
handler: createProjectHandler,
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Register the action in its service entry point, then register the service:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
// src/services/projects/index.ts
|
|
85
|
+
import { createService } from "@nilejs/nile";
|
|
86
|
+
import { createProjectAction } from "./actions/create";
|
|
87
|
+
|
|
88
|
+
export const projectsService = createService({
|
|
89
|
+
name: "projects",
|
|
90
|
+
description: "Project operations",
|
|
91
|
+
actions: [createProjectAction],
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
// src/services/services.config.ts
|
|
97
|
+
import { createServices } from "@nilejs/nile";
|
|
98
|
+
import { projectsService } from "./projects";
|
|
99
|
+
|
|
100
|
+
export const services = createServices([projectsService]);
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Action names are unique within a service. Folder names do not create routes; only the registry does.
|
|
104
|
+
|
|
105
|
+
## Handlers take (data, context)
|
|
106
|
+
|
|
107
|
+
Nile Js calls every handler with two arguments: the validated input and the execution's `NileContext`. The type marks the context optional. Use `createAction<Input>` for the schema’s parsed output type, and import `NileContext` when annotating a named handler’s second argument. Read an optional context with `?.` or explicitly reject its absence before using it.
|
|
108
|
+
|
|
109
|
+
- `context.getSession("rest")` returns the verified JWT identity (`userId`, `organizationId` and other claims) for a protected action.
|
|
110
|
+
- `context.resources` holds what `createNileServer` received (`logger`, `database`).
|
|
111
|
+
- `context.execution` holds `requestId`, `transport`, `signal` and `clientKey`.
|
|
112
|
+
- `context.updateHookState` and `context.hookContext.state` are per execution. `context.set` and `context.get` are shared by every request in the process.
|
|
113
|
+
- `getContext<T>()` returns the context outside a handler. It throws before `createNileServer` runs.
|
|
114
|
+
|
|
115
|
+
## Results
|
|
116
|
+
|
|
117
|
+
Handlers return `Ok(value)` or `Err(message)` from `@nilejs/nile/fp`, which re-exports `slang-ts`. Check `isErr` before reading `value`. Use `safeTry` around a call that can throw or reject. Use `utils.handleError({ message, data })` with `import { utils } from "@nilejs/nile"` to log an error and return it as an `Err` in one step.
|
|
118
|
+
|
|
119
|
+
The default success message is `Action completed.` Set `successMessage` on the action, or return `Ok(value, message)` to choose one per call; the handler's message wins.
|
|
120
|
+
|
|
121
|
+
HTTP envelope: success is `{ status: true, message, data }`. Failure is `{ status: false, message, data }`; inspect the envelope rather than assuming every failure has the same HTTP status. Validation failures use Zod’s prettified message (or an explicit validation message) and put a generated unique reference in `data.errorCode` alongside field paths in `data.issues`.
|
|
122
|
+
|
|
123
|
+
Slang 0.2.2 preserves caught causes on `result.raw`. `safeTry` returns a Result directly, including when the callback returns a Result; never expect a Result inside `result.value`. Nested `Ok(Ok(...))` and `Ok(Err(...))` are rejected. Return an existing Result directly.
|
|
124
|
+
|
|
125
|
+
`dbTry` is removed. For a database promise, use `safeTry`, inspect `result.raw` internally and pass `utils.describeError(result.raw)` to `utils.handleError` with a deliberate caller-safe message. `result.error` may contain driver text and must not be forwarded blindly. Unexpected throws inside Nile-managed work are logged and become `Error Code: <unique-code> - could not complete the action`. The generated reference matches the log’s `log_id`. Never use it as a stable error category. Custom success and intentional `Err` messages retain their wording.
|
|
126
|
+
|
|
127
|
+
`raw` is reserved for internal diagnostics and removed recursively from outgoing JSON and Socket.IO messages. Choose another name for public domain fields. Do not rely on that filter to sanitize secrets stored under other names.
|
|
128
|
+
|
|
129
|
+
## Choose FP helpers for the job
|
|
130
|
+
|
|
131
|
+
Import from `@nilejs/nile/fp`, so application code and Nile share the same Slang types. Import Nile-specific helpers through `utils` from `@nilejs/nile`. Check the installed declarations before using an unfamiliar helper; do not copy APIs from a different FP library.
|
|
132
|
+
|
|
133
|
+
| Need | Use | Important boundary |
|
|
134
|
+
|---|---|---|
|
|
135
|
+
| An operation can succeed or fail | `Ok`, `Err`, `Result` | Check `isErr` before reading `value`; propagate a narrowed failure directly. |
|
|
136
|
+
| A dependency can throw or reject | `safeTry` | Await it. A caught failure still needs a safe caller message. |
|
|
137
|
+
| A value may legitimately be absent | `option`, `Option` | Absence is different from a failed lookup. |
|
|
138
|
+
| Both Result or Option branches produce a value | `match` | Supply both branches; callbacks receive the variant, not its unwrapped payload. |
|
|
139
|
+
| Several domain cases choose behavior | `matchAll` | Include the `_` fallback; keep business cases explicit. |
|
|
140
|
+
| A successful value needs a transformation | `andThen` | It propagates failure; await async transformations. Do not return `undefined` to mean removal. |
|
|
141
|
+
| Several Result-producing steps form a pipeline | `pipe` | Use the installed `.run()` API. A plain sequence of early returns is often easier to read. |
|
|
142
|
+
| Indexed slicing, shaping or set operations need explicit failure handling | `list`, `struct`, `sets`, `Ok.iterate` | Collection chains can return `Fallible`; finish with a deliberate fallback or handle the failure. |
|
|
143
|
+
| One async operation needs cancellation/deadline control | `superPromise` | It settles to a Result. Pass its signal to the dependency. |
|
|
144
|
+
| Several controlled tasks share a concurrency budget | `taskGroup`, `superRunner` | Share the group across calls; creating one per action execution does not bound total concurrency. |
|
|
145
|
+
| A pure calculation is reused with the same arguments | `memoized` | It caches by argument identity; do not cache sessions, permissions or mutable database reads this way. |
|
|
146
|
+
| An in-process notification coordinates local functions | `createChannel`, `createSignal` | These are local primitives, not Nile’s schema-validated Socket.IO topics or a cross-process event bus. |
|
|
147
|
+
|
|
148
|
+
### Propagate Results instead of nesting them
|
|
149
|
+
|
|
150
|
+
Use an early failure branch when it makes the operation easier to follow. Once a Result is narrowed to `Err`, return it directly. A handler returning a Result does not need another `Ok` around that Result.
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
import { Err, Ok, type Result } from "@nilejs/nile/fp";
|
|
154
|
+
|
|
155
|
+
const calculateTotal = (quantity: number): Result<number, string> => {
|
|
156
|
+
if (quantity < 1) {
|
|
157
|
+
return Err("Choose at least one item");
|
|
158
|
+
}
|
|
159
|
+
return Ok(quantity * 100);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export const prepareQuote = (quantity: number) => {
|
|
163
|
+
const total = calculateTotal(quantity);
|
|
164
|
+
if (total.isErr) {
|
|
165
|
+
return total;
|
|
166
|
+
}
|
|
167
|
+
return Ok({ total: total.value });
|
|
168
|
+
};
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`andThen` is useful for a short transformation of a successful value. In this Slang version, callbacks may return a plain value or a Result; the returned Result is not nested. A callback returning `undefined` preserves the original value, so do not use that to remove data. Callback exceptions become failures; keep them free of secret-bearing exception messages if the Result will reach a caller.
|
|
172
|
+
|
|
173
|
+
### Model expected absence explicitly
|
|
174
|
+
|
|
175
|
+
Use an Option when “not configured yet” is a valid answer. Use an Err when the operation cannot proceed without the value. Do not silently turn a failed database call into an empty collection or a missing setting.
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
import { match, option } from "@nilejs/nile/fp";
|
|
179
|
+
|
|
180
|
+
export const getDisplayName = (name: string | null) => {
|
|
181
|
+
return match(option(name), {
|
|
182
|
+
Some: (present) => {
|
|
183
|
+
return present.value;
|
|
184
|
+
},
|
|
185
|
+
None: () => {
|
|
186
|
+
return "Unnamed project";
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
};
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Slang’s absence rules matter: `0` and `false` are present; `null`, `undefined`, empty strings and non-finite numbers are absent. Use an explicit check or a schema instead when an empty string is meaningful domain data. `.unwrap().else(fallback)` is for a deliberate fallback; `.expect()` and `.panic()` throw and should not replace normal input validation or business refusals.
|
|
193
|
+
|
|
194
|
+
### Translate dependency failures at their boundary
|
|
195
|
+
|
|
196
|
+
Use `safeTry` for a promise that can throw. Keep the original cause for bounded internal diagnostics and return an intentional message. A returned `Err` from your own code may already be safe and logged; propagate it instead of logging it repeatedly.
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
import { utils, type NileLogger } from "@nilejs/nile";
|
|
200
|
+
import { Ok, safeTry } from "@nilejs/nile/fp";
|
|
201
|
+
|
|
202
|
+
export const readProviderStatus = async (
|
|
203
|
+
url: string,
|
|
204
|
+
signal: AbortSignal,
|
|
205
|
+
logger: NileLogger
|
|
206
|
+
) => {
|
|
207
|
+
const response = await safeTry(() => fetch(url, { signal: signal }));
|
|
208
|
+
if (response.isErr) {
|
|
209
|
+
return utils.handleError({
|
|
210
|
+
message: "Could not check the provider status",
|
|
211
|
+
atFunction: "readProviderStatus",
|
|
212
|
+
data: utils.describeError(response.raw),
|
|
213
|
+
logger: logger,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
return Ok({ available: response.value.ok });
|
|
217
|
+
};
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
The same pattern applies to raw database promises. Nile’s model helpers already return Results; inspect those directly. `safeTry` can flatten a Result-returning callback, but a second wrapper is unnecessary when the operation already has the intended failure contract. Never look for another `isErr` inside `result.value`.
|
|
221
|
+
|
|
222
|
+
### Choose one owner for async policy
|
|
223
|
+
|
|
224
|
+
Use `superPromise` for a dependency operation that needs Slang’s timeout, cancellation or task-group controls. Use Nile’s execution settings and `utils.createQueue`, `utils.createRateLimiter` or `utils.retryFnWithJitter` when the policy belongs to an action or shared application resource. Avoid independently stacked retries and deadlines: they multiply attempts and obscure which operation failed.
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
import { superPromise, taskGroup } from "@nilejs/nile/fp";
|
|
228
|
+
|
|
229
|
+
const providerCalls = taskGroup("provider-status", { concurrency: 4 });
|
|
230
|
+
|
|
231
|
+
export const checkProvider = async (url: string, signal: AbortSignal) => {
|
|
232
|
+
const task = superPromise(
|
|
233
|
+
async (attemptSignal) => {
|
|
234
|
+
const response = await fetch(url, { signal: attemptSignal });
|
|
235
|
+
return { available: response.ok };
|
|
236
|
+
},
|
|
237
|
+
{ group: providerCalls, timeout: 3000 }
|
|
238
|
+
);
|
|
239
|
+
const cancel = () => {
|
|
240
|
+
task.abort();
|
|
241
|
+
};
|
|
242
|
+
if (signal.aborted) {
|
|
243
|
+
cancel();
|
|
244
|
+
} else {
|
|
245
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
return await task;
|
|
249
|
+
} finally {
|
|
250
|
+
signal.removeEventListener("abort", cancel);
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
This helper is internal: its failure still needs translation before being returned publicly. A timeout or aborted Result does not roll back completed effects. Retry only operations safe to repeat, use idempotency for writes, and keep retry count, pending work and concurrency bounded. A task group limits running attempts; it does not cap all pending work or persist jobs. Use a Nile queue with explicit capacity when you also need admission control. Long-lived resources belong in the application lifecycle so shutdown can drain or close them.
|
|
256
|
+
|
|
257
|
+
## Calling actions
|
|
258
|
+
|
|
259
|
+
- HTTP: `POST /api/services` with `{ "intent": "execute", "service": "projects", "action": "create", "payload": { ... } }`.
|
|
260
|
+
- Client (`@nilejs/client`): `createNileClient<Payloads>({ baseUrl })`, then `await nile.invoke({ service, action, payload })`. Every method returns a Result: `if (result.isErr) { ... return; }`, then read `result.value`. On failure `result.error` is the message and `result.details` holds the server's `data`. `data` is a deprecated alias; do not use it.
|
|
261
|
+
- Sockets: `createNileSocketClient<Actions, Topics>` owns its connection. Use `socket.invoke`, `socket.send` and `socket.receive`; check each Result and unsubscribe/close when finished. Declare backend topics and validate their data and scope; frontend types come from shared or generated definitions. Authorization is still a backend policy.
|
|
262
|
+
- Server code: `await server.invoke({ service, action, payload, credentials? })`. Protected actions need `credentials` with headers.
|
|
263
|
+
- Uploads: `nile.upload({ service, action, files, fields })` for `isSpecial: { contentType: "multipart/form-data" }` actions. A JSON request to such an action returns HTTP 400.
|
|
264
|
+
|
|
265
|
+
## Organize a feature as it grows
|
|
266
|
+
|
|
267
|
+
1. Find the domain in `services.config.ts` and read its `index.ts`. Name the action for the business operation, then define its input schema and named handler.
|
|
268
|
+
2. Keep a short, single-use rule in that handler. Extract ordinary domain functions when a rule is reused, complicated enough to test independently, or coordinates a transaction. Do not create a repository, class or service layer solely because a folder exists.
|
|
269
|
+
3. Put data access in the project’s model convention. `utils.createModel` and `utils.getZodSchema` are optional Drizzle helpers; a handwritten model is also valid. Pass the database or transaction explicitly where transaction ownership matters.
|
|
270
|
+
4. For a growing action, move its private helpers under its action folder’s `modules/`. Put helpers shared by the domain in the service’s `modules/`. Cross-domain rules can use `business/` when useful. Do not expose a private helper as an action just to call it.
|
|
271
|
+
5. Register the action in the domain’s `actions` list. An export alone does not make it reachable. Register a new service in the root registry.
|
|
272
|
+
6. Carry validated input into business functions and trusted identity into policy checks. Keep request identity in scoped sessions and execution state, not process-wide context values.
|
|
273
|
+
7. Verify the registered operation through `server.invoke` or a transport when changing validation, permissions or registration. Test a transaction’s rollback and idempotency when those are part of the business guarantee.
|
|
274
|
+
|
|
275
|
+
A transaction belongs to the layer that knows which writes must be atomic. Calling several actions or adding an after hook does not make the work transactional. An after hook is not a `finally` block: cleanup belongs in `finally` or the lifecycle, since after hooks do not run after every failure.
|
|
276
|
+
|
|
277
|
+
## Anti-patterns to avoid
|
|
278
|
+
|
|
279
|
+
| Anti-pattern | Prefer |
|
|
280
|
+
|---|---|
|
|
281
|
+
| `Ok(existingResult)` or checking `result.value.isErr` | One Result boundary, then an early failure branch or a short transformation. |
|
|
282
|
+
| Returning a caught driver’s `result.error` directly | Log bounded diagnostics and choose a safe business-facing message. |
|
|
283
|
+
| `.expect()`, `.panic()` or a non-null assertion to bypass an ordinary refusal | An explicit Err, Option branch or validation schema. |
|
|
284
|
+
| `unwrap().else([])` on a failed write or required lookup | Preserve the failure; default only when absence is a valid answer. |
|
|
285
|
+
| Long chains with hidden writes and logging | Named functions and explicit Result checks around side effects. |
|
|
286
|
+
| Parsing or guessing business values in a before hook | Treat payloads as untrusted; rely on the action’s parsed input for business logic. |
|
|
287
|
+
| `context.set("user", identity)` | Scoped sessions or an application-owned request scope. |
|
|
288
|
+
| Creating a queue, rate limiter or task group inside every invocation | A shared resource with an explicit owner and shutdown policy. |
|
|
289
|
+
| Unbounded `Promise.all` over user-sized inputs | A concurrency limit and bounded pending work. |
|
|
290
|
+
| Memoizing authorization or mutable account state | Fresh checks or an explicit cache with appropriate invalidation. |
|
|
291
|
+
| Treating a generated diagnostic code as `NOT_FOUND` or another stable category | Keep domain decisions in explicit data/schema contracts; use the generated code only to find a log. |
|
|
292
|
+
| Assuming optional schemas, topics or hooks authorize an operation | Enforce authorization at the backend policy boundary. |
|
|
293
|
+
|
|
294
|
+
## Protection and policy
|
|
295
|
+
|
|
296
|
+
- JWT: set `auth: { secret, method: "header" }` on the server and `isProtected: true` on the action. Both are required.
|
|
297
|
+
- `accessControl` lists role labels. Nile Js stores them but does not enforce them. Enforce them in a global `onBeforeActionHandler` that checks `action.accessControl` against the session's `roles`.
|
|
298
|
+
- Hooks: `hooks.before` and `hooks.after` run another action's handler directly. They do not run that action's JWT check, validation or hooks.
|
|
299
|
+
- Discovery is off by default. When `rest.discovery.secret` is configured, `explore` and `schema` need the matching `payload.discoverySecret`. Enabling discovery without a secret exposes metadata to callers.
|
|
300
|
+
|
|
301
|
+
## Feature guides
|
|
302
|
+
|
|
303
|
+
Read the page for the feature you change before you write code:
|
|
304
|
+
|
|
305
|
+
- [Validation and errors](https://nile-js.github.io/nile/guide/features/validation-and-errors)
|
|
306
|
+
- [Authentication and hooks](https://nile-js.github.io/nile/guide/features/authentication-and-hooks)
|
|
307
|
+
- [Context and logging](https://nile-js.github.io/nile/guide/features/context-and-logging)
|
|
308
|
+
- [Queues, debounce and throttle](https://nile-js.github.io/nile/guide/features/queues-debounce-throttle)
|
|
309
|
+
- [Telemetry](https://nile-js.github.io/nile/guide/features/telemetry)
|
|
310
|
+
- [WebSockets](https://nile-js.github.io/nile/guide/features/websockets)
|
|
311
|
+
- [Cron jobs](https://nile-js.github.io/nile/guide/features/cron-jobs)
|
|
312
|
+
- [Database models](https://nile-js.github.io/nile/guide/features/database-models)
|
|
313
|
+
- [API discovery and schemas](https://nile-js.github.io/nile/guide/features/api-discovery)
|
|
314
|
+
- [HTTP, WebSocket and direct calls](https://nile-js.github.io/nile/guide/features/calling-actions)
|
|
315
|
+
- [Retries and utilities](https://nile-js.github.io/nile/guide/features/retries-and-utilities)
|
|
316
|
+
- [Rate limiting](https://nile-js.github.io/nile/guide/features/rate-limiting)
|
|
317
|
+
- [File uploads](https://nile-js.github.io/nile/guide/features/file-uploads)
|
|
318
|
+
- [Readiness and shutdown](https://nile-js.github.io/nile/guide/features/readiness-and-shutdown)
|
|
319
|
+
|
|
320
|
+
Advanced pages cover handler context, composed hooks, execution options and testing with `server.invoke`: https://nile-js.github.io/nile/guide/advanced/
|
|
321
|
+
|
|
322
|
+
## Rules for agents
|
|
323
|
+
|
|
324
|
+
1. Read `services.config.ts`, the target service’s `index.ts` and its actions before you change anything.
|
|
325
|
+
2. Follow the existing pattern in that service. Do not add a second architecture.
|
|
326
|
+
3. Never invent an API. Check the guide, the installed `@nilejs/nile` types in `node_modules`, or the existing code. If it is not there, say so.
|
|
327
|
+
4. Use named schemas, inferred input types, named handlers, `createAction<T>` and braces on every `if`.
|
|
328
|
+
5. Start with business rules in the action’s named handler. Move reusable rules or transaction coordination into ordinary domain functions when that makes them easier to share or test. Keep persistence in models or repositories; keep transaction boundaries explicit.
|
|
329
|
+
6. Typecheck and run the relevant tests after a coherent change. Exercise the registered action through `server.invoke` or a transport when registration, policy or validation changes.
|
|
330
|
+
7. Do not commit, publish or change secrets, `.env` values or deployment settings unless the user asks.
|
|
331
|
+
|
|
332
|
+
## Common pitfalls
|
|
333
|
+
|
|
334
|
+
- `isProtected` without `auth` does nothing useful. Configure both.
|
|
335
|
+
- `accessControl` does not block anything by itself. Add a hook that checks it.
|
|
336
|
+
- A before hook sees the raw payload, before validation.
|
|
337
|
+
- A global before hook's returned value does not replace the payload. Use a local hook to transform input.
|
|
338
|
+
- A local hook does not protect the parent action.
|
|
339
|
+
- `context.set` values are shared by all requests. Never store the current user there.
|
|
340
|
+
- A rate limit or topic policy that is missing denies or does not apply. Read the feature page for the default.
|
|
341
|
+
- Multiple replicas each keep their own queues, schedules, rate-limit counters and WebSocket subscriptions.
|
|
342
|
+
- JWT failures return fixed messages such as `JWT authentication failed: token expired` or `JWT authentication failed: invalid token`. They never include the token or its claims, so do not add them back in your own logs.
|
|
343
|
+
- Import Results from `@nilejs/nile/fp`, not a separately installed `slang-ts`. Two copies of `slang-ts` produce type errors.
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
version: "3.8"
|
|
2
|
-
|
|
3
1
|
services:
|
|
4
2
|
nile:
|
|
5
3
|
build:
|
|
@@ -9,28 +7,10 @@ services:
|
|
|
9
7
|
- "8000:8000"
|
|
10
8
|
environment:
|
|
11
9
|
- NODE_ENV=production
|
|
12
|
-
- DB_HOST=postgres
|
|
13
|
-
- DB_PORT=5432
|
|
14
|
-
- DB_NAME=nile
|
|
15
|
-
- DB_USER=postgres
|
|
16
|
-
- DB_PASSWORD=postgres
|
|
17
|
-
depends_on:
|
|
18
|
-
postgres:
|
|
19
|
-
condition: service_healthy
|
|
20
|
-
|
|
21
|
-
postgres:
|
|
22
|
-
image: postgres:16-alpine
|
|
23
|
-
environment:
|
|
24
|
-
POSTGRES_DB: nile
|
|
25
|
-
POSTGRES_USER: postgres
|
|
26
|
-
POSTGRES_PASSWORD: postgres
|
|
27
10
|
volumes:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
interval: 5s
|
|
32
|
-
timeout: 5s
|
|
33
|
-
retries: 5
|
|
11
|
+
# The starter keeps its PGlite database in ./data; this volume keeps it across restarts.
|
|
12
|
+
- nile_data:/app/data
|
|
13
|
+
restart: unless-stopped
|
|
34
14
|
|
|
35
15
|
volumes:
|
|
36
|
-
|
|
16
|
+
nile_data:
|
package/template/package.json
CHANGED
|
@@ -4,23 +4,24 @@
|
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": true,
|
|
6
6
|
"scripts": {
|
|
7
|
-
"dev": "bun
|
|
7
|
+
"dev": "bun --watch src/index.ts",
|
|
8
8
|
"gen": "bunx @nilejs/cli generate",
|
|
9
9
|
"db:generate": "bunx drizzle-kit generate",
|
|
10
10
|
"db:push": "bunx drizzle-kit push",
|
|
11
|
-
"db:studio": "bunx drizzle-kit studio"
|
|
11
|
+
"db:studio": "bunx drizzle-kit studio",
|
|
12
|
+
"typecheck": "tsc --noEmit"
|
|
12
13
|
},
|
|
13
14
|
"dependencies": {
|
|
14
|
-
"@nilejs/nile": "
|
|
15
|
+
"@nilejs/nile": "^1.0.4",
|
|
15
16
|
"@electric-sql/pglite": "^0.2.5",
|
|
16
|
-
"drizzle-orm": "^0.45.
|
|
17
|
-
"pino": "^10.
|
|
18
|
-
"
|
|
19
|
-
"zod": "^
|
|
17
|
+
"drizzle-orm": "^0.45.4",
|
|
18
|
+
"pino": "^10.4.0",
|
|
19
|
+
"zod": "^4.6.7",
|
|
20
|
+
"drizzle-zod": "^0.8.3"
|
|
20
21
|
},
|
|
21
22
|
"devDependencies": {
|
|
22
23
|
"@types/bun": "latest",
|
|
23
24
|
"drizzle-kit": "^0.31.10",
|
|
24
|
-
"typescript": "^
|
|
25
|
+
"typescript": "^7.0.2"
|
|
25
26
|
}
|
|
26
27
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { utils } from "@nilejs/nile";
|
|
2
2
|
import { db } from "@/db/client";
|
|
3
3
|
import { tasks } from "@/db/schema";
|
|
4
4
|
|
|
5
5
|
/** CRUD model for tasks — auto-validates, handles errors, and supports transactions */
|
|
6
|
-
export const taskModel = createModel(tasks, { db, name: "task" });
|
|
6
|
+
export const taskModel = utils.createModel(tasks, { db, name: "task" });
|