create-warlock 5.34.3 → 5.35.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/CHANGELOG.md +22 -0
- package/README.md +8 -0
- package/esm/commands/create-new-app/index.mjs +4 -3
- package/esm/commands/create-new-app/index.mjs.map +1 -1
- package/esm/commands/create-warlock-app/index.mjs +7 -1
- package/esm/commands/create-warlock-app/index.mjs.map +1 -1
- package/esm/features/features-map.mjs +8 -2
- package/esm/features/features-map.mjs.map +1 -1
- package/esm/helpers/app.mjs +1 -0
- package/esm/helpers/app.mjs.map +1 -1
- package/esm/prompts/ask-stack.mjs +1 -1
- package/esm/prompts/ask-stack.mjs.map +1 -1
- package/llms-full.txt +58 -15
- package/package.json +2 -2
- package/skills/SKILL.md +58 -15
- package/templates/design-system/SOURCES.md +23 -0
- package/templates/design-system/package.json +14 -0
- package/templates/design-system/src/web/components/ui/LICENSE.md +21 -0
- package/templates/design-system/src/web/components/ui/button.tsx +61 -0
- package/templates/design-system/src/web/components/ui/calendar.tsx +178 -0
- package/templates/design-system/src/web/components/ui/checkbox.tsx +28 -0
- package/templates/design-system/src/web/components/ui/command.tsx +161 -0
- package/templates/design-system/src/web/components/ui/dialog.tsx +144 -0
- package/templates/design-system/src/web/components/ui/input.tsx +20 -0
- package/templates/design-system/src/web/components/ui/label.tsx +20 -0
- package/templates/design-system/src/web/components/ui/popover.tsx +73 -0
- package/templates/design-system/src/web/components/ui/radio-group.tsx +44 -0
- package/templates/design-system/src/web/components/ui/switch.tsx +34 -0
- package/templates/design-system/src/web/components/ui/textarea.tsx +17 -0
- package/templates/design-system/src/web/design-system/components/checkbox-input.tsx +19 -0
- package/templates/design-system/src/web/design-system/components/date-picker-input.tsx +59 -0
- package/templates/design-system/src/web/design-system/components/date-time-picker-input.tsx +67 -0
- package/templates/design-system/src/web/design-system/components/email-input.tsx +22 -0
- package/templates/design-system/src/web/design-system/components/field-frame.tsx +36 -0
- package/templates/design-system/src/web/design-system/components/file-input.tsx +24 -0
- package/templates/design-system/src/web/design-system/components/multi-select-input.tsx +94 -0
- package/templates/design-system/src/web/design-system/components/password-input.tsx +22 -0
- package/templates/design-system/src/web/design-system/components/radio-input.tsx +41 -0
- package/templates/design-system/src/web/design-system/components/select-input.tsx +94 -0
- package/templates/design-system/src/web/design-system/components/submit-button.tsx +20 -0
- package/templates/design-system/src/web/design-system/components/switch-input.tsx +19 -0
- package/templates/design-system/src/web/design-system/components/text-input.tsx +22 -0
- package/templates/design-system/src/web/design-system/components/textarea-input.tsx +22 -0
- package/templates/design-system/src/web/design-system/components/time-picker-input.tsx +22 -0
- package/templates/design-system/src/web/design-system/hooks/use-calendar-day-button.ts +9 -0
- package/templates/design-system/src/web/design-system/hooks/use-control.ts +29 -0
- package/templates/design-system/src/web/design-system/hooks/use-date-control.ts +42 -0
- package/templates/design-system/src/web/design-system/hooks/use-file-control.ts +24 -0
- package/templates/design-system/src/web/design-system/hooks/use-select-control.ts +78 -0
- package/templates/design-system/src/web/design-system/hooks/use-submit-control.ts +13 -0
- package/templates/design-system/src/web/design-system/types/control-props.ts +46 -0
- package/templates/warlock/e2e/home.e2e.ts +19 -0
- package/templates/warlock/package.json +1 -1
- package/templates/warlock/skills/api-design/SKILL.md +72 -9
- package/templates/warlock/skills/testing-strategy/SKILL.md +46 -5
- package/templates/warlock/skills/web-code-standards/SKILL.md +38 -0
- package/templates/warlock/src/app/auth/controllers/forgot-password.controller.ts +6 -0
- package/templates/warlock/src/app/auth/controllers/login.controller.ts +17 -0
- package/templates/warlock/src/app/auth/controllers/logout-all.controller.ts +6 -0
- package/templates/warlock/src/app/auth/controllers/logout.controller.ts +6 -0
- package/templates/warlock/src/app/auth/controllers/me.controller.ts +7 -0
- package/templates/warlock/src/app/auth/controllers/refresh-token.controller.ts +12 -0
- package/templates/warlock/src/app/auth/controllers/reset-password.controller.ts +6 -0
- package/templates/warlock/src/app/home/controllers/home-page.controller.ts +4 -0
- package/templates/warlock/src/app/posts/controllers/update-post.controller.ts +1 -0
- package/templates/warlock/src/app/shared/utils/format-http-error-response.ts +13 -1
- package/templates/warlock/src/app/users/controllers/create-new-user.controller.ts +1 -0
- package/templates/warlock/warlock.config.ts +1 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { FormControlProps } from "@mongez/react-form";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
export type ControlProps = Pick<
|
|
5
|
+
FormControlProps,
|
|
6
|
+
| "name"
|
|
7
|
+
| "rules"
|
|
8
|
+
| "schema"
|
|
9
|
+
| "validate"
|
|
10
|
+
| "validateOn"
|
|
11
|
+
| "onChange"
|
|
12
|
+
| "onError"
|
|
13
|
+
| "errors"
|
|
14
|
+
| "errorKeys"
|
|
15
|
+
| "disabled"
|
|
16
|
+
| "readOnly"
|
|
17
|
+
| "required"
|
|
18
|
+
> & {
|
|
19
|
+
id: string;
|
|
20
|
+
label: ReactNode;
|
|
21
|
+
hint?: ReactNode;
|
|
22
|
+
error?: ReactNode;
|
|
23
|
+
value?: unknown;
|
|
24
|
+
defaultValue?: unknown;
|
|
25
|
+
placeholder?: string;
|
|
26
|
+
className?: string;
|
|
27
|
+
dir?: "ltr" | "rtl";
|
|
28
|
+
autoComplete?: string;
|
|
29
|
+
rows?: number;
|
|
30
|
+
accept?: string;
|
|
31
|
+
multiple?: boolean;
|
|
32
|
+
};
|
|
33
|
+
export type Choice = { value: string; label: string; disabled?: boolean };
|
|
34
|
+
export type SelectProps = ControlProps & {
|
|
35
|
+
options: readonly Choice[];
|
|
36
|
+
searchLabel?: string;
|
|
37
|
+
emptyLabel?: string;
|
|
38
|
+
clearLabel?: string;
|
|
39
|
+
};
|
|
40
|
+
export type RadioProps = ControlProps & { options: readonly Choice[] };
|
|
41
|
+
export type CheckProps = ControlProps & { checked?: boolean; defaultChecked?: boolean };
|
|
42
|
+
export type DateProps = ControlProps & {
|
|
43
|
+
clearLabel?: string;
|
|
44
|
+
closeLabel?: string;
|
|
45
|
+
timeLabel?: string;
|
|
46
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineE2E, defaultViewports } from "@warlock.js/core/e2e";
|
|
2
|
+
|
|
3
|
+
export default defineE2E({
|
|
4
|
+
app: {
|
|
5
|
+
build: [process.execPath, "node_modules/@warlock.js/core/bin/warlock.js", "build"],
|
|
6
|
+
start: [process.execPath, "dist/app.js"],
|
|
7
|
+
exclude: ["storage", "data"],
|
|
8
|
+
},
|
|
9
|
+
run: async ({ page }) => {
|
|
10
|
+
for (const viewport of defaultViewports) {
|
|
11
|
+
await page.setViewport(viewport);
|
|
12
|
+
await page.goto("/");
|
|
13
|
+
await page.waitFor({ selector: "main" });
|
|
14
|
+
await page.assertAccessible();
|
|
15
|
+
await page.assertViewport();
|
|
16
|
+
await page.screenshot("home");
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
});
|
|
@@ -20,17 +20,26 @@ The project uses **three envelope shapes**, one per response kind. Each success
|
|
|
20
20
|
|
|
21
21
|
### 1.1 List endpoints — `{ <pluralResourceName>, pagination }`
|
|
22
22
|
|
|
23
|
-
The list lands under the **plural name of the module's resource** — `users`, `aiModels`, `orders` — paired with `pagination`. The repository returns the framework's `{ data, pagination }`;
|
|
23
|
+
The list lands under the **plural name of the module's resource** — `users`, `aiModels`, `orders` — paired with `pagination`. The repository returns the framework's `{ data, pagination }`; `listEndpoint` re-keys `data` to the plural name and enforces bounded page controls.
|
|
24
24
|
|
|
25
25
|
```typescript
|
|
26
26
|
// ✅ list-users.controller.ts
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
import { listEndpoint, type Request, type RequestHandler } from "@warlock.js/core";
|
|
28
|
+
import type { ListUsersQuery } from "../schema/list-users.schema";
|
|
29
|
+
import { usersRepository } from "../repositories/users.repository";
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
export const listUsersController: RequestHandler<Request<ListUsersQuery>> = async ({
|
|
32
|
+
request,
|
|
33
|
+
response,
|
|
34
|
+
}) => {
|
|
35
|
+
return response.success(
|
|
36
|
+
await listEndpoint(request, (options) => usersRepository.list(options), {
|
|
37
|
+
pluralKey: "users",
|
|
38
|
+
maxLimit: 100,
|
|
39
|
+
orderBy: ["createdAt"],
|
|
40
|
+
filters: ["status"],
|
|
41
|
+
}),
|
|
42
|
+
);
|
|
34
43
|
};
|
|
35
44
|
```
|
|
36
45
|
|
|
@@ -113,7 +122,7 @@ Every JSON error response has this shape, with optional `details` for safe field
|
|
|
113
122
|
}
|
|
114
123
|
```
|
|
115
124
|
|
|
116
|
-
Use the matching status helper or throw a framework error from the service. The starter's `http.errorResponse` formatter normalizes helper bodies and framework-generated JSON errors, including unmatched-route 404, validation 422, rate-limit 429, and 500. It removes stacks and masks
|
|
125
|
+
Use the matching status helper or throw a framework error from the service. The starter's `http.errorResponse` formatter normalizes helper bodies and framework-generated JSON errors, including unmatched-route 404, validation 422, rate-limit 429, and 500. It removes stacks and masks ordinary 5xx messages. Typed `AppError` messages and details are explicitly public and are preserved, including for 5xx responses. `requestId` is the real Warlock `request.id`, read from the framework's correlation header at the transport boundary; a rejection before Warlock creates a Request uses Fastify's request ID and echoes that same value in the header. Do not generate a new ID inside a controller or use a user ID. See the `observability-and-resilience` skill for trace propagation.
|
|
117
126
|
|
|
118
127
|
```typescript
|
|
119
128
|
return response.notFound(); // formatter supplies { error: { code, message }, requestId }
|
|
@@ -123,7 +132,7 @@ return response.conflict({
|
|
|
123
132
|
return response.badRequest({ error: { code: "invalidRequest", message: "Invalid request." } });
|
|
124
133
|
```
|
|
125
134
|
|
|
126
|
-
`http.errorMapper` translates thrown non-`HttpError` domain errors to `{ status, message?, payload? }`; it does **not** format response bodies. It skips `HttpError` subclasses, returned validation/helper responses, and Fastify's unmatched-route and global rate-limit responses. The starter config uses it to keep unknown thrown errors opaque; `http.errorResponse` handles the final JSON shape for all these paths. HTML error pages, files, and already-started streams retain their own contracts; neither hook rewrites their bodies.
|
|
135
|
+
`http.errorMapper` translates thrown non-`HttpError` domain errors to `{ status, message?, payload? }`; it does **not** format response bodies. It skips `AppError` and `HttpError` subclasses, returned validation/helper responses, and Fastify's unmatched-route and global rate-limit responses. The starter config uses it to keep unknown thrown errors opaque; `http.errorResponse` handles the final JSON shape for all these paths. HTML error pages, files, and already-started streams retain their own contracts; neither hook rewrites their bodies.
|
|
127
136
|
|
|
128
137
|
Never send a `200 OK` with an `error` field. Never put secrets, stack traces, raw request bodies, or internal database IDs in `details`.
|
|
129
138
|
|
|
@@ -519,3 +528,57 @@ Before merging any controller / route / resource change:
|
|
|
519
528
|
- [ ] `Cache-Control` set explicitly on `GET` endpoints
|
|
520
529
|
- [ ] Versioning: additive-only by default; explicit `/vN` prefix only when a real breaking change forces it
|
|
521
530
|
- [ ] Deprecated fields announced via `Deprecation` / `Sunset` headers, not silently changed
|
|
531
|
+
|
|
532
|
+
## Framework helpers for API contracts
|
|
533
|
+
|
|
534
|
+
Throw `AppError` for new coded domain failures. Its positional constructor is
|
|
535
|
+
`(code, status, publicMessage, details?)`; status must be 400–599. Details are
|
|
536
|
+
typed and must be safe to disclose. Existing `HttpError` subclasses remain
|
|
537
|
+
separate and retain their constructors and payload behavior. Neither needs
|
|
538
|
+
`http.errorMapper`. The starter formatter reads `context.appError` natively and
|
|
539
|
+
keeps this project's `{ error: { code, message }, details?, requestId }` contract.
|
|
540
|
+
Core's unformatted default places details inside `error`; declare the actual
|
|
541
|
+
formatted shape in controller response schemas.
|
|
542
|
+
|
|
543
|
+
```ts
|
|
544
|
+
import { AppError } from "@warlock.js/core/errors";
|
|
545
|
+
|
|
546
|
+
throw new AppError("quotaExceeded", 429, "Try again later.", { retryAfter: 30 });
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
For lists, use the `listEndpoint` example in § 1.1. Type the controller as
|
|
550
|
+
`RequestHandler<Request<ListUsersQuery>>`, attach the query schema with
|
|
551
|
+
`validating: ["query"]`, and configure repository `filterBy`. The helper reads
|
|
552
|
+
only `request.validated()`, defaults to page 1 / limit 20, clamps limits to
|
|
553
|
+
`maxLimit` (100 by default), rejects unlisted sort columns, and forwards only
|
|
554
|
+
named filters as flat repository options. Both `(options) => repo.list(options)`
|
|
555
|
+
and `(options) => repo.listCached(options)` preserve the repository receiver.
|
|
556
|
+
Page pagination is forced; cursor endpoints use a separate controller.
|
|
557
|
+
|
|
558
|
+
Test the declared response contract through actual HTTP, not just generated
|
|
559
|
+
types. Keep the existing `startHttpTestServer({ port: 0 })` / `setupTest` setup
|
|
560
|
+
and import routes in the worker so its registry contains handler schemas:
|
|
561
|
+
|
|
562
|
+
```ts
|
|
563
|
+
import { it } from "vitest";
|
|
564
|
+
import { router } from "@warlock.js/core";
|
|
565
|
+
import { assertResponseContracts } from "@warlock.js/core/tests";
|
|
566
|
+
import "../routes";
|
|
567
|
+
|
|
568
|
+
it("matches user response declarations", async () => {
|
|
569
|
+
await assertResponseContracts({
|
|
570
|
+
routes: router.list().filter((route) => route.path === "/users"),
|
|
571
|
+
cases: () => [{ path: "/users?page=1&limit=10", status: 200 }],
|
|
572
|
+
requireAllStatuses: true,
|
|
573
|
+
});
|
|
574
|
+
});
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
Omit `routes` to discover all registered routes with schemas. Supply fixtures
|
|
578
|
+
for every declared status when using `requireAllStatuses`; parameterized routes
|
|
579
|
+
and methods other than GET always require fixtures. Cases support a concrete
|
|
580
|
+
`path`, `init` (headers/serialized body), and expected `status`. Use isolated
|
|
581
|
+
seed data and cleanup for mutations. Pass `client` for an authenticated client
|
|
582
|
+
or `createTestHttpServer()` instance. Checks reject undeclared statuses, missing
|
|
583
|
+
fields and type mismatches; unsupported computed/builder shapes fail explicitly.
|
|
584
|
+
See core's `test-http` topic for lifecycle, fixture and schema limitations.
|
|
@@ -21,7 +21,7 @@ This skill governs projects that have **opted into testing**. Testing is **enabl
|
|
|
21
21
|
- `package.json` has a `test` (or `test:*`) script, **or**
|
|
22
22
|
- a `vitest.config.*` / `vite.config.*` file exists at the project root.
|
|
23
23
|
|
|
24
|
-
When the `test` feature is selected at scaffold time, both land automatically (Vitest, `@vitest/coverage-v8`,
|
|
24
|
+
When the `test` feature is selected at scaffold time, both land automatically (Vitest, `@vitest/coverage-v8`, and separate unit/integration projects with a per-file runtime by default).
|
|
25
25
|
|
|
26
26
|
### 0.1 If testing is enabled, tests are mandatory — not optional
|
|
27
27
|
|
|
@@ -119,7 +119,7 @@ Each test sets up and tears down its own world. No `let users = []` at the top o
|
|
|
119
119
|
|
|
120
120
|
Use transactions that roll back, or use testcontainers with a fresh container per file. Never share rows across tests.
|
|
121
121
|
|
|
122
|
-
|
|
122
|
+
Provide the database fixture in global setup; the framework does not create testcontainers automatically. Give each worker its own database/schema before resetting rows.
|
|
123
123
|
|
|
124
124
|
### 3.4 Time and randomness
|
|
125
125
|
|
|
@@ -309,12 +309,12 @@ Co-located with the integration: `src/app/payments/clients/stripe-client.contrac
|
|
|
309
309
|
src/app/orders/services/
|
|
310
310
|
place-order.service.ts
|
|
311
311
|
place-order.service.spec.ts ← unit test, mocks repository
|
|
312
|
-
place-order.service.
|
|
312
|
+
place-order.service.test.ts ← integration, real DB
|
|
313
313
|
```
|
|
314
314
|
|
|
315
315
|
- `.spec.ts` — default suffix; covers unit tests
|
|
316
|
-
- `.
|
|
317
|
-
-
|
|
316
|
+
- `.test.ts` or a spec inside `tests/` — integration, real I/O (DB, queue)
|
|
317
|
+
- Full-system journeys also use `.test.ts` in the integration project.
|
|
318
318
|
|
|
319
319
|
Tests live next to the file under test, always. Never in a separate `__tests__/` or `test/` folder — colocation makes the link obvious and survives refactors.
|
|
320
320
|
|
|
@@ -338,3 +338,44 @@ Before merging a change that should include tests (testing enabled — see § 0)
|
|
|
338
338
|
- [ ] No `.only`, no `.skip` without a tracking ticket
|
|
339
339
|
- [ ] Snapshots reviewed line-by-line if updated
|
|
340
340
|
- [ ] `npm run test --run` passes; `npm run test:coverage` doesn't drop the floor
|
|
341
|
+
|
|
342
|
+
## Runtime cost and release gates
|
|
343
|
+
|
|
344
|
+
With `--features=test`, create-warlock delegates to `warlock add test` to generate
|
|
345
|
+
`vite.config.ts`: the `unit` project runs `src/**/*.spec.ts` outside `tests/`
|
|
346
|
+
without framework or HTTP setup; `integration` runs `*.test.ts` and specs under
|
|
347
|
+
`tests/` with `src/test-setup.ts`, global HTTP setup and `src/test-runner.ts`.
|
|
348
|
+
Prefer `npx vitest run --project unit` during the pure service loop.
|
|
349
|
+
|
|
350
|
+
When a unit needs config values, call
|
|
351
|
+
`setupTest({ mode: "unit", importModule: (file) => import(file) })` from
|
|
352
|
+
`@warlock.js/core/tests` and pair it with `afterAll(teardownTest)`. Config loads
|
|
353
|
+
once per module graph, without hooks, connectors, native loader or fakes.
|
|
354
|
+
Most unit specs need no setup call.
|
|
355
|
+
|
|
356
|
+
Integration setup defaults to `tests.lifetime: "file"`. For reuse, set
|
|
357
|
+
`lifetime: "worker"` in `src/config/tests.ts`, change the integration project to
|
|
358
|
+
`pool: "forks", isolate: false`, keep the generated runner, and use `vitest run`.
|
|
359
|
+
Watch, threads and isolated graphs are refused. Keep `afterAll(teardownTest)`:
|
|
360
|
+
it marks file boundaries while the runner closes connectors at worker exit.
|
|
361
|
+
Set `tests.resetBetweenFiles` to an async application-owned cleanup callback;
|
|
362
|
+
it runs before every file, including the first, after selected fakes, clock and
|
|
363
|
+
IDs reset. Each worker needs a separate database/schema. Module singletons and
|
|
364
|
+
mocks survive; reset application state and restore mocks explicitly. Worker
|
|
365
|
+
storage is private and does not mirror the shared HTTP server's fake files.
|
|
366
|
+
|
|
367
|
+
Use `tests.profileSetup: true` or
|
|
368
|
+
`WARLOCK_TEST_PROFILE=1 npx vitest run --project integration` for file timings and
|
|
369
|
+
summaries per worker. Pass `file: expect.getState().testPath` in the setup call.
|
|
370
|
+
`getTestRuntimeStats()` exposes bootstrap and file counts. Generated configs use
|
|
371
|
+
`recommendedTestWorkers()` from `@warlock.js/core/vite` to respect visible cgroup
|
|
372
|
+
v2 CPU quotas; reduce `maxWorkers` further for database or memory contention.
|
|
373
|
+
|
|
374
|
+
Release gate:
|
|
375
|
+
|
|
376
|
+
```bash
|
|
377
|
+
npx warlock test --changed --since "$LAST_RELEASE_REF"
|
|
378
|
+
npx vitest run --project integration
|
|
379
|
+
# Nightly:
|
|
380
|
+
npx vitest run
|
|
381
|
+
```
|
|
@@ -229,6 +229,11 @@ Avoid copying props into hook state without a plan for refresh and client
|
|
|
229
229
|
navigation. Avoid setting another state variable inside a state-updater
|
|
230
230
|
callback. Prefer deriving display values from one authoritative state.
|
|
231
231
|
|
|
232
|
+
**State ownership.** Loader data is server truth. UI interaction state (open,
|
|
233
|
+
selection, draft input) lives in the component's own hook. Shared client state
|
|
234
|
+
goes only through a named store module, never a global variable or a copy
|
|
235
|
+
spread across components. Never copy props into state without a refresh plan.
|
|
236
|
+
|
|
232
237
|
Effects own their subscriptions and timers: unsubscribe, cancel, or ignore
|
|
233
238
|
stale async work on unmount or entity change. A failed script/socket load
|
|
234
239
|
must not leave a permanently rejected shared promise or silently disable a
|
|
@@ -326,6 +331,39 @@ feature hook. With generated route declarations and a handler's `responseSchema`
|
|
|
326
331
|
`@warlock.js/web` reads a declared status body. Do not replace this inference
|
|
327
332
|
with casts. Server endpoints must still validate input independently.
|
|
328
333
|
|
|
334
|
+
### Use the shared design-system controls
|
|
335
|
+
|
|
336
|
+
For Tailwind + shadcn projects, use the controls in `src/web/design-system/`:
|
|
337
|
+
TextInput, EmailInput, PasswordInput, TextareaInput, SelectInput (search),
|
|
338
|
+
MultiSelectInput, DatePickerInput, TimePickerInput, DateTimePickerInput,
|
|
339
|
+
CheckboxInput, RadioInput, SwitchInput, FileInput, and SubmitButton.
|
|
340
|
+
Import each component directly, for example
|
|
341
|
+
`import TextInput from "web/design-system/components/text-input";` — no barrels.
|
|
342
|
+
Use `Form` from `@mongez/react-form` directly. Every control requires a stable
|
|
343
|
+
explicit `id`, `name`, and visible `label`, and accepts `hint`, `error`,
|
|
344
|
+
Mongez `schema`/`rules`, and `validateOn`. Put behavior in design-system hooks
|
|
345
|
+
and props in design-system types. See the Web `build-accessible-controls`
|
|
346
|
+
topic for keyboard/focus, RTL, reset and calendar-date semantics.
|
|
347
|
+
|
|
348
|
+
```tsx
|
|
349
|
+
import { Form } from "@mongez/react-form";
|
|
350
|
+
import TextInput from "web/design-system/components/text-input";
|
|
351
|
+
import SubmitButton from "web/design-system/components/submit-button";
|
|
352
|
+
|
|
353
|
+
export default function ProfileForm() {
|
|
354
|
+
return (
|
|
355
|
+
<Form>
|
|
356
|
+
<TextInput id="profile-name" name="name" label="Name" hint="Display name" />
|
|
357
|
+
<SubmitButton id="profile-save" name="save" label="Save" />
|
|
358
|
+
</Form>
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
The full-stack preset and shadcn selection scaffold these controls. Existing
|
|
364
|
+
Tailwind apps can run `npx warlock add design-system`; install Tailwind first
|
|
365
|
+
with `npx warlock add tailwind` when missing. Existing files are preserved.
|
|
366
|
+
|
|
329
367
|
### Build custom controls with the project's UI kit
|
|
330
368
|
|
|
331
369
|
Do not render native `<select>`, `<input type="checkbox">`,
|
|
@@ -23,3 +23,9 @@ forgotPasswordController.validation = {
|
|
|
23
23
|
email: v.email(),
|
|
24
24
|
}),
|
|
25
25
|
};
|
|
26
|
+
|
|
27
|
+
forgotPasswordController.responseSchema = {
|
|
28
|
+
200: { body: { message: "string" } },
|
|
29
|
+
422: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
30
|
+
500: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
31
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Request, type RequestHandler } from "@warlock.js/core";
|
|
2
|
+
import { UserResource } from "app/users/resources/user.resource";
|
|
2
3
|
import { loginSchema, type LoginSchema } from "../schema/login.schema";
|
|
3
4
|
import { loginService } from "../services/auth.service";
|
|
4
5
|
|
|
@@ -23,3 +24,19 @@ loginController.description = "User Login";
|
|
|
23
24
|
loginController.validation = {
|
|
24
25
|
schema: loginSchema,
|
|
25
26
|
};
|
|
27
|
+
|
|
28
|
+
// The starter disables refresh tokens; add tokens.refreshToken when enabling them.
|
|
29
|
+
loginController.responseSchema = {
|
|
30
|
+
200: {
|
|
31
|
+
body: {
|
|
32
|
+
user: UserResource,
|
|
33
|
+
tokens: {
|
|
34
|
+
accessToken: { token: "string", expiresAt: "string" },
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
401: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
39
|
+
403: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
40
|
+
422: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
41
|
+
500: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
42
|
+
};
|
|
@@ -17,3 +17,9 @@ export const logoutAllController: GuardedRequestHandler = async ({ request, resp
|
|
|
17
17
|
};
|
|
18
18
|
|
|
19
19
|
logoutAllController.description = "Logout from all devices";
|
|
20
|
+
|
|
21
|
+
logoutAllController.responseSchema = {
|
|
22
|
+
200: { body: { message: "string" } },
|
|
23
|
+
401: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
24
|
+
500: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
25
|
+
};
|
|
@@ -19,3 +19,9 @@ export const logoutController: GuardedRequestHandler = async ({ request, respons
|
|
|
19
19
|
};
|
|
20
20
|
|
|
21
21
|
logoutController.description = "User Logout";
|
|
22
|
+
|
|
23
|
+
logoutController.responseSchema = {
|
|
24
|
+
200: { body: { message: "string" } },
|
|
25
|
+
401: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
26
|
+
500: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
27
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type GuardedRequestHandler } from "app/auth/requests/guarded.request";
|
|
2
|
+
import { UserResource } from "app/users/resources/user.resource";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Get current user controller
|
|
@@ -16,3 +17,9 @@ export const meController: GuardedRequestHandler = async ({ request, response })
|
|
|
16
17
|
};
|
|
17
18
|
|
|
18
19
|
meController.description = "Get Current User";
|
|
20
|
+
|
|
21
|
+
meController.responseSchema = {
|
|
22
|
+
200: { body: { user: UserResource } },
|
|
23
|
+
401: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
24
|
+
500: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
25
|
+
};
|
|
@@ -24,3 +24,15 @@ refreshTokenController.validation = {
|
|
|
24
24
|
refreshToken: v.string(),
|
|
25
25
|
}),
|
|
26
26
|
};
|
|
27
|
+
|
|
28
|
+
refreshTokenController.responseSchema = {
|
|
29
|
+
200: {
|
|
30
|
+
body: {
|
|
31
|
+
accessToken: { token: "string", expiresAt: "string" },
|
|
32
|
+
refreshToken: { token: "string", expiresAt: "string" },
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
401: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
36
|
+
422: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
37
|
+
500: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
38
|
+
};
|
|
@@ -21,3 +21,9 @@ resetPasswordController.description = "Reset password with OTP";
|
|
|
21
21
|
resetPasswordController.validation = {
|
|
22
22
|
schema: resetPasswordSchema,
|
|
23
23
|
};
|
|
24
|
+
|
|
25
|
+
resetPasswordController.responseSchema = {
|
|
26
|
+
200: { body: { message: "string" } },
|
|
27
|
+
422: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
28
|
+
500: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
29
|
+
};
|
|
@@ -10,3 +10,7 @@ export const homePageController: RequestHandler = async ({ response }) => {
|
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
homePageController.description = "Welcome Home Page";
|
|
13
|
+
|
|
14
|
+
homePageController.responseSchema = {
|
|
15
|
+
200: { body: { message: "string", version: "string", docs: "string" } },
|
|
16
|
+
};
|
|
@@ -32,4 +32,5 @@ updatePostController.responseSchema = {
|
|
|
32
32
|
200: { body: { post: PostResource } },
|
|
33
33
|
422: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
34
34
|
500: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
35
|
+
404: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
35
36
|
};
|
|
@@ -11,7 +11,19 @@ const statusCodes: Record<number, string> = {
|
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
/** Convert framework JSON errors to the project's public error contract. */
|
|
14
|
-
export function formatHttpErrorResponse({
|
|
14
|
+
export function formatHttpErrorResponse({
|
|
15
|
+
status,
|
|
16
|
+
body,
|
|
17
|
+
requestId,
|
|
18
|
+
appError,
|
|
19
|
+
}: HttpErrorResponseContext) {
|
|
20
|
+
if (appError) {
|
|
21
|
+
return {
|
|
22
|
+
error: { code: appError.code, message: appError.publicMessage },
|
|
23
|
+
...(appError.details === undefined ? {} : { details: appError.details }),
|
|
24
|
+
requestId,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
15
27
|
const source = isRecord(body) ? body : {};
|
|
16
28
|
const error = isRecord(source.error) ? source.error : {};
|
|
17
29
|
const serverFault = status >= 500;
|
|
@@ -35,4 +35,5 @@ createNewUserController.responseSchema = {
|
|
|
35
35
|
201: { body: { message: "string", user: UserResource } },
|
|
36
36
|
422: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
37
37
|
500: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
38
|
+
400: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
38
39
|
};
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
import { defineConfig } from "@warlock.js/core";
|
|
7
7
|
|
|
8
8
|
export default defineConfig({
|
|
9
|
+
build: { budgets: { page: { js: "150kb", css: "50kb" }, mode: "error" } },
|
|
9
10
|
cli: {
|
|
10
11
|
commands: [registerJWTSecretGeneratorCommand(), registerAuthCleanupCommand()],
|
|
11
12
|
},
|