@nilejs/cli 1.0.0 → 1.0.2
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 +15 -5
- package/dist/index.js +36 -38
- package/package.json +5 -3
- package/template/README.md +21 -11
- package/template/SKILL.md +343 -0
- package/template/package.json +8 -6
- package/template/src/db/models/tasks.ts +2 -2
- package/template/src/index.ts +31 -6
- package/template/src/services/services.config.ts +3 -20
- package/template/src/services/tasks/index.ts +18 -0
- package/template/tsconfig.json +17 -5
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ The starter includes:
|
|
|
20
20
|
|
|
21
21
|
- `src/index.ts`: server entry point on Bun's native HTTP server, with a logger and an `onBoot` schema push
|
|
22
22
|
- `src/db/`: PGlite and Drizzle client, the `tasks` table and a `taskModel` built with `createModel`
|
|
23
|
-
- `src/services/`: a `tasks` service with create, list, get, update and delete actions
|
|
23
|
+
- `src/services/`: an explicit registry and domain entry point for a `tasks` service with create, list, get, update and delete actions
|
|
24
24
|
- `Dockerfile`, `docker-compose.yml` and `docker-compose.dev.yml` for container deployments
|
|
25
25
|
- `drizzle.config.ts`, `.env.example`, `tsconfig.json` and `package.json`
|
|
26
26
|
|
|
@@ -48,7 +48,7 @@ Creates:
|
|
|
48
48
|
```text
|
|
49
49
|
src/services/users/
|
|
50
50
|
actions/sample.ts Sample action with a schema, an input type and a named handler
|
|
51
|
-
index.ts
|
|
51
|
+
index.ts Registers the service and its public actions
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
The CLI then asks whether to register the service in `src/services/services.config.ts`. If you decline, it prints the lines to add yourself.
|
|
@@ -85,7 +85,7 @@ export const getUserAction = createAction<GetUserInput>({
|
|
|
85
85
|
});
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
-
Kebab-case names become camelCase variables and PascalCase types. Register the action by importing it in `src/services/
|
|
88
|
+
Kebab-case names become camelCase variables and PascalCase types. Register the action by importing it in `src/services/users/index.ts` and adding it to the service’s `actions` list. The root registry imports services, not individual actions.
|
|
89
89
|
|
|
90
90
|
### Schemas and types
|
|
91
91
|
|
|
@@ -96,14 +96,24 @@ bun run gen schema
|
|
|
96
96
|
Reads every action's validation schema from `src/services/services.config.ts` and writes:
|
|
97
97
|
|
|
98
98
|
- `src/generated/schemas.ts`: named Zod schemas, such as `tasksCreateSchema`
|
|
99
|
-
- `src/generated/types.ts`:
|
|
99
|
+
- `src/generated/types.ts`: input types, such as `TasksCreatePayload`
|
|
100
100
|
|
|
101
101
|
| Flag | Description |
|
|
102
102
|
|---|---|
|
|
103
103
|
| `-e, --entry <path>` | Path to the services config (detected by default) |
|
|
104
104
|
| `-o, --output <path>` | Output directory (default: `src/generated`) |
|
|
105
105
|
|
|
106
|
-
Actions without a validation schema
|
|
106
|
+
Every registered action is included in `ServicePayloads`. Actions without a validation schema accept an object payload (`Record<string, unknown>`), so actions such as `tasks.list` can be called with `{}`. This fallback does not add server-side validation. Schemas that cannot be converted are reported separately. Share the generated files with a frontend to validate forms with the same rules as the server.
|
|
107
|
+
|
|
108
|
+
## Current starter
|
|
109
|
+
|
|
110
|
+
CLI 1.0.2 targets Nile `^1.0.4`, Zod 4, TypeScript 7 and the Drizzle model helpers. The generated layout passes Nile’s strict boot checks. Database setup returns a Result and prevents startup on failure; shutdown drains Nile’s work before closing PGlite. Helpers are available as `utils.createModel`, and Result helpers come from `@nilejs/nile/fp`.
|
|
111
|
+
|
|
112
|
+
Existing apps are not rewritten when the CLI updates. For an older starter, add a service entry point at `src/services/<domain>/index.ts`, register its actions there, and import that service from `services.config.ts`. [Nile’s migration notes](https://github.com/nile-js/nile/blob/main/packages/nile/docs/release-migration.md) describe the current error and startup behavior.
|
|
113
|
+
|
|
114
|
+
## Coding assistants
|
|
115
|
+
|
|
116
|
+
The starter includes `SKILL.md` with the current Nile action pattern, service layout, Results, access policies and client calls. Ask your assistant to read it before changing the backend. After updating Nile, use the version shipped at `node_modules/@nilejs/nile/SKILL.md`.
|
|
107
117
|
|
|
108
118
|
## Requirements
|
|
109
119
|
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { resolve as resolve6 } from "node:path";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
// package.json
|
|
7
|
-
var version = "1.0.
|
|
7
|
+
var version = "1.0.2";
|
|
8
8
|
|
|
9
9
|
// src/commands/generate-action.ts
|
|
10
10
|
import { resolve } from "node:path";
|
|
@@ -141,8 +141,8 @@ var generateActionCommand = async (serviceName, actionName, options = {}) => {
|
|
|
141
141
|
spinner.stop(`Action created at src/services/${serviceName}/${relative}`);
|
|
142
142
|
const camel = toCamelCase(actionName);
|
|
143
143
|
console.log("");
|
|
144
|
-
hint("
|
|
145
|
-
hint(` import { ${camel}Action } from "
|
|
144
|
+
hint("Import the action in your service index.ts and add it to actions:");
|
|
145
|
+
hint(` import { ${camel}Action } from "./actions/${actionName}";`);
|
|
146
146
|
outro();
|
|
147
147
|
};
|
|
148
148
|
|
|
@@ -186,12 +186,14 @@ var inputPrompt = (question) => {
|
|
|
186
186
|
// src/utils/schema-codegen.ts
|
|
187
187
|
import { jsonSchemaToZod } from "json-schema-to-zod";
|
|
188
188
|
var toSchemaExportName = (service, action) => {
|
|
189
|
-
const
|
|
189
|
+
const camelAction = toCamelCase2(action);
|
|
190
|
+
const pascalAction = camelAction.charAt(0).toUpperCase() + camelAction.slice(1);
|
|
190
191
|
return `${toCamelCase2(service)}${pascalAction}Schema`;
|
|
191
192
|
};
|
|
192
193
|
var toTypeExportName = (service, action) => {
|
|
193
194
|
const pascalService = toCamelCase2(service).charAt(0).toUpperCase() + toCamelCase2(service).slice(1);
|
|
194
|
-
const
|
|
195
|
+
const camelAction = toCamelCase2(action);
|
|
196
|
+
const pascalAction = camelAction.charAt(0).toUpperCase() + camelAction.slice(1);
|
|
195
197
|
return `${pascalService}${pascalAction}Payload`;
|
|
196
198
|
};
|
|
197
199
|
var toCamelCase2 = (str) => str.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
|
|
@@ -208,7 +210,7 @@ var buildSchemaEntries = (extraction) => {
|
|
|
208
210
|
const entries = [];
|
|
209
211
|
for (const [serviceName, actions] of Object.entries(extraction.schemas)) {
|
|
210
212
|
for (const [actionName, jsonSchema] of Object.entries(actions)) {
|
|
211
|
-
const zodCode = jsonSchemaToZod(jsonSchema, {
|
|
213
|
+
const zodCode = jsonSchema === null ? "z.record(z.string(), z.unknown())" : jsonSchemaToZod(jsonSchema, {
|
|
212
214
|
noImport: true
|
|
213
215
|
});
|
|
214
216
|
entries.push({
|
|
@@ -235,7 +237,7 @@ var generateSchemasFile = (extraction) => {
|
|
|
235
237
|
lines.push("");
|
|
236
238
|
}
|
|
237
239
|
if (extraction.skipped.length > 0) {
|
|
238
|
-
lines.push(`// Skipped (
|
|
240
|
+
lines.push(`// Skipped (schema conversion failed): ${extraction.skipped.join(", ")}`);
|
|
239
241
|
lines.push("");
|
|
240
242
|
}
|
|
241
243
|
return lines.join(`
|
|
@@ -245,7 +247,7 @@ var generateTypesFile = (extraction) => {
|
|
|
245
247
|
const entries = buildSchemaEntries(extraction);
|
|
246
248
|
if (entries.length === 0) {
|
|
247
249
|
return `${generateHeader()}
|
|
248
|
-
|
|
250
|
+
export type ServicePayloads = Record<never, never>;
|
|
249
251
|
`;
|
|
250
252
|
}
|
|
251
253
|
const schemaImports = entries.map((e) => e.exportName);
|
|
@@ -258,7 +260,7 @@ var generateTypesFile = (extraction) => {
|
|
|
258
260
|
""
|
|
259
261
|
];
|
|
260
262
|
for (const entry of entries) {
|
|
261
|
-
lines.push(`export type ${entry.typeName} = z.
|
|
263
|
+
lines.push(`export type ${entry.typeName} = z.input<typeof ${entry.exportName}>;`);
|
|
262
264
|
}
|
|
263
265
|
lines.push("");
|
|
264
266
|
lines.push("/** Map of all services and their action payloads for @nilejs/client */");
|
|
@@ -300,12 +302,12 @@ for (const service of services) {
|
|
|
300
302
|
|
|
301
303
|
for (const action of service.actions) {
|
|
302
304
|
if (!action.validation) {
|
|
303
|
-
|
|
305
|
+
serviceSchemas[action.name] = null;
|
|
304
306
|
continue;
|
|
305
307
|
}
|
|
306
308
|
|
|
307
309
|
try {
|
|
308
|
-
const jsonSchema = z.toJSONSchema(action.validation, { unrepresentable: "any" });
|
|
310
|
+
const jsonSchema = z.toJSONSchema(action.validation, { unrepresentable: "any", io: "input" });
|
|
309
311
|
serviceSchemas[action.name] = jsonSchema;
|
|
310
312
|
} catch {
|
|
311
313
|
skipped.push(\`\${service.name}.\${action.name}\`);
|
|
@@ -417,15 +419,8 @@ var generateSchemaCommand = async (options) => {
|
|
|
417
419
|
error(err instanceof Error ? err.message : String(err));
|
|
418
420
|
process.exit(1);
|
|
419
421
|
}
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
spinner.stop("No action schemas found");
|
|
423
|
-
warn("None of the actions have validation schemas defined.");
|
|
424
|
-
hint("Add a Zod validation schema to your actions to generate types.");
|
|
425
|
-
outro();
|
|
426
|
-
return;
|
|
427
|
-
}
|
|
428
|
-
spinner.stop(`Extracted ${schemaCount} action schema${schemaCount > 1 ? "s" : ""}`);
|
|
422
|
+
const actionCount = Object.values(extraction.schemas).reduce((sum, actions) => sum + Object.keys(actions).length, 0);
|
|
423
|
+
spinner.stop(`Read ${actionCount} action contract${actionCount === 1 ? "" : "s"}`);
|
|
429
424
|
const outputDir = resolve3(projectRoot, options.output ?? DEFAULT_OUTPUT_DIR);
|
|
430
425
|
await ensureDir(outputDir);
|
|
431
426
|
const schemasContent = generateSchemasFile(extraction);
|
|
@@ -438,7 +433,7 @@ var generateSchemaCommand = async (options) => {
|
|
|
438
433
|
success(`Generated ${typesPath.replace(projectRoot, "").replace(LEADING_SLASH2, "")}`);
|
|
439
434
|
if (extraction.skipped.length > 0) {
|
|
440
435
|
console.log("");
|
|
441
|
-
|
|
436
|
+
warn(`Could not convert validation schemas for ${extraction.skipped.length} action(s):`);
|
|
442
437
|
hint(extraction.skipped.join(", "));
|
|
443
438
|
}
|
|
444
439
|
outro();
|
|
@@ -478,17 +473,21 @@ export const sample${pascal}Action = createAction<Sample${pascal}Input>({
|
|
|
478
473
|
};
|
|
479
474
|
var generateBarrelContent = (serviceName) => {
|
|
480
475
|
const pascal = toPascalCase2(serviceName);
|
|
481
|
-
|
|
476
|
+
const camel = toCamelCase3(serviceName);
|
|
477
|
+
return `import { createService } from "@nilejs/nile";
|
|
478
|
+
import { sample${pascal}Action } from "./actions/sample";
|
|
479
|
+
|
|
480
|
+
export const ${camel}Service = createService({
|
|
481
|
+
name: "${serviceName}",
|
|
482
|
+
description: "${pascal} service",
|
|
483
|
+
actions: [sample${pascal}Action],
|
|
484
|
+
});
|
|
482
485
|
`;
|
|
483
486
|
};
|
|
484
487
|
var generateConfigSnippet = (serviceName) => {
|
|
485
|
-
const
|
|
486
|
-
const importLine = `import {
|
|
487
|
-
const serviceEntry = ` {
|
|
488
|
-
name: "${serviceName}",
|
|
489
|
-
description: "${pascal} service",
|
|
490
|
-
actions: [sample${pascal}Action],
|
|
491
|
-
},`;
|
|
488
|
+
const camel = toCamelCase3(serviceName);
|
|
489
|
+
const importLine = `import { ${camel}Service } from "./${serviceName}";`;
|
|
490
|
+
const serviceEntry = ` ${camel}Service,`;
|
|
492
491
|
return `${importLine}
|
|
493
492
|
|
|
494
493
|
// Add to services array:
|
|
@@ -497,8 +496,8 @@ ${serviceEntry}`;
|
|
|
497
496
|
var autoRegisterService = async (configPath, serviceName) => {
|
|
498
497
|
try {
|
|
499
498
|
let content = await readFileContent(configPath);
|
|
500
|
-
const
|
|
501
|
-
const importLine = `import {
|
|
499
|
+
const camel = toCamelCase3(serviceName);
|
|
500
|
+
const importLine = `import { ${camel}Service } from "./${serviceName}";`;
|
|
502
501
|
const importLines = content.split(`
|
|
503
502
|
`).filter((line) => line.startsWith("import "));
|
|
504
503
|
if (importLines.length === 0) {
|
|
@@ -510,19 +509,18 @@ var autoRegisterService = async (configPath, serviceName) => {
|
|
|
510
509
|
}
|
|
511
510
|
content = content.replace(lastImport, `${lastImport}
|
|
512
511
|
${importLine}`);
|
|
513
|
-
const serviceEntry = ` {
|
|
514
|
-
name: "${serviceName}",
|
|
515
|
-
description: "${pascal} service",
|
|
516
|
-
actions: [sample${pascal}Action],
|
|
517
|
-
},`;
|
|
512
|
+
const serviceEntry = ` ${camel}Service,`;
|
|
518
513
|
const wrappedClosingIndex = content.lastIndexOf("]);");
|
|
519
514
|
const plainClosingIndex = content.lastIndexOf("];");
|
|
520
515
|
const closingIndex = Math.max(wrappedClosingIndex, plainClosingIndex);
|
|
521
516
|
if (closingIndex === -1) {
|
|
522
517
|
return false;
|
|
523
518
|
}
|
|
524
|
-
|
|
525
|
-
|
|
519
|
+
const prefix = content.slice(0, closingIndex).trimEnd();
|
|
520
|
+
const separator = prefix.endsWith("[") || prefix.endsWith(",") ? "" : ",";
|
|
521
|
+
content = `${prefix}${separator}
|
|
522
|
+
${serviceEntry}
|
|
523
|
+
${content.slice(closingIndex)}`;
|
|
526
524
|
await writeFileSafe(configPath, content);
|
|
527
525
|
return true;
|
|
528
526
|
} catch {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nilejs/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "CLI for scaffolding and generating Nile backend projects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,12 +17,14 @@
|
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"@types/node": "^22.8.1",
|
|
20
|
-
"typescript": "^5"
|
|
20
|
+
"typescript": "^5",
|
|
21
|
+
"zod": "4.6.7"
|
|
21
22
|
},
|
|
22
23
|
"scripts": {
|
|
23
24
|
"build": "bun run scripts/build.ts",
|
|
24
25
|
"dev": "bun run src/index.ts",
|
|
25
26
|
"typecheck": "tsc --noEmit",
|
|
26
|
-
"export": "bun run typecheck && bun run build && node dist/index.js --version"
|
|
27
|
+
"export": "bun run typecheck && bun run build && node dist/index.js --version",
|
|
28
|
+
"test": "bun test scripts/schema-contracts.spec.ts"
|
|
27
29
|
}
|
|
28
30
|
}
|
package/template/README.md
CHANGED
|
@@ -4,6 +4,8 @@ Built with [Nile Js](https://nile-js.github.io/nile/). Read the [getting started
|
|
|
4
4
|
|
|
5
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
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.
|
|
8
|
+
|
|
7
9
|
## Setup
|
|
8
10
|
|
|
9
11
|
```bash
|
|
@@ -41,17 +43,19 @@ src/
|
|
|
41
43
|
index.ts # Barrel export
|
|
42
44
|
services/
|
|
43
45
|
services.config.ts # Service registry
|
|
44
|
-
tasks/
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
tasks/
|
|
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
|
|
50
54
|
```
|
|
51
55
|
|
|
52
56
|
## Usage
|
|
53
57
|
|
|
54
|
-
|
|
58
|
+
HTTP action calls go through a single POST endpoint. The `intent` field determines the operation.
|
|
55
59
|
|
|
56
60
|
### Explore available services
|
|
57
61
|
|
|
@@ -77,6 +81,12 @@ curl -X POST http://localhost:8000/api/services \
|
|
|
77
81
|
-d '{"intent":"schema","service":"tasks","action":"*","payload":{}}'
|
|
78
82
|
```
|
|
79
83
|
|
|
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
|
+
|
|
80
90
|
## Adding services
|
|
81
91
|
|
|
82
92
|
Generate a new service with the CLI:
|
|
@@ -85,7 +95,7 @@ Generate a new service with the CLI:
|
|
|
85
95
|
bun run gen service users
|
|
86
96
|
```
|
|
87
97
|
|
|
88
|
-
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
|
|
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.
|
|
89
99
|
|
|
90
100
|
## Adding actions
|
|
91
101
|
|
|
@@ -95,7 +105,7 @@ Generate a new action in an existing service:
|
|
|
95
105
|
bun run gen action users get-user
|
|
96
106
|
```
|
|
97
107
|
|
|
98
|
-
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/
|
|
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.
|
|
99
109
|
|
|
100
110
|
## Generating schemas and types
|
|
101
111
|
|
|
@@ -108,7 +118,7 @@ bun run gen schema
|
|
|
108
118
|
This auto-detects `src/services/services.config.ts`, reads validation schemas from all actions, and outputs two files:
|
|
109
119
|
|
|
110
120
|
- `src/generated/schemas.ts`: named Zod schema exports
|
|
111
|
-
- `src/generated/types.ts`:
|
|
121
|
+
- `src/generated/types.ts`: input TypeScript types via `z.input`
|
|
112
122
|
|
|
113
123
|
Options:
|
|
114
124
|
|
|
@@ -117,7 +127,7 @@ Options:
|
|
|
117
127
|
| `-e, --entry <path>` | Path to services config (auto-detected by default) |
|
|
118
128
|
| `-o, --output <path>` | Output directory (default: `src/generated`) |
|
|
119
129
|
|
|
120
|
-
Actions without
|
|
130
|
+
Every registered action is included in `ServicePayloads`. Actions without validation accept an object payload (`Record<string, unknown>`); call `tasks.list` with `{}`. This fallback does not add server-side validation. Schemas that cannot be converted are reported separately.
|
|
121
131
|
|
|
122
132
|
## Database
|
|
123
133
|
|
|
@@ -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.
|
package/template/package.json
CHANGED
|
@@ -8,18 +8,20 @@
|
|
|
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
|
-
"zod": "^4.
|
|
17
|
+
"drizzle-orm": "^0.45.4",
|
|
18
|
+
"pino": "^10.4.0",
|
|
19
|
+
"zod": "^4.6.7",
|
|
20
|
+
"drizzle-zod": "^0.8.3"
|
|
19
21
|
},
|
|
20
22
|
"devDependencies": {
|
|
21
23
|
"@types/bun": "latest",
|
|
22
24
|
"drizzle-kit": "^0.31.10",
|
|
23
|
-
"typescript": "^
|
|
25
|
+
"typescript": "^7.0.2"
|
|
24
26
|
}
|
|
25
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" });
|
package/template/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { createLogger, createNileServer } from "@nilejs/nile";
|
|
1
|
+
import { createLogger, createNileServer, utils } from "@nilejs/nile";
|
|
2
2
|
import { sql } from "drizzle-orm";
|
|
3
3
|
import { Ok, safeTry } from "@nilejs/nile/fp";
|
|
4
|
-
import { db } from "@/db/client";
|
|
4
|
+
import { db, pglite } from "@/db/client";
|
|
5
5
|
import { services } from "@/services/services.config";
|
|
6
6
|
|
|
7
7
|
const logger = createLogger("{{projectName}}", {
|
|
@@ -25,15 +25,22 @@ const pushSchema = async () => {
|
|
|
25
25
|
);
|
|
26
26
|
|
|
27
27
|
if (result.isErr) {
|
|
28
|
-
|
|
28
|
+
return utils.handleError({
|
|
29
|
+
message: "Could not prepare the task database.",
|
|
30
|
+
data: utils.describeError(result.raw),
|
|
31
|
+
atFunction: "pushSchema",
|
|
32
|
+
logger: logger,
|
|
33
|
+
});
|
|
29
34
|
}
|
|
35
|
+
|
|
36
|
+
return Ok(null);
|
|
30
37
|
};
|
|
31
38
|
|
|
32
39
|
const server = await createNileServer({
|
|
33
40
|
serverName: "{{projectName}}",
|
|
34
41
|
runtime: "bun",
|
|
35
42
|
native: true,
|
|
36
|
-
project: { structure: "error" },
|
|
43
|
+
project: { root: process.cwd(), structure: "error" },
|
|
37
44
|
services,
|
|
38
45
|
resources: { logger, database: db },
|
|
39
46
|
rest: {
|
|
@@ -48,7 +55,10 @@ const server = await createNileServer({
|
|
|
48
55
|
// The first PGlite start on a slow machine can take longer than the 10 second default.
|
|
49
56
|
maxWaitTime: 30_000,
|
|
50
57
|
fn: async (ctx) => {
|
|
51
|
-
await pushSchema();
|
|
58
|
+
const prepared = await pushSchema();
|
|
59
|
+
if (prepared.isErr) {
|
|
60
|
+
return prepared;
|
|
61
|
+
}
|
|
52
62
|
ctx.resources?.logger?.info({
|
|
53
63
|
atFunction: "onBoot",
|
|
54
64
|
message: "{{projectName}} booted - PGLite schema ready",
|
|
@@ -72,6 +82,21 @@ server.nativeRest?.addMiddleware("/api", async (c, next) => {
|
|
|
72
82
|
|
|
73
83
|
if (server.nativeRest) {
|
|
74
84
|
const port = server.config.rest?.port ?? 8000;
|
|
75
|
-
Bun.serve({
|
|
85
|
+
const listener = Bun.serve({
|
|
86
|
+
port: port,
|
|
87
|
+
fetch: server.nativeRest.fetch,
|
|
88
|
+
});
|
|
89
|
+
let closing = false;
|
|
90
|
+
const shutdown = async () => {
|
|
91
|
+
if (closing) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
closing = true;
|
|
95
|
+
listener.stop();
|
|
96
|
+
await server.lifecycle.shutdown();
|
|
97
|
+
await pglite.close();
|
|
98
|
+
};
|
|
99
|
+
process.once("SIGINT", shutdown);
|
|
100
|
+
process.once("SIGTERM", shutdown);
|
|
76
101
|
console.log(`\n{{projectName}} listening on http://localhost:${port}\n`);
|
|
77
102
|
}
|
|
@@ -1,21 +1,4 @@
|
|
|
1
|
-
import { createServices
|
|
2
|
-
import {
|
|
3
|
-
import { deleteTaskAction } from "./tasks/actions/delete";
|
|
4
|
-
import { getTaskAction } from "./tasks/actions/get";
|
|
5
|
-
import { listTaskAction } from "./tasks/actions/list";
|
|
6
|
-
import { updateTaskAction } from "./tasks/actions/update";
|
|
1
|
+
import { createServices } from "@nilejs/nile";
|
|
2
|
+
import { tasksService } from "./tasks";
|
|
7
3
|
|
|
8
|
-
export const services
|
|
9
|
-
{
|
|
10
|
-
name: "tasks",
|
|
11
|
-
description: "Task management with CRUD operations",
|
|
12
|
-
meta: { version: "1.0.0" },
|
|
13
|
-
actions: [
|
|
14
|
-
createTaskAction,
|
|
15
|
-
listTaskAction,
|
|
16
|
-
getTaskAction,
|
|
17
|
-
updateTaskAction,
|
|
18
|
-
deleteTaskAction,
|
|
19
|
-
],
|
|
20
|
-
},
|
|
21
|
-
]);
|
|
4
|
+
export const services = createServices([tasksService]);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createService } from "@nilejs/nile";
|
|
2
|
+
import { createTaskAction } from "./actions/create";
|
|
3
|
+
import { deleteTaskAction } from "./actions/delete";
|
|
4
|
+
import { getTaskAction } from "./actions/get";
|
|
5
|
+
import { listTaskAction } from "./actions/list";
|
|
6
|
+
import { updateTaskAction } from "./actions/update";
|
|
7
|
+
|
|
8
|
+
export const tasksService = createService({
|
|
9
|
+
name: "tasks",
|
|
10
|
+
description: "Task management with CRUD operations",
|
|
11
|
+
actions: [
|
|
12
|
+
createTaskAction,
|
|
13
|
+
listTaskAction,
|
|
14
|
+
getTaskAction,
|
|
15
|
+
updateTaskAction,
|
|
16
|
+
deleteTaskAction,
|
|
17
|
+
],
|
|
18
|
+
});
|
package/template/tsconfig.json
CHANGED
|
@@ -8,11 +8,23 @@
|
|
|
8
8
|
"skipLibCheck": true,
|
|
9
9
|
"outDir": "./dist",
|
|
10
10
|
"rootDir": ".",
|
|
11
|
-
"baseUrl": ".",
|
|
12
11
|
"paths": {
|
|
13
|
-
"@/*": [
|
|
14
|
-
|
|
12
|
+
"@/*": [
|
|
13
|
+
"./src/*"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"noEmit": true,
|
|
17
|
+
"types": [
|
|
18
|
+
"bun"
|
|
19
|
+
]
|
|
15
20
|
},
|
|
16
|
-
"include": [
|
|
17
|
-
|
|
21
|
+
"include": [
|
|
22
|
+
"./src/**/*.ts",
|
|
23
|
+
"./drizzle.config.ts"
|
|
24
|
+
],
|
|
25
|
+
"exclude": [
|
|
26
|
+
"node_modules",
|
|
27
|
+
"dist",
|
|
28
|
+
"data"
|
|
29
|
+
]
|
|
18
30
|
}
|