@nilejs/cli 0.0.6 → 1.0.0
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 +51 -126
- package/dist/index.js +63 -39
- package/package.json +8 -6
- package/template/.dockerignore +12 -0
- package/template/Dockerfile +18 -0
- package/template/README.md +18 -16
- package/template/docker-compose.dev.yml +13 -0
- package/template/docker-compose.yml +16 -0
- package/template/gitignore +16 -0
- package/template/package.json +1 -2
- package/template/src/index.ts +11 -5
- package/template/src/services/services.config.ts +5 -5
- 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/delete.ts +0 -23
package/README.md
CHANGED
|
@@ -2,187 +2,112 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@nilejs/cli)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Create [Nile Js](https://nile-js.github.io/nile/) projects and generate services, actions and schemas.
|
|
6
6
|
|
|
7
|
-
##
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
bun add -g @nilejs/cli
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
npm install -g @nilejs/cli
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
Or use without installing:
|
|
7
|
+
## Create a project
|
|
18
8
|
|
|
19
9
|
```bash
|
|
20
10
|
bunx @nilejs/cli new my-app
|
|
11
|
+
cd my-app
|
|
12
|
+
bun install
|
|
13
|
+
cp .env.example .env
|
|
14
|
+
bun run dev
|
|
21
15
|
```
|
|
22
16
|
|
|
23
|
-
|
|
24
|
-
npx @nilejs/cli new my-app
|
|
25
|
-
```
|
|
17
|
+
The server starts on `http://localhost:8000`. The starter stores its data in an embedded PGlite database, so there is no database server to install. The [getting started guide](https://nile-js.github.io/nile/guide/start/getting-started/) walks through the starter step by step.
|
|
26
18
|
|
|
27
|
-
|
|
19
|
+
The starter includes:
|
|
28
20
|
|
|
29
|
-
|
|
21
|
+
- `src/index.ts`: server entry point on Bun's native HTTP server, with a logger and an `onBoot` schema push
|
|
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
|
|
24
|
+
- `Dockerfile`, `docker-compose.yml` and `docker-compose.dev.yml` for container deployments
|
|
25
|
+
- `drizzle.config.ts`, `.env.example`, `tsconfig.json` and `package.json`
|
|
30
26
|
|
|
31
|
-
|
|
27
|
+
## Generate code
|
|
32
28
|
|
|
33
|
-
|
|
34
|
-
nile new my-app
|
|
35
|
-
```
|
|
29
|
+
Run the generators from the project root. The starter's `gen` script calls the CLI for you.
|
|
36
30
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
Configuring project...
|
|
44
|
-
Project "my-app" created.
|
|
45
|
-
|
|
46
|
-
Next steps:
|
|
47
|
-
|
|
48
|
-
cd my-app
|
|
49
|
-
bun install
|
|
50
|
-
cp .env.example .env
|
|
51
|
-
bun run dev
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
The scaffolded project includes:
|
|
55
|
-
|
|
56
|
-
- `src/index.ts`, server entry with PGLite and Drizzle
|
|
57
|
-
- `src/db/`, database client, schema, types, and a `tasks` model using `createModel`
|
|
58
|
-
- `src/services/`, a `tasks` service with five CRUD actions
|
|
59
|
-
- `drizzle.config.ts`, `.env.example`, `tsconfig.json`, `package.json`
|
|
60
|
-
|
|
61
|
-
### `nile generate service <name>`
|
|
31
|
+
| Command | Description |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `bun run gen service <name>` | Add a service with a sample action |
|
|
34
|
+
| `bun run gen action <service> <name>` | Add an action to an existing service |
|
|
35
|
+
| `bun run gen action <service> <name> --folder` | Add an action in its own folder, for private helper modules |
|
|
36
|
+
| `bun run gen schema` | Generate Zod schemas and TypeScript types from your actions |
|
|
62
37
|
|
|
63
|
-
|
|
38
|
+
`generate` also has the alias `gen`, so `bunx @nilejs/cli gen service users` works outside the starter.
|
|
64
39
|
|
|
65
|
-
|
|
40
|
+
### Services
|
|
66
41
|
|
|
67
42
|
```bash
|
|
68
|
-
|
|
43
|
+
bun run gen service users
|
|
69
44
|
```
|
|
70
45
|
|
|
71
46
|
Creates:
|
|
72
47
|
|
|
73
|
-
```
|
|
48
|
+
```text
|
|
74
49
|
src/services/users/
|
|
75
|
-
sample.ts
|
|
76
|
-
index.ts
|
|
50
|
+
actions/sample.ts Sample action with a schema, an input type and a named handler
|
|
51
|
+
index.ts Exports the sample action
|
|
77
52
|
```
|
|
78
53
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
### `nile generate action <service-name> <action-name>`
|
|
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.
|
|
82
55
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
Generate a new action file in an existing service directory.
|
|
56
|
+
### Actions
|
|
86
57
|
|
|
87
58
|
```bash
|
|
88
|
-
|
|
59
|
+
bun run gen action users get-user
|
|
89
60
|
```
|
|
90
61
|
|
|
91
|
-
Creates `src/services/users/get-user.ts
|
|
62
|
+
Creates `src/services/users/actions/get-user.ts`:
|
|
92
63
|
|
|
93
|
-
```
|
|
94
|
-
import {
|
|
95
|
-
import { Ok } from "
|
|
64
|
+
```ts
|
|
65
|
+
import { createAction } from "@nilejs/nile";
|
|
66
|
+
import { Ok } from "@nilejs/nile/fp";
|
|
96
67
|
import z from "zod";
|
|
97
68
|
|
|
98
69
|
const getUserSchema = z.object({
|
|
99
|
-
//
|
|
70
|
+
// Describe the input this action accepts
|
|
100
71
|
});
|
|
101
72
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
73
|
+
type GetUserInput = z.infer<typeof getUserSchema>;
|
|
74
|
+
|
|
75
|
+
const getUserHandler = async (input: GetUserInput) => {
|
|
76
|
+
// Implement users.get-user here
|
|
77
|
+
return Ok({ result: input });
|
|
105
78
|
};
|
|
106
79
|
|
|
107
|
-
export const getUserAction
|
|
80
|
+
export const getUserAction = createAction<GetUserInput>({
|
|
108
81
|
name: "get-user",
|
|
109
82
|
description: "GetUser action for users",
|
|
110
|
-
handler: getUserHandler,
|
|
111
83
|
validation: getUserSchema,
|
|
84
|
+
handler: getUserHandler,
|
|
112
85
|
});
|
|
113
86
|
```
|
|
114
87
|
|
|
115
|
-
Kebab-case names
|
|
116
|
-
|
|
117
|
-
### `nile generate schema`
|
|
88
|
+
Kebab-case names become camelCase variables and PascalCase types. Register the action by importing it in `src/services/services.config.ts` and adding it to the service's `actions` list.
|
|
118
89
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
Extract Zod validation schemas from your action definitions and generate TypeScript files with schema exports and inferred types.
|
|
90
|
+
### Schemas and types
|
|
122
91
|
|
|
123
92
|
```bash
|
|
124
|
-
|
|
93
|
+
bun run gen schema
|
|
125
94
|
```
|
|
126
95
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
Output (default `src/generated/`):
|
|
96
|
+
Reads every action's validation schema from `src/services/services.config.ts` and writes:
|
|
130
97
|
|
|
131
|
-
- `schemas.ts
|
|
132
|
-
- `types.ts
|
|
133
|
-
|
|
134
|
-
Options:
|
|
98
|
+
- `src/generated/schemas.ts`: named Zod schemas, such as `tasksCreateSchema`
|
|
99
|
+
- `src/generated/types.ts`: inferred types, such as `TasksCreatePayload`
|
|
135
100
|
|
|
136
101
|
| Flag | Description |
|
|
137
102
|
|---|---|
|
|
138
|
-
| `-e, --entry <path>` | Path to services config (
|
|
103
|
+
| `-e, --entry <path>` | Path to the services config (detected by default) |
|
|
139
104
|
| `-o, --output <path>` | Output directory (default: `src/generated`) |
|
|
140
105
|
|
|
141
|
-
Actions without a
|
|
142
|
-
|
|
143
|
-
Requires [Bun](https://bun.sh) installed for the extraction subprocess.
|
|
144
|
-
|
|
145
|
-
### Quick Reference
|
|
146
|
-
|
|
147
|
-
| Command | Description |
|
|
148
|
-
|---|---|
|
|
149
|
-
| `nile new <name>` | Scaffold a new project |
|
|
150
|
-
| `nile gen service <name>` | Add a service with a demo action |
|
|
151
|
-
| `nile gen action <service> <name>` | Add an action to an existing service |
|
|
152
|
-
| `nile gen schema` | Generate Zod schemas and TypeScript types |
|
|
153
|
-
|
|
154
|
-
## Generated Project Structure
|
|
155
|
-
|
|
156
|
-
```
|
|
157
|
-
my-app/
|
|
158
|
-
package.json
|
|
159
|
-
tsconfig.json
|
|
160
|
-
drizzle.config.ts
|
|
161
|
-
.env.example
|
|
162
|
-
src/
|
|
163
|
-
index.ts
|
|
164
|
-
db/
|
|
165
|
-
client.ts
|
|
166
|
-
schema.ts
|
|
167
|
-
types.ts
|
|
168
|
-
index.ts
|
|
169
|
-
models/
|
|
170
|
-
tasks.ts
|
|
171
|
-
index.ts
|
|
172
|
-
services/
|
|
173
|
-
services.config.ts
|
|
174
|
-
tasks/
|
|
175
|
-
create.ts
|
|
176
|
-
list.ts
|
|
177
|
-
get.ts
|
|
178
|
-
update.ts
|
|
179
|
-
delete.ts
|
|
180
|
-
```
|
|
106
|
+
Actions without a validation schema are skipped and listed in the output. Share the generated files with a frontend to validate forms with the same rules as the server.
|
|
181
107
|
|
|
182
108
|
## Requirements
|
|
183
109
|
|
|
184
|
-
-
|
|
185
|
-
- The scaffolded project uses Bun as its runtime (`Bun.serve`, `bun run`)
|
|
110
|
+
- [Bun](https://bun.sh) 1.4.2 or later. The starter runs on Bun, and `gen schema` imports your services with Bun.
|
|
186
111
|
|
|
187
112
|
## License
|
|
188
113
|
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { resolve as resolve6 } from "node:path";
|
|
5
5
|
import { Command } from "commander";
|
|
6
|
+
// package.json
|
|
7
|
+
var version = "1.0.0";
|
|
6
8
|
|
|
7
9
|
// src/commands/generate-action.ts
|
|
8
10
|
import { resolve } from "node:path";
|
|
@@ -87,47 +89,60 @@ var toPascalCase = (str) => {
|
|
|
87
89
|
var generateActionContent = (actionName, serviceName) => {
|
|
88
90
|
const camel = toCamelCase(actionName);
|
|
89
91
|
const pascal = toPascalCase(actionName);
|
|
90
|
-
return `import {
|
|
91
|
-
import { Ok } from "
|
|
92
|
+
return `import { createAction } from "@nilejs/nile";
|
|
93
|
+
import { Ok } from "@nilejs/nile/fp";
|
|
92
94
|
import z from "zod";
|
|
93
95
|
|
|
94
96
|
const ${camel}Schema = z.object({
|
|
95
|
-
//
|
|
97
|
+
// Describe the input this action accepts
|
|
96
98
|
});
|
|
97
99
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
100
|
+
type ${pascal}Input = z.infer<typeof ${camel}Schema>;
|
|
101
|
+
|
|
102
|
+
const ${camel}Handler = async (input: ${pascal}Input) => {
|
|
103
|
+
// Implement ${serviceName}.${actionName} here
|
|
104
|
+
return Ok({ result: input });
|
|
101
105
|
};
|
|
102
106
|
|
|
103
|
-
export const ${camel}Action
|
|
107
|
+
export const ${camel}Action = createAction<${pascal}Input>({
|
|
104
108
|
name: "${actionName}",
|
|
105
109
|
description: "${pascal} action for ${serviceName}",
|
|
106
|
-
handler: ${camel}Handler,
|
|
107
110
|
validation: ${camel}Schema,
|
|
111
|
+
handler: ${camel}Handler,
|
|
108
112
|
});
|
|
109
113
|
`;
|
|
110
114
|
};
|
|
111
|
-
var generateActionCommand = async (serviceName, actionName) => {
|
|
115
|
+
var generateActionCommand = async (serviceName, actionName, options = {}) => {
|
|
112
116
|
const serviceDir = resolve(process.cwd(), "src/services", serviceName);
|
|
113
117
|
if (!pathExists(serviceDir)) {
|
|
114
118
|
error(`Service "${serviceName}" not found at src/services/${serviceName}/`);
|
|
115
119
|
hint(`Create the service first: nile generate service ${serviceName}`);
|
|
116
120
|
process.exit(1);
|
|
117
121
|
}
|
|
118
|
-
const
|
|
119
|
-
|
|
122
|
+
const relative = options.folder ? `actions/${actionName}/${actionName}.ts` : `actions/${actionName}.ts`;
|
|
123
|
+
const actionFile = resolve(serviceDir, relative);
|
|
124
|
+
const existingEntries = [
|
|
125
|
+
actionFile,
|
|
126
|
+
resolve(serviceDir, `${actionName}.ts`),
|
|
127
|
+
resolve(serviceDir, `actions/${actionName}.ts`),
|
|
128
|
+
resolve(serviceDir, `actions/${actionName}/${actionName}.ts`)
|
|
129
|
+
];
|
|
130
|
+
if (existingEntries.some(pathExists)) {
|
|
120
131
|
error(`Action file "${actionName}.ts" already exists in src/services/${serviceName}/`);
|
|
121
132
|
process.exit(1);
|
|
122
133
|
}
|
|
123
134
|
brand();
|
|
124
135
|
const spinner = createSpinner(`Creating action ${actionName}...`);
|
|
125
136
|
await writeFileSafe(actionFile, generateActionContent(actionName, serviceName));
|
|
126
|
-
|
|
137
|
+
if (options.folder) {
|
|
138
|
+
await writeFileSafe(resolve(serviceDir, `actions/${actionName}/index.ts`), `export { ${toCamelCase(actionName)}Action } from "./${actionName}";
|
|
139
|
+
`);
|
|
140
|
+
}
|
|
141
|
+
spinner.stop(`Action created at src/services/${serviceName}/${relative}`);
|
|
127
142
|
const camel = toCamelCase(actionName);
|
|
128
143
|
console.log("");
|
|
129
144
|
hint("Register the action in your service config:");
|
|
130
|
-
hint(` import { ${camel}Action } from "./${serviceName}/${actionName}";`);
|
|
145
|
+
hint(` import { ${camel}Action } from "./${serviceName}/actions/${actionName}";`);
|
|
131
146
|
outro();
|
|
132
147
|
};
|
|
133
148
|
|
|
@@ -143,15 +158,15 @@ var confirmPrompt = (question, defaultYes = true) => {
|
|
|
143
158
|
input: process.stdin,
|
|
144
159
|
output: process.stdout
|
|
145
160
|
});
|
|
146
|
-
return new Promise((
|
|
161
|
+
return new Promise((resolve) => {
|
|
147
162
|
rl.question(` ${question} ${suffix} `, (answer) => {
|
|
148
163
|
rl.close();
|
|
149
164
|
const trimmed = answer.trim().toLowerCase();
|
|
150
165
|
if (trimmed === "") {
|
|
151
|
-
|
|
166
|
+
resolve(defaultYes);
|
|
152
167
|
return;
|
|
153
168
|
}
|
|
154
|
-
|
|
169
|
+
resolve(trimmed === "y" || trimmed === "yes");
|
|
155
170
|
});
|
|
156
171
|
});
|
|
157
172
|
};
|
|
@@ -160,10 +175,10 @@ var inputPrompt = (question) => {
|
|
|
160
175
|
input: process.stdin,
|
|
161
176
|
output: process.stdout
|
|
162
177
|
});
|
|
163
|
-
return new Promise((
|
|
178
|
+
return new Promise((resolve) => {
|
|
164
179
|
rl.question(` ${question} `, (answer) => {
|
|
165
180
|
rl.close();
|
|
166
|
-
|
|
181
|
+
resolve(answer.trim());
|
|
167
182
|
});
|
|
168
183
|
});
|
|
169
184
|
};
|
|
@@ -313,7 +328,7 @@ var extractSchemas = (configPath, projectRoot) => {
|
|
|
313
328
|
const importPath = resolveImportPath(configPath, projectRoot);
|
|
314
329
|
const scriptContent = buildExtractionScript(importPath);
|
|
315
330
|
const tempScriptPath = resolve2(projectRoot, TEMP_SCRIPT_NAME);
|
|
316
|
-
return new Promise((
|
|
331
|
+
return new Promise((resolve, reject) => {
|
|
317
332
|
writeFile2(tempScriptPath, scriptContent, "utf-8").then(() => {
|
|
318
333
|
execFile("bun", ["run", TEMP_SCRIPT_NAME], { cwd: projectRoot, timeout: 30000 }, (err, stdout, stderr) => {
|
|
319
334
|
unlink(tempScriptPath).catch(() => {});
|
|
@@ -325,7 +340,7 @@ ${message}`));
|
|
|
325
340
|
}
|
|
326
341
|
try {
|
|
327
342
|
const result = JSON.parse(stdout.trim());
|
|
328
|
-
|
|
343
|
+
resolve(result);
|
|
329
344
|
} catch {
|
|
330
345
|
reject(new Error(`Failed to parse extraction output. Raw output:
|
|
331
346
|
${stdout.trim()}`));
|
|
@@ -347,12 +362,12 @@ var DEFAULT_OUTPUT_DIR = "src/generated";
|
|
|
347
362
|
var LEADING_SLASH2 = /^\//;
|
|
348
363
|
var resolveConfigPath = async (projectRoot, entryFlag) => {
|
|
349
364
|
if (entryFlag) {
|
|
350
|
-
const
|
|
351
|
-
if (!pathExists(
|
|
365
|
+
const absolute = resolve3(projectRoot, entryFlag);
|
|
366
|
+
if (!pathExists(absolute)) {
|
|
352
367
|
error(`Config file not found: ${entryFlag}`);
|
|
353
368
|
return null;
|
|
354
369
|
}
|
|
355
|
-
return
|
|
370
|
+
return absolute;
|
|
356
371
|
}
|
|
357
372
|
const detected = findServicesConfig(projectRoot);
|
|
358
373
|
if (detected) {
|
|
@@ -439,34 +454,36 @@ var toPascalCase2 = (str) => {
|
|
|
439
454
|
var generateActionContent2 = (serviceName) => {
|
|
440
455
|
const camel = toCamelCase3(serviceName);
|
|
441
456
|
const pascal = toPascalCase2(serviceName);
|
|
442
|
-
return `import {
|
|
443
|
-
import { Ok } from "
|
|
457
|
+
return `import { createAction } from "@nilejs/nile";
|
|
458
|
+
import { Ok } from "@nilejs/nile/fp";
|
|
444
459
|
import z from "zod";
|
|
445
460
|
|
|
446
461
|
const sample${pascal}Schema = z.object({
|
|
447
462
|
name: z.string().min(1, "Name is required"),
|
|
448
463
|
});
|
|
449
464
|
|
|
450
|
-
|
|
451
|
-
|
|
465
|
+
type Sample${pascal}Input = z.infer<typeof sample${pascal}Schema>;
|
|
466
|
+
|
|
467
|
+
const sample${pascal}Handler = async (input: Sample${pascal}Input) => {
|
|
468
|
+
return Ok({ ${camel}: { name: input.name } });
|
|
452
469
|
};
|
|
453
470
|
|
|
454
|
-
export const sample${pascal}Action
|
|
471
|
+
export const sample${pascal}Action = createAction<Sample${pascal}Input>({
|
|
455
472
|
name: "sample",
|
|
456
473
|
description: "Sample ${serviceName} action",
|
|
457
|
-
handler: sample${pascal}Handler,
|
|
458
474
|
validation: sample${pascal}Schema,
|
|
475
|
+
handler: sample${pascal}Handler,
|
|
459
476
|
});
|
|
460
477
|
`;
|
|
461
478
|
};
|
|
462
479
|
var generateBarrelContent = (serviceName) => {
|
|
463
480
|
const pascal = toPascalCase2(serviceName);
|
|
464
|
-
return `export { sample${pascal}Action } from "./sample";
|
|
481
|
+
return `export { sample${pascal}Action } from "./actions/sample";
|
|
465
482
|
`;
|
|
466
483
|
};
|
|
467
484
|
var generateConfigSnippet = (serviceName) => {
|
|
468
485
|
const pascal = toPascalCase2(serviceName);
|
|
469
|
-
const importLine = `import { sample${pascal}Action } from "./${serviceName}/sample";`;
|
|
486
|
+
const importLine = `import { sample${pascal}Action } from "./${serviceName}/actions/sample";`;
|
|
470
487
|
const serviceEntry = ` {
|
|
471
488
|
name: "${serviceName}",
|
|
472
489
|
description: "${pascal} service",
|
|
@@ -481,7 +498,7 @@ var autoRegisterService = async (configPath, serviceName) => {
|
|
|
481
498
|
try {
|
|
482
499
|
let content = await readFileContent(configPath);
|
|
483
500
|
const pascal = toPascalCase2(serviceName);
|
|
484
|
-
const importLine = `import { sample${pascal}Action } from "./${serviceName}/sample";`;
|
|
501
|
+
const importLine = `import { sample${pascal}Action } from "./${serviceName}/actions/sample";`;
|
|
485
502
|
const importLines = content.split(`
|
|
486
503
|
`).filter((line) => line.startsWith("import "));
|
|
487
504
|
if (importLines.length === 0) {
|
|
@@ -498,7 +515,9 @@ ${importLine}`);
|
|
|
498
515
|
description: "${pascal} service",
|
|
499
516
|
actions: [sample${pascal}Action],
|
|
500
517
|
},`;
|
|
501
|
-
const
|
|
518
|
+
const wrappedClosingIndex = content.lastIndexOf("]);");
|
|
519
|
+
const plainClosingIndex = content.lastIndexOf("];");
|
|
520
|
+
const closingIndex = Math.max(wrappedClosingIndex, plainClosingIndex);
|
|
502
521
|
if (closingIndex === -1) {
|
|
503
522
|
return false;
|
|
504
523
|
}
|
|
@@ -525,7 +544,7 @@ var generateServiceCommand = async (serviceName) => {
|
|
|
525
544
|
brand();
|
|
526
545
|
const spinner = createSpinner(`Creating service ${serviceName}...`);
|
|
527
546
|
await ensureDir(serviceDir);
|
|
528
|
-
await writeFileSafe(resolve4(serviceDir, "sample.ts"), generateActionContent2(serviceName));
|
|
547
|
+
await writeFileSafe(resolve4(serviceDir, "actions/sample.ts"), generateActionContent2(serviceName));
|
|
529
548
|
await writeFileSafe(resolve4(serviceDir, "index.ts"), generateBarrelContent(serviceName));
|
|
530
549
|
spinner.stop(`Service created at src/services/${serviceName}/`);
|
|
531
550
|
const configPath = resolve4(servicesDir, "services.config.ts");
|
|
@@ -557,6 +576,7 @@ ${generateConfigSnippet(serviceName)}
|
|
|
557
576
|
};
|
|
558
577
|
|
|
559
578
|
// src/commands/new.ts
|
|
579
|
+
import { rename } from "node:fs/promises";
|
|
560
580
|
import { resolve as resolve5 } from "node:path";
|
|
561
581
|
import { fileURLToPath } from "node:url";
|
|
562
582
|
var __dirname2 = fileURLToPath(new URL(".", import.meta.url));
|
|
@@ -581,6 +601,10 @@ var newCommand = async (projectName) => {
|
|
|
581
601
|
const spinner = createSpinner(`Creating ${projectName}...`);
|
|
582
602
|
const templateDir = resolveTemplateDir();
|
|
583
603
|
await copyDir(templateDir, targetDir);
|
|
604
|
+
const shippedGitignore = resolve5(targetDir, "gitignore");
|
|
605
|
+
if (pathExists(shippedGitignore)) {
|
|
606
|
+
await rename(shippedGitignore, resolve5(targetDir, ".gitignore"));
|
|
607
|
+
}
|
|
584
608
|
const allFiles = await getFilesRecursive(targetDir);
|
|
585
609
|
const replacements = { "{{projectName}}": projectName };
|
|
586
610
|
for (const filePath of allFiles) {
|
|
@@ -597,9 +621,9 @@ var newCommand = async (projectName) => {
|
|
|
597
621
|
hint("bun run dev");
|
|
598
622
|
console.log("");
|
|
599
623
|
success("Generate with the CLI:");
|
|
600
|
-
hint("
|
|
601
|
-
hint("
|
|
602
|
-
hint("
|
|
624
|
+
hint("bun run gen service <name> Add a new service");
|
|
625
|
+
hint("bun run gen action <service> <name> Add an action to a service");
|
|
626
|
+
hint("bun run gen schema Generate Zod schemas and types");
|
|
603
627
|
outro();
|
|
604
628
|
};
|
|
605
629
|
|
|
@@ -613,10 +637,10 @@ var ensureDepsInstalled = () => {
|
|
|
613
637
|
}
|
|
614
638
|
};
|
|
615
639
|
var program = new Command;
|
|
616
|
-
program.name("nile").description("CLI for the Nile backend framework").version(
|
|
640
|
+
program.name("nile").description("CLI for the Nile backend framework").version(version);
|
|
617
641
|
program.command("new").argument("<project-name>", "Name of the project to create").description("Scaffold a new Nile project").action(newCommand);
|
|
618
642
|
var generate = program.command("generate").alias("gen").description("Generate services and actions").hook("preAction", ensureDepsInstalled);
|
|
619
643
|
generate.command("service").argument("<name>", "Service name (kebab-case recommended)").description("Generate a new service with a demo action").action(generateServiceCommand);
|
|
620
|
-
generate.command("action").argument("<service-name>", "Name of the existing service").argument("<action-name>", "Name of the action to create").description("Generate a new action in an existing service").action(generateActionCommand);
|
|
644
|
+
generate.command("action").argument("<service-name>", "Name of the existing service").argument("<action-name>", "Name of the action to create").description("Generate a new action in an existing service").option("--folder", "Create an action folder for private modules").action(generateActionCommand);
|
|
621
645
|
generate.command("schema").option("-e, --entry <path>", "Path to services config file").option("-o, --output <path>", "Output directory for generated files").description("Generate Zod schemas and TypeScript types from action validations").action(generateSchemaCommand);
|
|
622
646
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nilejs/cli",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "CLI for scaffolding and generating Nile backend projects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,10 +10,6 @@
|
|
|
10
10
|
"dist",
|
|
11
11
|
"template"
|
|
12
12
|
],
|
|
13
|
-
"scripts": {
|
|
14
|
-
"build": "bun run scripts/build.ts",
|
|
15
|
-
"dev": "bun run src/index.ts"
|
|
16
|
-
},
|
|
17
13
|
"dependencies": {
|
|
18
14
|
"commander": "^13.1.0",
|
|
19
15
|
"json-schema-to-zod": "^2.7.0",
|
|
@@ -22,5 +18,11 @@
|
|
|
22
18
|
"devDependencies": {
|
|
23
19
|
"@types/node": "^22.8.1",
|
|
24
20
|
"typescript": "^5"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "bun run scripts/build.ts",
|
|
24
|
+
"dev": "bun run src/index.ts",
|
|
25
|
+
"typecheck": "tsc --noEmit",
|
|
26
|
+
"export": "bun run typecheck && bun run build && node dist/index.js --version"
|
|
25
27
|
}
|
|
26
|
-
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
FROM oven/bun:1-alpine
|
|
2
|
+
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
|
|
5
|
+
# Copy package files
|
|
6
|
+
COPY package.json bun.lock ./
|
|
7
|
+
|
|
8
|
+
# Install production dependencies only
|
|
9
|
+
RUN bun install --frozen-lockfile --production
|
|
10
|
+
|
|
11
|
+
# Copy source code
|
|
12
|
+
COPY . .
|
|
13
|
+
|
|
14
|
+
# Expose the port (default for Nile)
|
|
15
|
+
EXPOSE 8000
|
|
16
|
+
|
|
17
|
+
# Start the server
|
|
18
|
+
CMD ["bun", "run", "src/index.ts"]
|
package/template/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
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`.
|
|
4
6
|
|
|
5
7
|
## Setup
|
|
6
8
|
|
|
@@ -10,7 +12,7 @@ cp .env.example .env
|
|
|
10
12
|
bun run dev
|
|
11
13
|
```
|
|
12
14
|
|
|
13
|
-
The server starts at `http://localhost:8000`.
|
|
15
|
+
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
16
|
|
|
15
17
|
## Scripts
|
|
16
18
|
|
|
@@ -24,13 +26,13 @@ The server starts at `http://localhost:8000`. PGLite creates an embedded Postgre
|
|
|
24
26
|
| `bun run db:push` | Push schema changes directly to the database |
|
|
25
27
|
| `bun run db:studio` | Open Drizzle Studio to browse your data |
|
|
26
28
|
|
|
27
|
-
## Project
|
|
29
|
+
## Project structure
|
|
28
30
|
|
|
29
31
|
```
|
|
30
32
|
src/
|
|
31
33
|
index.ts # Server entry point
|
|
32
34
|
db/
|
|
33
|
-
client.ts #
|
|
35
|
+
client.ts # PGlite + Drizzle client
|
|
34
36
|
schema.ts # Drizzle table definitions
|
|
35
37
|
types.ts # Inferred types from schema
|
|
36
38
|
index.ts # Barrel export
|
|
@@ -39,7 +41,7 @@ src/
|
|
|
39
41
|
index.ts # Barrel export
|
|
40
42
|
services/
|
|
41
43
|
services.config.ts # Service registry
|
|
42
|
-
tasks/
|
|
44
|
+
tasks/actions/
|
|
43
45
|
create.ts # Create task action
|
|
44
46
|
list.ts # List tasks action
|
|
45
47
|
get.ts # Get task by ID action
|
|
@@ -75,7 +77,7 @@ curl -X POST http://localhost:8000/api/services \
|
|
|
75
77
|
-d '{"intent":"schema","service":"tasks","action":"*","payload":{}}'
|
|
76
78
|
```
|
|
77
79
|
|
|
78
|
-
## Adding
|
|
80
|
+
## Adding services
|
|
79
81
|
|
|
80
82
|
Generate a new service with the CLI:
|
|
81
83
|
|
|
@@ -83,9 +85,9 @@ Generate a new service with the CLI:
|
|
|
83
85
|
bun run gen service users
|
|
84
86
|
```
|
|
85
87
|
|
|
86
|
-
|
|
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 register its actions in `services.config.ts`.
|
|
87
89
|
|
|
88
|
-
## Adding
|
|
90
|
+
## Adding actions
|
|
89
91
|
|
|
90
92
|
Generate a new action in an existing service:
|
|
91
93
|
|
|
@@ -93,9 +95,9 @@ Generate a new action in an existing service:
|
|
|
93
95
|
bun run gen action users get-user
|
|
94
96
|
```
|
|
95
97
|
|
|
96
|
-
Each action file exports
|
|
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/services.config.ts` and add it to the service's `actions` list.
|
|
97
99
|
|
|
98
|
-
## Generating
|
|
100
|
+
## Generating schemas and types
|
|
99
101
|
|
|
100
102
|
Extract Zod validation schemas from your actions and generate TypeScript types:
|
|
101
103
|
|
|
@@ -105,8 +107,8 @@ bun run gen schema
|
|
|
105
107
|
|
|
106
108
|
This auto-detects `src/services/services.config.ts`, reads validation schemas from all actions, and outputs two files:
|
|
107
109
|
|
|
108
|
-
- `src/generated/schemas.ts
|
|
109
|
-
- `src/generated/types.ts
|
|
110
|
+
- `src/generated/schemas.ts`: named Zod schema exports
|
|
111
|
+
- `src/generated/types.ts`: inferred TypeScript types via `z.infer`
|
|
110
112
|
|
|
111
113
|
Options:
|
|
112
114
|
|
|
@@ -119,14 +121,14 @@ Actions without a validation schema are skipped and listed in the CLI output.
|
|
|
119
121
|
|
|
120
122
|
## Database
|
|
121
123
|
|
|
122
|
-
This project ships with [
|
|
124
|
+
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
125
|
|
|
124
126
|
See the [Drizzle getting started guide](https://orm.drizzle.team/docs/get-started) for setup instructions with each supported database.
|
|
125
127
|
|
|
126
|
-
## Tech
|
|
128
|
+
## Tech stack
|
|
127
129
|
|
|
128
130
|
- **Runtime:** [Bun](https://bun.sh)
|
|
129
|
-
- **Framework:** [
|
|
130
|
-
- **Database:** [
|
|
131
|
+
- **Framework:** [Nile Js](https://nile-js.github.io/nile/) (`@nilejs/nile`)
|
|
132
|
+
- **Database:** [PGlite](https://electric-sql.com/product/pglite) (embedded Postgres)
|
|
131
133
|
- **ORM:** [Drizzle](https://orm.drizzle.team)
|
|
132
134
|
- **Validation:** [Zod](https://zod.dev)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
services:
|
|
2
|
+
nile:
|
|
3
|
+
build:
|
|
4
|
+
context: .
|
|
5
|
+
dockerfile: Dockerfile
|
|
6
|
+
ports:
|
|
7
|
+
- "8000:8000"
|
|
8
|
+
environment:
|
|
9
|
+
- NODE_ENV=production
|
|
10
|
+
volumes:
|
|
11
|
+
# The starter keeps its PGlite database in ./data; this volume keeps it across restarts.
|
|
12
|
+
- nile_data:/app/data
|
|
13
|
+
restart: unless-stopped
|
|
14
|
+
|
|
15
|
+
volumes:
|
|
16
|
+
nile_data:
|
package/template/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
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",
|
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
"@electric-sql/pglite": "^0.2.5",
|
|
16
16
|
"drizzle-orm": "^0.45.2",
|
|
17
17
|
"pino": "^10.3.1",
|
|
18
|
-
"slang-ts": "^0.0.7",
|
|
19
18
|
"zod": "^4.3.6"
|
|
20
19
|
},
|
|
21
20
|
"devDependencies": {
|
package/template/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createLogger, createNileServer } from "@nilejs/nile";
|
|
2
2
|
import { sql } from "drizzle-orm";
|
|
3
|
-
import {
|
|
3
|
+
import { Ok, safeTry } from "@nilejs/nile/fp";
|
|
4
4
|
import { db } from "@/db/client";
|
|
5
5
|
import { services } from "@/services/services.config";
|
|
6
6
|
|
|
@@ -31,6 +31,9 @@ const pushSchema = async () => {
|
|
|
31
31
|
|
|
32
32
|
const server = await createNileServer({
|
|
33
33
|
serverName: "{{projectName}}",
|
|
34
|
+
runtime: "bun",
|
|
35
|
+
native: true,
|
|
36
|
+
project: { structure: "error" },
|
|
34
37
|
services,
|
|
35
38
|
resources: { logger, database: db },
|
|
36
39
|
rest: {
|
|
@@ -42,6 +45,8 @@ const server = await createNileServer({
|
|
|
42
45
|
discovery: { enabled: true },
|
|
43
46
|
},
|
|
44
47
|
onBoot: {
|
|
48
|
+
// The first PGlite start on a slow machine can take longer than the 10 second default.
|
|
49
|
+
maxWaitTime: 30_000,
|
|
45
50
|
fn: async (ctx) => {
|
|
46
51
|
await pushSchema();
|
|
47
52
|
ctx.resources?.logger?.info({
|
|
@@ -54,18 +59,19 @@ const server = await createNileServer({
|
|
|
54
59
|
});
|
|
55
60
|
|
|
56
61
|
// Register custom middleware (runs before all service requests)
|
|
57
|
-
server.
|
|
62
|
+
server.nativeRest?.addMiddleware("/api", async (c, next) => {
|
|
58
63
|
const start = performance.now();
|
|
59
64
|
await next();
|
|
60
65
|
const ms = (performance.now() - start).toFixed(1);
|
|
61
66
|
logger.info({
|
|
62
67
|
atFunction: "requestLog",
|
|
63
|
-
message: `${c.
|
|
68
|
+
message: `${c.request.method} ${c.path} - ${ms}ms`,
|
|
64
69
|
});
|
|
70
|
+
return undefined;
|
|
65
71
|
});
|
|
66
72
|
|
|
67
|
-
if (server.
|
|
73
|
+
if (server.nativeRest) {
|
|
68
74
|
const port = server.config.rest?.port ?? 8000;
|
|
69
|
-
Bun.serve({ port, fetch: server.
|
|
75
|
+
Bun.serve({ port, fetch: server.nativeRest.fetch });
|
|
70
76
|
console.log(`\n{{projectName}} listening on http://localhost:${port}\n`);
|
|
71
77
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createServices, type Services } from "@nilejs/nile";
|
|
2
|
-
import { createTaskAction } from "./tasks/create";
|
|
3
|
-
import { deleteTaskAction } from "./tasks/delete";
|
|
4
|
-
import { getTaskAction } from "./tasks/get";
|
|
5
|
-
import { listTaskAction } from "./tasks/list";
|
|
6
|
-
import { updateTaskAction } from "./tasks/update";
|
|
2
|
+
import { createTaskAction } from "./tasks/actions/create";
|
|
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";
|
|
7
7
|
|
|
8
8
|
export const services: Services = createServices([
|
|
9
9
|
{
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Err, Ok } from "
|
|
1
|
+
import { createAction } from "@nilejs/nile";
|
|
2
|
+
import { Err, Ok } from "@nilejs/nile/fp";
|
|
3
3
|
import z from "zod";
|
|
4
4
|
import { taskModel } from "@/db/models";
|
|
5
5
|
|
|
@@ -12,23 +12,27 @@ const createTaskSchema = z.object({
|
|
|
12
12
|
.default("pending"),
|
|
13
13
|
});
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
type CreateTaskInput = z.infer<typeof createTaskSchema>;
|
|
16
|
+
|
|
17
|
+
const createTaskHandler = async (input: CreateTaskInput) => {
|
|
16
18
|
const result = await taskModel.create({
|
|
17
19
|
data: {
|
|
18
|
-
title:
|
|
19
|
-
description:
|
|
20
|
-
status:
|
|
20
|
+
title: input.title,
|
|
21
|
+
description: input.description,
|
|
22
|
+
status: input.status,
|
|
21
23
|
},
|
|
22
24
|
});
|
|
25
|
+
|
|
23
26
|
if (result.isErr) {
|
|
24
27
|
return Err(result.error);
|
|
25
28
|
}
|
|
29
|
+
|
|
26
30
|
return Ok({ task: result.value });
|
|
27
31
|
};
|
|
28
32
|
|
|
29
|
-
export const createTaskAction
|
|
33
|
+
export const createTaskAction = createAction<CreateTaskInput>({
|
|
30
34
|
name: "create",
|
|
31
35
|
description: "Create a new task",
|
|
32
|
-
handler: createTaskHandler,
|
|
33
36
|
validation: createTaskSchema,
|
|
37
|
+
handler: createTaskHandler,
|
|
34
38
|
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createAction } from "@nilejs/nile";
|
|
2
|
+
import { Err, Ok } from "@nilejs/nile/fp";
|
|
3
|
+
import z from "zod";
|
|
4
|
+
import { taskModel } from "@/db/models";
|
|
5
|
+
|
|
6
|
+
const deleteTaskSchema = z.object({
|
|
7
|
+
id: z.string().min(1, "Task ID is required"),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
type DeleteTaskInput = z.infer<typeof deleteTaskSchema>;
|
|
11
|
+
|
|
12
|
+
const deleteTaskHandler = async (input: DeleteTaskInput) => {
|
|
13
|
+
const result = await taskModel.delete(input.id);
|
|
14
|
+
|
|
15
|
+
if (result.isErr) {
|
|
16
|
+
return Err(result.error);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return Ok({ deleted: true, id: input.id });
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const deleteTaskAction = createAction<DeleteTaskInput>({
|
|
23
|
+
name: "delete",
|
|
24
|
+
description: "Delete a task by ID",
|
|
25
|
+
validation: deleteTaskSchema,
|
|
26
|
+
handler: deleteTaskHandler,
|
|
27
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Err, Ok } from "
|
|
1
|
+
import { createAction } from "@nilejs/nile";
|
|
2
|
+
import { Err, Ok } from "@nilejs/nile/fp";
|
|
3
3
|
import z from "zod";
|
|
4
4
|
import { taskModel } from "@/db/models";
|
|
5
5
|
|
|
@@ -7,17 +7,21 @@ const getTaskSchema = z.object({
|
|
|
7
7
|
id: z.string().min(1, "Task ID is required"),
|
|
8
8
|
});
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
type GetTaskInput = z.infer<typeof getTaskSchema>;
|
|
11
|
+
|
|
12
|
+
const getTaskHandler = async (input: GetTaskInput) => {
|
|
13
|
+
const result = await taskModel.findById(input.id);
|
|
14
|
+
|
|
12
15
|
if (result.isErr) {
|
|
13
16
|
return Err(result.error);
|
|
14
17
|
}
|
|
18
|
+
|
|
15
19
|
return Ok({ task: result.value });
|
|
16
20
|
};
|
|
17
21
|
|
|
18
|
-
export const getTaskAction
|
|
22
|
+
export const getTaskAction = createAction<GetTaskInput>({
|
|
19
23
|
name: "get",
|
|
20
24
|
description: "Get a task by ID",
|
|
21
|
-
handler: getTaskHandler,
|
|
22
25
|
validation: getTaskSchema,
|
|
26
|
+
handler: getTaskHandler,
|
|
23
27
|
});
|
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Err, Ok } from "
|
|
1
|
+
import { createAction } from "@nilejs/nile";
|
|
2
|
+
import { Err, Ok } from "@nilejs/nile/fp";
|
|
3
3
|
import { taskModel } from "@/db/models";
|
|
4
4
|
|
|
5
5
|
const listTasksHandler = async () => {
|
|
6
6
|
const result = await taskModel.findAll();
|
|
7
|
+
|
|
7
8
|
if (result.isErr) {
|
|
8
9
|
return Err(result.error);
|
|
9
10
|
}
|
|
11
|
+
|
|
10
12
|
return Ok({ tasks: result.value });
|
|
11
13
|
};
|
|
12
14
|
|
|
13
|
-
export const listTaskAction
|
|
15
|
+
export const listTaskAction = createAction({
|
|
14
16
|
name: "list",
|
|
15
17
|
description: "List all tasks",
|
|
16
18
|
handler: listTasksHandler,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Err, Ok } from "
|
|
1
|
+
import { createAction } from "@nilejs/nile";
|
|
2
|
+
import { Err, Ok } from "@nilejs/nile/fp";
|
|
3
3
|
import z from "zod";
|
|
4
4
|
import { taskModel } from "@/db/models";
|
|
5
5
|
|
|
@@ -10,22 +10,29 @@ const updateTaskSchema = z.object({
|
|
|
10
10
|
status: z.enum(["pending", "in-progress", "done"]).optional(),
|
|
11
11
|
});
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
const { id, ...updates } = data;
|
|
13
|
+
type UpdateTaskInput = z.infer<typeof updateTaskSchema>;
|
|
15
14
|
|
|
15
|
+
const updateTaskHandler = async (input: UpdateTaskInput) => {
|
|
16
|
+
// Fields left out of the request stay undefined and are not changed.
|
|
16
17
|
const result = await taskModel.update({
|
|
17
|
-
id: id
|
|
18
|
-
data:
|
|
18
|
+
id: input.id,
|
|
19
|
+
data: {
|
|
20
|
+
title: input.title,
|
|
21
|
+
description: input.description,
|
|
22
|
+
status: input.status,
|
|
23
|
+
},
|
|
19
24
|
});
|
|
25
|
+
|
|
20
26
|
if (result.isErr) {
|
|
21
27
|
return Err(result.error);
|
|
22
28
|
}
|
|
29
|
+
|
|
23
30
|
return Ok({ task: result.value });
|
|
24
31
|
};
|
|
25
32
|
|
|
26
|
-
export const updateTaskAction
|
|
33
|
+
export const updateTaskAction = createAction<UpdateTaskInput>({
|
|
27
34
|
name: "update",
|
|
28
35
|
description: "Update an existing task",
|
|
29
|
-
handler: updateTaskHandler,
|
|
30
36
|
validation: updateTaskSchema,
|
|
37
|
+
handler: updateTaskHandler,
|
|
31
38
|
});
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { type Action, createAction } from "@nilejs/nile";
|
|
2
|
-
import { Err, Ok } from "slang-ts";
|
|
3
|
-
import z from "zod";
|
|
4
|
-
import { taskModel } from "@/db/models";
|
|
5
|
-
|
|
6
|
-
const deleteTaskSchema = z.object({
|
|
7
|
-
id: z.string().min(1, "Task ID is required"),
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
const deleteTaskHandler = async (data: Record<string, unknown>) => {
|
|
11
|
-
const result = await taskModel.delete(data.id as string);
|
|
12
|
-
if (result.isErr) {
|
|
13
|
-
return Err(result.error);
|
|
14
|
-
}
|
|
15
|
-
return Ok({ deleted: true, id: data.id });
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export const deleteTaskAction: Action = createAction({
|
|
19
|
-
name: "delete",
|
|
20
|
-
description: "Delete a task by ID",
|
|
21
|
-
handler: deleteTaskHandler,
|
|
22
|
-
validation: deleteTaskSchema,
|
|
23
|
-
});
|