domain-driver 0.3.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +125 -24
- package/dist/cli.js +5 -4
- package/dist/commands/action.js +19 -5
- package/dist/commands/controller-renderer.js +25 -0
- package/dist/commands/controller.js +6 -4
- package/dist/commands/feature.js +8 -3
- package/dist/commands/hints.js +10 -0
- package/dist/commands/hook-renderer.js +63 -0
- package/dist/commands/hook.js +21 -43
- package/dist/commands/repository.js +8 -1
- package/dist/commands/target.js +7 -3
- package/dist/init/content.js +9 -4
- package/dist/stack/detect.js +57 -11
- package/dist/stack/profiles/nest.js +1 -0
- package/dist/stack/profiles/next-frontend.js +1 -0
- package/dist/stack/profiles/next-fullstack.js +1 -0
- package/dist/stack/profiles/node.js +1 -0
- package/dist/stack/profiles/react.js +1 -0
- package/dist/stack/profiles/tanstack-start.js +46 -0
- package/dist/stack/registry.js +4 -1
- package/dist/stack/types.js +1 -1
- package/dist/templates/actions.js +12 -0
- package/dist/templates/controllers/server-fn.js +42 -0
- package/dist/templates/frontend/container.js +10 -6
- package/dist/templates/frontend/hook.js +63 -79
- package/dist/templates/frontend/query-hook.js +83 -0
- package/dist/templates/frontend/query-keys.js +15 -0
- package/dist/templates/frontend/route.js +33 -0
- package/dist/templates/frontend/server-fn-repository.js +27 -0
- package/package.json +6 -3
package/dist/commands/target.js
CHANGED
|
@@ -3,11 +3,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.parseTarget = parseTarget;
|
|
4
4
|
exports.parseFeatureTarget = parseFeatureTarget;
|
|
5
5
|
const naming_1 = require("../utils/naming");
|
|
6
|
-
const NAME_PATTERN = /^[A-
|
|
6
|
+
const NAME_PATTERN = /^[A-Z][A-Za-z0-9]*$/;
|
|
7
|
+
const HOOK_NAME_PATTERN = /^use[A-Z]/;
|
|
7
8
|
function validateName(name) {
|
|
8
|
-
if (
|
|
9
|
-
|
|
9
|
+
if (NAME_PATTERN.test(name))
|
|
10
|
+
return;
|
|
11
|
+
if (HOOK_NAME_PATTERN.test(name)) {
|
|
12
|
+
throw new Error(`Name "${name}" is a hook name. Pass the entity instead, for example ${name.slice(3)}.`);
|
|
10
13
|
}
|
|
14
|
+
throw new Error(`Name "${name}" must be PascalCase, for example User.`);
|
|
11
15
|
}
|
|
12
16
|
function parseTarget(value) {
|
|
13
17
|
const parts = value.split('/');
|
package/dist/init/content.js
CHANGED
|
@@ -15,6 +15,7 @@ const AGENTS_LINES = [
|
|
|
15
15
|
'Rules the generated code follows, and that new code must keep:',
|
|
16
16
|
'',
|
|
17
17
|
'- One file per action per layer. `findActiveUsers` gets `FindActiveUsers.service.ts`, `FindActiveUsers.repository.ts`, and `FindActiveUsers.controller.ts`. It never goes inside `ShowUser.service.ts` or `ListUser.service.ts`.',
|
|
18
|
+
'- Hooks are one file per action too: `<Action><Entity>.hook.ts` exporting `use<Action><Entity>` (for example `useListUser`, `useCreateUser`). There is no combined `use<Entity>.ts`.',
|
|
18
19
|
'- The chain is controller or hook, then service, then repository. Business logic lives in services. Data access lives in repositories. Controllers validate input and call one service.',
|
|
19
20
|
'- Feature folders are kebab-case (`coffee-type`). Entity, action, and class names are PascalCase (`CoffeeType`, `FindActiveUsers`).',
|
|
20
21
|
'- Generated repositories throw until you wire them to your data source. Generated controllers on Node need a line in `<feature>.routes.ts`, and on Nest need registering in `<feature>.module.ts`; the tool prints the exact line.',
|
|
@@ -34,7 +35,8 @@ const SKILL_LINES = [
|
|
|
34
35
|
'',
|
|
35
36
|
'## Detect the stack',
|
|
36
37
|
'',
|
|
37
|
-
'The tool reads `package.json` and prints `Stack: <stack> (detected)
|
|
38
|
+
'The tool reads `package.json` and prints `Stack: <stack> (detected), root: <dir>` before every command. Stacks: `next-fullstack`, `next-frontend`, `react`, `node` (Express, Fastify, Hono, or none), `nest`, `tanstack-start`. Override with `--stack <name>` if detection is wrong.',
|
|
39
|
+
'Where features are written can be overridden too: `--root <dir>` for one command, or a `domainDriver.featureRoot` key in `package.json` for the project. Read the root off the stack line rather than assuming the convention.',
|
|
38
40
|
'',
|
|
39
41
|
'## Commands',
|
|
40
42
|
'',
|
|
@@ -49,10 +51,12 @@ const SKILL_LINES = [
|
|
|
49
51
|
'| Five controllers or Next route handlers | `npx domain-driver make:controller users/User` |',
|
|
50
52
|
'| A bespoke operation | `npx domain-driver make:action users/User findActiveUsers` |',
|
|
51
53
|
'| Bespoke operation with a request body | `npx domain-driver make:action users/User archiveUser --with-input --returns one` |',
|
|
52
|
-
'| Component, container, hook | `npx domain-driver make:component users/UserCard`, `make:container users/UserContainer`, `make:hook users/
|
|
54
|
+
'| Component, container, hook | `npx domain-driver make:component users/UserCard`, `make:container users/UserContainer`, `make:hook users/User` |',
|
|
53
55
|
'| Refresh this guidance | `npx domain-driver init` |',
|
|
54
56
|
'',
|
|
55
|
-
'On `next-fullstack`, `make:service` and `make:repository` take `--side client|server|both` (default both).',
|
|
57
|
+
'On `next-fullstack` and `tanstack-start`, `make:service` and `make:repository` take `--side client|server|both` (default both).',
|
|
58
|
+
'',
|
|
59
|
+
'Hooks are one file per action — `<Action><Entity>.hook.ts` exporting `use<Action><Entity>` — never a combined `use<Entity>.ts`. On `tanstack-start` they are TanStack Query hooks backed by a generated `<feature>.keys.ts`; elsewhere they are plain React state.',
|
|
56
60
|
'',
|
|
57
61
|
'## Rules',
|
|
58
62
|
'',
|
|
@@ -67,7 +71,8 @@ const SKILL_LINES = [
|
|
|
67
71
|
'',
|
|
68
72
|
'- Next.js: `app/<feature>` or `src/app/<feature>`; route handlers under `app/api/<feature>`.',
|
|
69
73
|
'- React and Node: `src/features/<feature>` or `features/<feature>`.',
|
|
70
|
-
'- Nest: `src/<feature>` with a `<feature>.module.ts`.',
|
|
74
|
+
'- Nest: `src/features/<feature>` when `src/features` exists, otherwise `src/<feature>`, with a `<feature>.module.ts`.',
|
|
75
|
+
'- TanStack Start: `src/routes/<feature>` or `routes/<feature>`; every layer folder is prefixed with `-` so the router ignores it, and the entry file is `index.tsx`, not `page.tsx`.',
|
|
71
76
|
];
|
|
72
77
|
exports.AGENTS_SECTION = AGENTS_LINES.join('\n');
|
|
73
78
|
exports.SKILL_CONTENT = `${SKILL_LINES.join('\n')}\n`;
|
package/dist/stack/detect.js
CHANGED
|
@@ -41,22 +41,30 @@ const path = __importStar(require("path"));
|
|
|
41
41
|
const fs_1 = require("../utils/fs");
|
|
42
42
|
const types_1 = require("./types");
|
|
43
43
|
const API_DIRS = ['app/api', 'src/app/api', 'pages/api', 'src/pages/api'];
|
|
44
|
+
const ROOT_LABELS = Object.freeze({
|
|
45
|
+
detected: '',
|
|
46
|
+
flag: ' (--root)',
|
|
47
|
+
config: ' (package.json)',
|
|
48
|
+
});
|
|
44
49
|
let cached;
|
|
45
50
|
function resetStackCache() {
|
|
46
51
|
cached = undefined;
|
|
47
52
|
}
|
|
48
|
-
function detectStack(override) {
|
|
53
|
+
function detectStack(override, rootOverride) {
|
|
49
54
|
if (cached)
|
|
50
55
|
return cached;
|
|
51
56
|
const cwd = process.cwd();
|
|
52
57
|
const overridden = override !== undefined;
|
|
53
|
-
const
|
|
58
|
+
const pkg = readPackageJson(cwd, overridden);
|
|
59
|
+
const deps = dependencyNames(pkg);
|
|
54
60
|
const stack = overridden ? parseOverride(override) : inferStack(cwd, deps);
|
|
61
|
+
const root = resolveRoot(cwd, stack, pkg, rootOverride);
|
|
55
62
|
const result = Object.freeze({
|
|
56
63
|
stack,
|
|
57
64
|
source: overridden ? 'override' : 'detected',
|
|
58
65
|
httpFramework: stack === 'node' ? detectHttpFramework(deps) : null,
|
|
59
|
-
featureRoot:
|
|
66
|
+
featureRoot: root.featureRoot,
|
|
67
|
+
featureRootSource: root.source,
|
|
60
68
|
hasNestjsZod: deps.has('nestjs-zod'),
|
|
61
69
|
});
|
|
62
70
|
cached = result;
|
|
@@ -64,18 +72,19 @@ function detectStack(override) {
|
|
|
64
72
|
}
|
|
65
73
|
function describeStack(stack) {
|
|
66
74
|
const base = `Stack: ${stack.stack} (${stack.source})`;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return `${base}, http: ${stack.httpFramework ?? 'none'}`;
|
|
75
|
+
const http = stack.stack === 'node' ? `, http: ${stack.httpFramework ?? 'none'}` : '';
|
|
76
|
+
return `${base}${http}, root: ${stack.featureRoot}${ROOT_LABELS[stack.featureRootSource]}`;
|
|
70
77
|
}
|
|
71
|
-
function
|
|
78
|
+
function readPackageJson(cwd, optional) {
|
|
72
79
|
const pkgPath = path.join(cwd, 'package.json');
|
|
73
80
|
if (!fs.existsSync(pkgPath)) {
|
|
74
81
|
if (optional)
|
|
75
|
-
return
|
|
82
|
+
return {};
|
|
76
83
|
throw new Error(`No package.json found in ${cwd}. Run domain-driver from your project root, or pass --stack <name>.`);
|
|
77
84
|
}
|
|
78
|
-
|
|
85
|
+
return parsePackageJson(pkgPath);
|
|
86
|
+
}
|
|
87
|
+
function dependencyNames(pkg) {
|
|
79
88
|
return new Set([
|
|
80
89
|
...Object.keys(pkg.dependencies ?? {}),
|
|
81
90
|
...Object.keys(pkg.devDependencies ?? {}),
|
|
@@ -99,6 +108,8 @@ function parseOverride(value) {
|
|
|
99
108
|
function inferStack(cwd, deps) {
|
|
100
109
|
if (deps.has('@nestjs/core'))
|
|
101
110
|
return 'nest';
|
|
111
|
+
if (deps.has('@tanstack/react-start'))
|
|
112
|
+
return 'tanstack-start';
|
|
102
113
|
if (deps.has('next'))
|
|
103
114
|
return hasApiDir(cwd) ? 'next-fullstack' : 'next-frontend';
|
|
104
115
|
if (deps.has('react'))
|
|
@@ -111,7 +122,38 @@ function hasApiDir(cwd) {
|
|
|
111
122
|
function detectHttpFramework(deps) {
|
|
112
123
|
return types_1.HTTP_FRAMEWORKS.find((name) => deps.has(name)) ?? null;
|
|
113
124
|
}
|
|
114
|
-
function
|
|
125
|
+
function resolveRoot(cwd, stack, pkg, flag) {
|
|
126
|
+
if (flag !== undefined) {
|
|
127
|
+
return { featureRoot: validateRoot(flag, '--root'), source: 'flag' };
|
|
128
|
+
}
|
|
129
|
+
const configured = configuredRoot(pkg);
|
|
130
|
+
if (configured !== null) {
|
|
131
|
+
return { featureRoot: configured, source: 'config' };
|
|
132
|
+
}
|
|
133
|
+
return { featureRoot: conventionalRoot(cwd, stack), source: 'detected' };
|
|
134
|
+
}
|
|
135
|
+
function configuredRoot(pkg) {
|
|
136
|
+
const value = pkg.domainDriver?.featureRoot;
|
|
137
|
+
if (value === undefined)
|
|
138
|
+
return null;
|
|
139
|
+
if (typeof value !== 'string') {
|
|
140
|
+
throw new Error('package.json "domainDriver.featureRoot" must be a string, for example "src/features".');
|
|
141
|
+
}
|
|
142
|
+
return validateRoot(value, 'package.json');
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* The root becomes a filesystem path under the project, so an absolute path or any
|
|
146
|
+
* `..` segment would write outside it. Both are rejected rather than normalised away.
|
|
147
|
+
*/
|
|
148
|
+
function validateRoot(value, source) {
|
|
149
|
+
const normalized = value.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
150
|
+
const escapes = normalized === '' || path.isAbsolute(value) || normalized.split('/').includes('..');
|
|
151
|
+
if (escapes) {
|
|
152
|
+
throw new Error(`Feature root "${value}" from ${source} must be a relative path inside the project, for example src/features.`);
|
|
153
|
+
}
|
|
154
|
+
return normalized;
|
|
155
|
+
}
|
|
156
|
+
function conventionalRoot(cwd, stack) {
|
|
115
157
|
switch (stack) {
|
|
116
158
|
case 'next-fullstack':
|
|
117
159
|
case 'next-frontend':
|
|
@@ -120,6 +162,10 @@ function resolveFeatureRoot(cwd, stack) {
|
|
|
120
162
|
case 'node':
|
|
121
163
|
return (0, fs_1.isDirectory)(path.join(cwd, 'src')) ? 'src/features' : 'features';
|
|
122
164
|
case 'nest':
|
|
123
|
-
return 'src';
|
|
165
|
+
return (0, fs_1.isDirectory)(path.join(cwd, 'src', 'features')) ? 'src/features' : 'src';
|
|
166
|
+
case 'tanstack-start':
|
|
167
|
+
if ((0, fs_1.isDirectory)(path.join(cwd, 'src', 'routes')))
|
|
168
|
+
return 'src/routes';
|
|
169
|
+
return (0, fs_1.isDirectory)(path.join(cwd, 'routes')) ? 'routes' : 'src/routes';
|
|
124
170
|
}
|
|
125
171
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.tanstackStart = void 0;
|
|
4
|
+
exports.tanstackStart = Object.freeze({
|
|
5
|
+
name: 'tanstack-start',
|
|
6
|
+
folders: [
|
|
7
|
+
'-components',
|
|
8
|
+
'-containers',
|
|
9
|
+
'-hooks',
|
|
10
|
+
'-client/services',
|
|
11
|
+
'-client/repositories',
|
|
12
|
+
'-server/functions',
|
|
13
|
+
'-server/services',
|
|
14
|
+
'-server/repositories',
|
|
15
|
+
'-schemas',
|
|
16
|
+
'-types',
|
|
17
|
+
],
|
|
18
|
+
layers: [
|
|
19
|
+
'page',
|
|
20
|
+
'component',
|
|
21
|
+
'container',
|
|
22
|
+
'hook',
|
|
23
|
+
'clientService',
|
|
24
|
+
'clientRepository',
|
|
25
|
+
'serverService',
|
|
26
|
+
'serverRepository',
|
|
27
|
+
'controller',
|
|
28
|
+
'schema',
|
|
29
|
+
'types',
|
|
30
|
+
],
|
|
31
|
+
layerDirs: {
|
|
32
|
+
component: '-components',
|
|
33
|
+
container: '-containers',
|
|
34
|
+
hook: '-hooks',
|
|
35
|
+
clientService: '-client/services',
|
|
36
|
+
clientRepository: '-client/repositories',
|
|
37
|
+
serverService: '-server/services',
|
|
38
|
+
serverRepository: '-server/repositories',
|
|
39
|
+
controller: '-server/functions',
|
|
40
|
+
schema: '-schemas',
|
|
41
|
+
types: '-types',
|
|
42
|
+
},
|
|
43
|
+
clientDirective: false,
|
|
44
|
+
serverComponents: false,
|
|
45
|
+
queryHooks: true,
|
|
46
|
+
});
|
package/dist/stack/registry.js
CHANGED
|
@@ -11,12 +11,14 @@ const next_frontend_1 = require("./profiles/next-frontend");
|
|
|
11
11
|
const react_1 = require("./profiles/react");
|
|
12
12
|
const node_1 = require("./profiles/node");
|
|
13
13
|
const nest_1 = require("./profiles/nest");
|
|
14
|
+
const tanstack_start_1 = require("./profiles/tanstack-start");
|
|
14
15
|
const PROFILES = Object.freeze({
|
|
15
16
|
'next-fullstack': next_fullstack_1.nextFullstack,
|
|
16
17
|
'next-frontend': next_frontend_1.nextFrontend,
|
|
17
18
|
react: react_1.react,
|
|
18
19
|
node: node_1.node,
|
|
19
20
|
nest: nest_1.nest,
|
|
21
|
+
'tanstack-start': tanstack_start_1.tanstackStart,
|
|
20
22
|
});
|
|
21
23
|
const LAYER_COMMANDS = Object.freeze({
|
|
22
24
|
component: 'make:component',
|
|
@@ -45,7 +47,8 @@ function layerDir(profile, layer) {
|
|
|
45
47
|
return dir;
|
|
46
48
|
}
|
|
47
49
|
function componentDir(profile, type) {
|
|
48
|
-
|
|
50
|
+
const base = layerDir(profile, 'component');
|
|
51
|
+
return profile.serverComponents ? `${base}/${type}` : base;
|
|
49
52
|
}
|
|
50
53
|
function availableCommands(profile) {
|
|
51
54
|
const commands = profile.layers
|
package/dist/stack/types.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.LAYERS = exports.HTTP_FRAMEWORKS = exports.STACK_NAMES = void 0;
|
|
4
4
|
exports.isStackName = isStackName;
|
|
5
|
-
exports.STACK_NAMES = ['next-fullstack', 'next-frontend', 'react', 'node', 'nest'];
|
|
5
|
+
exports.STACK_NAMES = ['next-fullstack', 'next-frontend', 'react', 'node', 'nest', 'tanstack-start'];
|
|
6
6
|
exports.HTTP_FRAMEWORKS = ['express', 'fastify', 'hono'];
|
|
7
7
|
exports.LAYERS = [
|
|
8
8
|
'page',
|
|
@@ -5,6 +5,7 @@ exports.actionCase = actionCase;
|
|
|
5
5
|
exports.standardAction = standardAction;
|
|
6
6
|
exports.standardActions = standardActions;
|
|
7
7
|
exports.customAction = customAction;
|
|
8
|
+
exports.isQueryAction = isQueryAction;
|
|
8
9
|
const naming_1 = require("../utils/naming");
|
|
9
10
|
exports.ACTIONS = ['List', 'Show', 'Create', 'Update', 'Delete'];
|
|
10
11
|
exports.WRITE_ACTIONS = ['Create', 'Update'];
|
|
@@ -66,3 +67,14 @@ function customAction(entity, actionName, options) {
|
|
|
66
67
|
failure: `Failed to ${camel} ${entity}`,
|
|
67
68
|
});
|
|
68
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Whether a hook renders as a query (auto-fetches, returns data) rather than a mutation
|
|
72
|
+
* (triggered imperatively). `spec.method === 'get'` alone is not enough: a custom action
|
|
73
|
+
* declared `--returns void` is also a GET (no input forces GET regardless of return kind),
|
|
74
|
+
* but it has no payload worth polling for and must not auto-fire on mount. `usesEntityType`
|
|
75
|
+
* is false exactly when the action returns void, so requiring it here routes that case to
|
|
76
|
+
* the mutation branch instead.
|
|
77
|
+
*/
|
|
78
|
+
function isQueryAction(spec) {
|
|
79
|
+
return spec.method === 'get' && spec.usesEntityType;
|
|
80
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderServerFn = renderServerFn;
|
|
4
|
+
const naming_1 = require("../../utils/naming");
|
|
5
|
+
function inputFor(spec) {
|
|
6
|
+
if (spec.schema !== null && spec.usesId) {
|
|
7
|
+
return {
|
|
8
|
+
validator: `z.object({ id: z.string(), data: ${spec.schema}Schema })`,
|
|
9
|
+
call: 'data.id, data.data',
|
|
10
|
+
handlerArg: '{ data }',
|
|
11
|
+
needsZod: true,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
if (spec.schema !== null) {
|
|
15
|
+
return { validator: `${spec.schema}Schema`, call: 'data', handlerArg: '{ data }', needsZod: false };
|
|
16
|
+
}
|
|
17
|
+
if (spec.usesId) {
|
|
18
|
+
return { validator: 'z.string()', call: 'data', handlerArg: '{ data }', needsZod: true };
|
|
19
|
+
}
|
|
20
|
+
return { validator: null, call: '', handlerArg: '', needsZod: false };
|
|
21
|
+
}
|
|
22
|
+
function renderServerFn(ctx, spec, entity, fromFile) {
|
|
23
|
+
const { validator, call, handlerArg, needsZod } = inputFor(spec);
|
|
24
|
+
const method = spec.method === 'get' ? 'GET' : 'POST';
|
|
25
|
+
const servicePath = ctx.importLayer(fromFile, 'serverService', `${spec.name}.service`);
|
|
26
|
+
const imports = ["import { createServerFn } from '@tanstack/react-start';"];
|
|
27
|
+
if (needsZod)
|
|
28
|
+
imports.push("import { z } from 'zod';");
|
|
29
|
+
if (spec.schema !== null) {
|
|
30
|
+
const schemaPath = ctx.importLayer(fromFile, 'schema', `${spec.schema}.schema`);
|
|
31
|
+
imports.push(`import { ${spec.schema}Schema } from '${schemaPath}';`);
|
|
32
|
+
}
|
|
33
|
+
imports.push(`import { ${spec.name}Service } from '${servicePath}';`);
|
|
34
|
+
const validatorLine = validator === null ? '' : `\n .validator(${validator})`;
|
|
35
|
+
return `${imports.join('\n')}
|
|
36
|
+
|
|
37
|
+
const service = new ${spec.name}Service();
|
|
38
|
+
|
|
39
|
+
export const ${(0, naming_1.lowerFirst)(spec.name)} = createServerFn({ method: '${method}' })${validatorLine}
|
|
40
|
+
.handler(async (${handlerArg}) => service.handle(${call}));
|
|
41
|
+
`;
|
|
42
|
+
}
|
|
@@ -4,21 +4,25 @@ exports.renderContainer = renderContainer;
|
|
|
4
4
|
const registry_1 = require("../../stack/registry");
|
|
5
5
|
function renderContainer(ctx, containerName, entity, fromFile) {
|
|
6
6
|
const header = ctx.profile.clientDirective ? "'use client';\n\n" : '';
|
|
7
|
-
const hookName = `
|
|
8
|
-
const hookPath = ctx.importLayer(fromFile, 'hook',
|
|
7
|
+
const hookName = `useList${entity}`;
|
|
8
|
+
const hookPath = ctx.importLayer(fromFile, 'hook', `List${entity}.hook`);
|
|
9
9
|
const componentPath = ctx.importFrom(fromFile, `${(0, registry_1.componentDir)(ctx.profile, 'client')}/${entity}`);
|
|
10
|
+
const query = ctx.profile.queryHooks;
|
|
11
|
+
const loadingField = query ? 'isPending' : 'loading';
|
|
12
|
+
const errorExpression = query ? '{error.message}' : '{error}';
|
|
13
|
+
const items = query ? '(data ?? [])' : 'data';
|
|
10
14
|
return `${header}import { ${hookName} } from '${hookPath}';
|
|
11
15
|
import ${entity} from '${componentPath}';
|
|
12
16
|
|
|
13
17
|
export default function ${containerName}() {
|
|
14
|
-
const {
|
|
18
|
+
const { data, ${loadingField}, error } = ${hookName}();
|
|
15
19
|
|
|
16
|
-
if (
|
|
17
|
-
if (error) return <div>Error: {
|
|
20
|
+
if (${loadingField}) return <div>Loading...</div>;
|
|
21
|
+
if (error) return <div>Error: ${errorExpression}</div>;
|
|
18
22
|
|
|
19
23
|
return (
|
|
20
24
|
<div>
|
|
21
|
-
{items.map((item) => (
|
|
25
|
+
{${items}.map((item) => (
|
|
22
26
|
<${entity} key={item.id} {...item} />
|
|
23
27
|
))}
|
|
24
28
|
</div>
|
|
@@ -1,109 +1,93 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.renderHook = renderHook;
|
|
4
|
+
const naming_1 = require("../../utils/naming");
|
|
4
5
|
const actions_1 = require("../actions");
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
return `import { ${action}${entity}Service } from '${servicePath}';`;
|
|
9
|
-
}).join('\n');
|
|
6
|
+
const signatures_1 = require("../signatures");
|
|
7
|
+
function payloadType(spec) {
|
|
8
|
+
return spec.returns.replace(/^Promise<(.*)>$/, '$1');
|
|
10
9
|
}
|
|
11
|
-
function
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const createPath = ctx.importLayer(fromFile, 'schema', `Create${entity}.schema`);
|
|
18
|
-
const updatePath = ctx.importLayer(fromFile, 'schema', `Update${entity}.schema`);
|
|
19
|
-
return `${header}import { useState, useEffect, useCallback } from 'react';
|
|
20
|
-
import { ${entity} } from '${typePath}';
|
|
21
|
-
${serviceImports(ctx, fromFile, entity)}
|
|
22
|
-
import { Create${entity} } from '${createPath}';
|
|
23
|
-
import { Update${entity} } from '${updatePath}';
|
|
24
|
-
|
|
25
|
-
${serviceInstances(entity)}
|
|
10
|
+
function header(ctx, spec, entity, fromFile, hooks) {
|
|
11
|
+
const directive = ctx.profile.clientDirective ? "'use client';\n\n" : '';
|
|
12
|
+
const servicePath = ctx.importLayer(fromFile, 'clientService', `${spec.name}.service`);
|
|
13
|
+
const domain = (0, signatures_1.domainImports)(ctx, fromFile, spec, entity);
|
|
14
|
+
return `${directive}import { ${hooks} } from 'react';
|
|
15
|
+
${domain.join('\n')}${domain.length > 0 ? '\n' : ''}import { ${spec.name}Service } from '${servicePath}';
|
|
26
16
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
17
|
+
const service = new ${spec.name}Service();
|
|
18
|
+
`;
|
|
19
|
+
}
|
|
20
|
+
function renderQuery(ctx, spec, entity, fromFile) {
|
|
21
|
+
const type = payloadType(spec);
|
|
22
|
+
const isList = type.endsWith('[]');
|
|
23
|
+
const stateType = isList ? type : `${type} | null`;
|
|
24
|
+
const initial = isList ? '[]' : 'null';
|
|
25
|
+
const deps = spec.usesId ? '[id]' : '[]';
|
|
26
|
+
return `${header(ctx, spec, entity, fromFile, 'useState, useEffect, useCallback')}
|
|
27
|
+
export function use${spec.name}(${spec.params}) {
|
|
28
|
+
const [data, setData] = useState<${stateType}>(${initial});
|
|
30
29
|
const [loading, setLoading] = useState(false);
|
|
31
30
|
const [error, setError] = useState<string | null>(null);
|
|
32
31
|
|
|
33
|
-
const
|
|
34
|
-
setLoading(true);
|
|
35
|
-
setError(null);
|
|
36
|
-
try {
|
|
37
|
-
const data = await listService.handle();
|
|
38
|
-
setItems(data);
|
|
39
|
-
} catch (err: unknown) {
|
|
40
|
-
setError(err instanceof Error ? err.message : 'Failed to fetch');
|
|
41
|
-
} finally {
|
|
42
|
-
setLoading(false);
|
|
43
|
-
}
|
|
44
|
-
}, []);
|
|
45
|
-
|
|
46
|
-
const fetchOne = useCallback(async (id: string) => {
|
|
32
|
+
const refetch = useCallback(async () => {
|
|
47
33
|
setLoading(true);
|
|
48
34
|
setError(null);
|
|
49
35
|
try {
|
|
50
|
-
|
|
51
|
-
setSelected(data);
|
|
36
|
+
setData(await service.handle(${spec.args}));
|
|
52
37
|
} catch (err: unknown) {
|
|
53
|
-
setError(err instanceof Error ? err.message : '
|
|
38
|
+
setError(err instanceof Error ? err.message : '${spec.failure}');
|
|
54
39
|
} finally {
|
|
55
40
|
setLoading(false);
|
|
56
41
|
}
|
|
57
|
-
},
|
|
42
|
+
}, ${deps});
|
|
58
43
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
try {
|
|
63
|
-
const created = await createService.handle(data);
|
|
64
|
-
setItems((prev) => [...prev, created]);
|
|
65
|
-
return created;
|
|
66
|
-
} catch (err: unknown) {
|
|
67
|
-
setError(err instanceof Error ? err.message : 'Failed to create');
|
|
68
|
-
return null;
|
|
69
|
-
} finally {
|
|
70
|
-
setLoading(false);
|
|
71
|
-
}
|
|
72
|
-
}, []);
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
void refetch();
|
|
46
|
+
}, [refetch]);
|
|
73
47
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
48
|
+
return { data, loading, error, refetch };
|
|
49
|
+
}
|
|
50
|
+
`;
|
|
51
|
+
}
|
|
52
|
+
function renderMutation(ctx, spec, entity, fromFile) {
|
|
53
|
+
const callable = (0, naming_1.lowerFirst)(spec.name);
|
|
54
|
+
const returnsValue = spec.usesEntityType;
|
|
55
|
+
const type = payloadType(spec);
|
|
56
|
+
const callbackType = returnsValue ? `(result: ${type}) => void` : '() => void';
|
|
57
|
+
const body = returnsValue
|
|
58
|
+
? ` const result = await service.handle(${spec.args});
|
|
59
|
+
onSuccess?.(result);
|
|
60
|
+
return result;`
|
|
61
|
+
: ` await service.handle(${spec.args});
|
|
62
|
+
onSuccess?.();`;
|
|
63
|
+
const failure = returnsValue
|
|
64
|
+
? ` setError(err instanceof Error ? err.message : '${spec.failure}');
|
|
65
|
+
return null;`
|
|
66
|
+
: ` setError(err instanceof Error ? err.message : '${spec.failure}');`;
|
|
67
|
+
return `${header(ctx, spec, entity, fromFile, 'useState, useCallback')}
|
|
68
|
+
export function use${spec.name}(options: { onSuccess?: ${callbackType} } = {}) {
|
|
69
|
+
const { onSuccess } = options;
|
|
70
|
+
const [loading, setLoading] = useState(false);
|
|
71
|
+
const [error, setError] = useState<string | null>(null);
|
|
88
72
|
|
|
89
|
-
const
|
|
73
|
+
const ${callable} = useCallback(async (${spec.params}) => {
|
|
90
74
|
setLoading(true);
|
|
91
75
|
setError(null);
|
|
92
76
|
try {
|
|
93
|
-
|
|
94
|
-
setItems((prev) => prev.filter((item) => item.id !== id));
|
|
77
|
+
${body}
|
|
95
78
|
} catch (err: unknown) {
|
|
96
|
-
|
|
79
|
+
${failure}
|
|
97
80
|
} finally {
|
|
98
81
|
setLoading(false);
|
|
99
82
|
}
|
|
100
|
-
}, []);
|
|
101
|
-
|
|
102
|
-
useEffect(() => {
|
|
103
|
-
fetchAll();
|
|
104
|
-
}, [fetchAll]);
|
|
83
|
+
}, [onSuccess]);
|
|
105
84
|
|
|
106
|
-
return {
|
|
85
|
+
return { ${callable}, loading, error };
|
|
107
86
|
}
|
|
108
87
|
`;
|
|
109
88
|
}
|
|
89
|
+
function renderHook(ctx, spec, entity, fromFile) {
|
|
90
|
+
return (0, actions_1.isQueryAction)(spec)
|
|
91
|
+
? renderQuery(ctx, spec, entity, fromFile)
|
|
92
|
+
: renderMutation(ctx, spec, entity, fromFile);
|
|
93
|
+
}
|