create-warlock 5.34.1 → 5.34.3
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 +16 -0
- package/llms-full.txt +21 -28
- package/llms.txt +1 -1
- package/package.json +2 -2
- package/skills/{create-a-warlock-project/SKILL.md → SKILL.md} +20 -27
- package/templates/toolchains/oxc/.oxlintrc.json +3 -1
- package/templates/warlock/.oxlintrc.json +3 -1
- package/templates/warlock/AGENTS.md +10 -10
- package/templates/warlock/skills/api-design/SKILL.md +110 -50
- package/templates/warlock/skills/code-standards/SKILL.md +48 -39
- package/templates/warlock/skills/data-and-persistence/SKILL.md +72 -57
- package/templates/warlock/skills/git-workflow/SKILL.md +9 -9
- package/templates/warlock/skills/module-boundaries/SKILL.md +2 -2
- package/templates/warlock/skills/observability-and-resilience/SKILL.md +8 -6
- package/templates/warlock/skills/security-baseline/SKILL.md +23 -22
- package/templates/warlock/skills/testing-strategy/SKILL.md +7 -7
- package/templates/warlock/skills/web-code-standards/SKILL.md +157 -60
- package/templates/warlock/src/app/auth/models/otp/otp.model.ts +9 -3
- package/templates/warlock/src/app/auth/services/otp.service.ts +18 -6
- package/templates/warlock/src/app/contact/controllers/contact.controller.ts +6 -0
- package/templates/warlock/src/app/locale/controllers/locale.controller.ts +6 -0
- package/templates/warlock/src/app/posts/controllers/create-new-post.controller.ts +7 -0
- package/templates/warlock/src/app/posts/controllers/update-post.controller.ts +14 -9
- package/templates/warlock/src/app/posts/models/post/migrations/09-01-2026_02-07-51-post.migration.ts +1 -0
- package/templates/warlock/src/app/posts/models/post/post.model.ts +6 -0
- package/templates/warlock/src/app/posts/resources/post.resource.ts +1 -1
- package/templates/warlock/src/app/posts/schema/update-post.schema.ts +1 -0
- package/templates/warlock/src/app/shared/utils/format-http-error-response.ts +41 -0
- package/templates/warlock/src/app/users/commands/hello-world.command.ts +2 -1
- package/templates/warlock/src/app/users/controllers/create-new-user.controller.ts +12 -1
- package/templates/warlock/src/app/users/controllers/list-users.controller.ts +24 -1
- package/templates/warlock/src/app/users/models/user/migrations/11-12-2025_23-58-03-user.migration.ts +1 -0
- package/templates/warlock/src/app/users/models/user/user.model.ts +5 -0
- package/templates/warlock/src/app/users/repositories/users.repository.ts +2 -2
- package/templates/warlock/src/app/users/resources/user.resource.ts +1 -1
- package/templates/warlock/src/app/users/schema/list-users.schema.ts +1 -1
- package/templates/warlock/src/config/http.ts +6 -0
- package/templates/warlock/src/web/home/components/contact-field.tsx +18 -0
- package/templates/warlock/src/web/home/components/contact-section.tsx +5 -29
- package/templates/warlock/src/web/home/components/contact-submit-button.tsx +16 -0
- package/templates/warlock/src/web/home/hooks/use-contact-field.ts +9 -0
- package/templates/warlock/src/web/home/hooks/use-contact-form.ts +5 -8
- package/templates/warlock/src/web/home/hooks/use-contact-locale.ts +34 -0
- package/templates/warlock/src/web/home/hooks/use-contact-submit-button.ts +6 -0
- package/templates/warlock/src/web/home/index.page.tsx +1 -1
- package/templates/warlock/src/web/home/types/contact-controls.type.ts +13 -0
- package/templates/warlock/src/web/shared/utils/set-form-errors.ts +3 -1
- package/templates/warlock/src/web/home/components/contact-form-controls.tsx +0 -42
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: code-standards
|
|
3
|
-
description:
|
|
3
|
+
description: "Shared TypeScript rules: contracts versus data types, function services, file names, app barrels, errors, async work, logging and Oxc enforcement. Use when writing or reviewing TypeScript. Use web-code-standards instead for web components, hooks and setup boundaries."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Code Standards
|
|
@@ -8,7 +8,7 @@ description: 'Project-level TypeScript code standards for apps built on Warlock.
|
|
|
8
8
|
**Status:** Stable
|
|
9
9
|
**Applies to:** All TypeScript in this project — services, models, resources, modules, app code, scripts.
|
|
10
10
|
|
|
11
|
-
> **Sub-agent rule:** Before writing any code, read this file end-to-end and apply every rule. When spawning a sub-agent that will write code, prepend: _"First, read `
|
|
11
|
+
> **Sub-agent rule:** Before writing any code, read this file end-to-end and apply every rule. When spawning a sub-agent that will write code, prepend: _"First, read the `code-standards` skill and apply every rule."_
|
|
12
12
|
|
|
13
13
|
The bar is **senior-level clean code**. Not "it compiles" — code that the next person on this codebase can read in one pass.
|
|
14
14
|
|
|
@@ -18,6 +18,7 @@ The bar is **senior-level clean code**. Not "it compiles" — code that the next
|
|
|
18
18
|
|
|
19
19
|
### 1.1 `interface` for contracts, `type` for data
|
|
20
20
|
|
|
21
|
+
- Declaration-merging targets (for example framework module augmentations) also require `interface`.
|
|
21
22
|
- `interface` describes a **contract** — anything with methods, anything a class implements, anything that holds actions.
|
|
22
23
|
- `type` describes a **data shape** — plain objects, unions, tuples, mapped types, discriminated unions.
|
|
23
24
|
|
|
@@ -49,7 +50,7 @@ Never omit `public` / `private` / `protected`. Explicit is faster to read than i
|
|
|
49
50
|
|
|
50
51
|
```typescript
|
|
51
52
|
// ✅
|
|
52
|
-
export class
|
|
53
|
+
export class Cart {
|
|
53
54
|
public readonly currency: string;
|
|
54
55
|
private items: CartItem[] = [];
|
|
55
56
|
public add(item: CartItem) {
|
|
@@ -58,7 +59,7 @@ export class CartService {
|
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
// ❌
|
|
61
|
-
export class
|
|
62
|
+
export class Cart {
|
|
62
63
|
readonly currency: string; // missing public
|
|
63
64
|
items: CartItem[] = []; // missing private
|
|
64
65
|
add(item: CartItem) {} // missing public
|
|
@@ -160,7 +161,7 @@ import { formatUserName } from "app/users/utils";
|
|
|
160
161
|
import { listUsersService, formatUserName, User } from "app/users";
|
|
161
162
|
```
|
|
162
163
|
|
|
163
|
-
The module-root barrel is the one that causes circular imports and tree-shaking misses.
|
|
164
|
+
The module-root barrel is the one that causes circular imports and tree-shaking misses. This recommendation applies to server app modules under `src/app/**`. Web feature folders under `src/web/**` have no barrels: imports point to the exact component or hook file so client bundles stay lean and server-only code is never pulled through a barrel. See the `web-code-standards` skill.
|
|
164
165
|
|
|
165
166
|
### 2.3 No file extension in import paths
|
|
166
167
|
|
|
@@ -170,14 +171,24 @@ Always bare: `from "./order.service"`, never `from "./order.service.ts"` or `.js
|
|
|
170
171
|
|
|
171
172
|
## 3. File naming
|
|
172
173
|
|
|
173
|
-
| Suffix
|
|
174
|
-
|
|
|
175
|
-
| `.contract.ts`
|
|
176
|
-
| `.type.ts`
|
|
177
|
-
| `.service.ts`
|
|
178
|
-
| `.
|
|
179
|
-
| `.
|
|
180
|
-
| `.
|
|
174
|
+
| Suffix | Purpose | Example |
|
|
175
|
+
| ------------------------- | ---------------------------------------------- | --------------------------------------- |
|
|
176
|
+
| `.contract.ts` | Interface contract | `payment.contract.ts` |
|
|
177
|
+
| `.type.ts` | Data types | `order.type.ts` |
|
|
178
|
+
| `.service.ts` | Named service function | `list-users.service.ts` |
|
|
179
|
+
| `.controller.ts` | HTTP handler function | `me.controller.ts` |
|
|
180
|
+
| `.repository.ts` | Framework repository subclass and instance | `users.repository.ts` |
|
|
181
|
+
| `.schema.ts` | Seal schema and inferred type | `create-user.schema.ts` |
|
|
182
|
+
| `.error.ts` | Framework error subclass | `order-conflict.error.ts` |
|
|
183
|
+
| `.middleware.ts` | Request middleware function | `require-owner.middleware.ts` |
|
|
184
|
+
| `.model.ts` | Cascade model subclass | `user.model.ts` |
|
|
185
|
+
| `.migration.ts` | Timestamped model migration | `20-10-2026_12-00-00-user.migration.ts` |
|
|
186
|
+
| `.seeder.ts` / `.seed.ts` | Seeder (starter uses `.seed.ts`) | `users.seed.ts` |
|
|
187
|
+
| `.page.tsx` | Web page component | `index.page.tsx` |
|
|
188
|
+
| `.setup.ts` | Page, layout or root policy and server exports | `index.setup.ts` |
|
|
189
|
+
| `use-*.ts` | Named feature hook | `use-contact-form.ts` |
|
|
190
|
+
| `.resource.ts` | Output mapper | `user.resource.ts` |
|
|
191
|
+
| `.spec.ts` / `.test.ts` | Co-located test | `cart.service.spec.ts` |
|
|
181
192
|
|
|
182
193
|
**One primary export per file.** If a `.contract.ts` also defines closely-related data types used only by that contract, that's fine — but the primary export is the interface.
|
|
183
194
|
|
|
@@ -192,8 +203,8 @@ If the file's description needs an "and", split it. The same goes for classes an
|
|
|
192
203
|
**File-level SRP.** If a file has more than one top-level helper that isn't a direct implementation detail of the main export, extract helpers into a `utils/` folder — one helper per file. The main file imports from leaf paths.
|
|
193
204
|
|
|
194
205
|
```
|
|
195
|
-
❌ cart.service.ts holds
|
|
196
|
-
✅ cart.service.ts holds
|
|
206
|
+
❌ cart.service.ts holds cartService + calculateTax + formatLineItem + applyDiscount
|
|
207
|
+
✅ cart.service.ts holds cartService only
|
|
197
208
|
utils/calculate-tax.ts
|
|
198
209
|
utils/format-line-item.ts
|
|
199
210
|
utils/apply-discount.ts
|
|
@@ -201,7 +212,7 @@ If the file's description needs an "and", split it. The same goes for classes an
|
|
|
201
212
|
|
|
202
213
|
### 4.2 Public API is functional, classes are an implementation detail
|
|
203
214
|
|
|
204
|
-
|
|
215
|
+
This rule governs app code: services are named functions such as `listUsersService(input)`. A stateful app primitive may use a **factory function returning a plain object**. Framework base classes are extended as their package skills require: models, repositories, connectors, and errors remain subclasses; resources returned by `defineResource` are constructed with `new`.
|
|
205
216
|
|
|
206
217
|
Reach for a `class` in exactly three cases:
|
|
207
218
|
|
|
@@ -304,7 +315,7 @@ for (const item of items) {
|
|
|
304
315
|
// ✅ the WHY isn't obvious from the code
|
|
305
316
|
// Stripe returns idempotency_key on retry but not on first call —
|
|
306
317
|
// fall back to the order id so downstream dedup still works.
|
|
307
|
-
const key = response.
|
|
318
|
+
const key = response.idempotencyKey ?? order.id;
|
|
308
319
|
```
|
|
309
320
|
|
|
310
321
|
### 4.7 Pure helpers live outside classes
|
|
@@ -323,20 +334,19 @@ Define the class once, then throw it from the call site by name — the type its
|
|
|
323
334
|
|
|
324
335
|
```typescript
|
|
325
336
|
// ✅ define
|
|
326
|
-
import {
|
|
337
|
+
import { ConflictError } from "@warlock.js/core";
|
|
327
338
|
|
|
328
|
-
export class CartLockedError extends
|
|
329
|
-
public readonly statusCode = 409;
|
|
339
|
+
export class CartLockedError extends ConflictError {
|
|
330
340
|
public constructor(public readonly cartId: string) {
|
|
331
341
|
super(`Cart ${cartId} is locked`);
|
|
332
342
|
}
|
|
333
343
|
}
|
|
334
344
|
|
|
335
|
-
// ✅ throw — the class
|
|
345
|
+
// ✅ throw — the class identifies the exception; the HTTP formatter supplies the wire code
|
|
336
346
|
import { CartLockedError } from "app/cart/errors/cart-locked.error";
|
|
337
347
|
|
|
338
348
|
if (cart.isLocked) {
|
|
339
|
-
throw new CartLockedError(cart.
|
|
349
|
+
throw new CartLockedError(cart.get<string>("publicId"));
|
|
340
350
|
}
|
|
341
351
|
|
|
342
352
|
// ❌ bare Error — no status code, no type to catch, no classification
|
|
@@ -455,14 +465,14 @@ log.info("order-placed", { orderId, userId, total });
|
|
|
455
465
|
**Other rules:**
|
|
456
466
|
|
|
457
467
|
- Log at boundaries (request in, request out, error caught) — not every internal step.
|
|
458
|
-
- **Redact at the source.** Tokens, passwords, credit card numbers, personal identifiers never appear in log context. Configure redaction once via `log.configure({ redact: ... })` — see `@warlock.js/logger
|
|
468
|
+
- **Redact at the source.** Tokens, passwords, credit card numbers, personal identifiers never appear in log context. Configure redaction once via `log.configure({ redact: ... })` — see the `@warlock.js/logger` skill's `redact-sensitive-log-fields` topic.
|
|
459
469
|
- `module` is the area of the app (`"orders"`, `"auth"`, `"payments"`), `action` is the verb-noun event (`"order-placed"`, `"login-failed"`, `"refund-issued"`). Both are searchable indexes — keep them consistent across the codebase.
|
|
460
470
|
|
|
461
471
|
---
|
|
462
472
|
|
|
463
473
|
## 9. Non-negotiable defaults
|
|
464
474
|
|
|
465
|
-
Senior-team baseline rules. Each one prevents a real class of bug or incident. Each one is short because each one is unconditional. Deeper coverage lives in sibling skills — `
|
|
475
|
+
Senior-team baseline rules. Each one prevents a real class of bug or incident. Each one is short because each one is unconditional. Deeper coverage lives in sibling skills — the `security-baseline` skill, the `data-and-persistence` skill, the `api-design` skill, the `observability-and-resilience` skill.
|
|
466
476
|
|
|
467
477
|
### 9.1 Validate untrusted input at the boundary
|
|
468
478
|
|
|
@@ -485,7 +495,7 @@ export type CreateOrderSchema = Infer<typeof createOrderSchema>;
|
|
|
485
495
|
const input = request.body as CreateOrderSchema;
|
|
486
496
|
```
|
|
487
497
|
|
|
488
|
-
Validation runs once at the route boundary; everything inward consumes the typed result. See `
|
|
498
|
+
Validation runs once at the route boundary; everything inward consumes the typed result. See the `security-baseline` skill for the full input-trust model.
|
|
489
499
|
|
|
490
500
|
### 9.2 Money is integer minor units, time is UTC at rest
|
|
491
501
|
|
|
@@ -500,13 +510,13 @@ type Order = { totalCents: number; placedAtUtc: Date };
|
|
|
500
510
|
type Order = { total: number; placedAt: string }; // float money, ambiguous tz
|
|
501
511
|
```
|
|
502
512
|
|
|
503
|
-
See `
|
|
513
|
+
See the `data-and-persistence` skill.
|
|
504
514
|
|
|
505
515
|
### 9.3 IDs in URLs are opaque
|
|
506
516
|
|
|
507
|
-
Use
|
|
517
|
+
Use an opaque UUID for public resource IDs in URLs, responses, and webhooks. The starter models store a separate `publicId`, generated by `randomUUID()` from `node:crypto` through a Seal default, with a unique migration column. The resource emits `id: ["publicId", "string"]`; services look up `{ publicId: id }`, then use the model's internal identity for writes.
|
|
508
518
|
|
|
509
|
-
|
|
519
|
+
Cascade's default `id` is sequential (including its NoSQL-generated integer); `model.uuid` only stringifies the primary key. Neither creates an opaque identifier. Internal primary/foreign keys may remain numeric. Never use `find(publicId)` or repository `getCached(publicId)` unless the primary key was explicitly changed. The `data-and-persistence` skill covers the schema and migration.
|
|
510
520
|
|
|
511
521
|
### 9.4 Every external call has a timeout
|
|
512
522
|
|
|
@@ -514,7 +524,7 @@ HTTP, AI, queue, database driver — every call to something outside this proces
|
|
|
514
524
|
|
|
515
525
|
`@warlock.js/http` accepts `timeout` per request and `timeout` per `Http` instance. Set a default at the instance level; override per request when a specific endpoint needs more.
|
|
516
526
|
|
|
517
|
-
See `
|
|
527
|
+
See the `observability-and-resilience` skill.
|
|
518
528
|
|
|
519
529
|
### 9.5 Idempotency keys for external side effects
|
|
520
530
|
|
|
@@ -524,7 +534,7 @@ This is the difference between "the network blipped" and "the customer got bille
|
|
|
524
534
|
|
|
525
535
|
### 9.6 One response envelope across the project
|
|
526
536
|
|
|
527
|
-
Every HTTP response
|
|
537
|
+
Every HTTP response follows the named success envelopes or the canonical `{ error: { code, message }, details?, requestId }` error envelope in the `api-design` skill. The moment this rule lands, the frontend stops writing per-endpoint special cases.
|
|
528
538
|
|
|
529
539
|
### 9.7 Config validated at boot, no scattered `process.env`
|
|
530
540
|
|
|
@@ -579,15 +589,14 @@ bounded regexes on input, one controller per file, `type` (not `kind`) as a
|
|
|
579
589
|
union's discriminator. Each problem has a `WL` code and its fix
|
|
580
590
|
(`npx warlock explain <CODE>`).
|
|
581
591
|
|
|
582
|
-
|
|
592
|
+
The scaffold enables these Oxlint rules:
|
|
583
593
|
|
|
584
|
-
- `typescript/explicit-member-accessibility` —
|
|
585
|
-
- `typescript/no-explicit-any` —
|
|
586
|
-
- `
|
|
587
|
-
- `
|
|
588
|
-
|
|
589
|
-
- `
|
|
590
|
-
- `no-console` — enforces § 8
|
|
594
|
+
- `typescript/explicit-member-accessibility` — § 1.2
|
|
595
|
+
- `typescript/no-explicit-any` — § 1.3
|
|
596
|
+
- `curly` — § 4.4
|
|
597
|
+
- `no-console` — § 8
|
|
598
|
+
|
|
599
|
+
`typescript/consistent-type-definitions` cannot express § 1.1's hybrid rule (interfaces for contracts, types for data); its global `type` or `interface` preference would contradict that rule, so it stays disabled. Review the distinction manually. `typescript/no-floating-promises` and `typescript/await-thenable` remain optional: both require `oxlint-tsgolint` and `options.typeAware: true`.
|
|
591
600
|
|
|
592
601
|
Match `.oxfmtrc.json`: 100 columns, double quotes, semicolons, trailing commas and built-in import sorting.
|
|
593
602
|
|
|
@@ -619,7 +628,7 @@ Before any change ships:
|
|
|
619
628
|
|
|
620
629
|
- [ ] Every untrusted input has a `@warlock.js/seal` schema at the boundary
|
|
621
630
|
- [ ] Money fields are integer minor units, time stored as UTC
|
|
622
|
-
- [ ] IDs exposed in URLs are opaque (
|
|
631
|
+
- [ ] IDs exposed in URLs are opaque (the starter uses a separate public UUID)
|
|
623
632
|
- [ ] Every external call (HTTP, AI, queue, DB) has an explicit timeout
|
|
624
633
|
- [ ] External side effects (payments, emails, webhooks) use idempotency keys
|
|
625
634
|
- [ ] Response shape matches the project's canonical envelope
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: data-and-persistence
|
|
3
|
-
description:
|
|
3
|
+
description: "Project persistence conventions: money, UTC dates, public UUIDs, Cascade models, migrations, indexes, relations and delete strategies. Use when changing stored data or model schemas. Use api-design instead for HTTP envelopes and resource output."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Data & persistence
|
|
@@ -8,7 +8,7 @@ description: 'How this project models, stores, and migrates data with `@warlock.
|
|
|
8
8
|
**Status:** Stable
|
|
9
9
|
**Applies to:** Every model, schema, migration, and persisted entity in `src/app/**`.
|
|
10
10
|
|
|
11
|
-
How we store data so it survives a year of new requirements, a currency conversion bug, and a timezone-aware feature request. Framework mechanics live in `@warlock.js/cascade
|
|
11
|
+
How we store data so it survives a year of new requirements, a currency conversion bug, and a timezone-aware feature request. Framework mechanics live in the `@warlock.js/cascade` skill — this skill is the **project-level conventions** layered on top.
|
|
12
12
|
|
|
13
13
|
> **Sub-agent rule:** Before writing any model, schema, or migration, read this file.
|
|
14
14
|
|
|
@@ -19,17 +19,17 @@ How we store data so it survives a year of new requirements, a currency conversi
|
|
|
19
19
|
### 1.1 The rule
|
|
20
20
|
|
|
21
21
|
- Store money as **integers in the currency's smallest unit** — cents for USD, halalas for SAR.
|
|
22
|
-
- Column name carries the unit: `
|
|
22
|
+
- Column name carries the unit: `totalCents`, `amountCents`, `feeCents`, `discountCents`.
|
|
23
23
|
- A `currency` column lives next to every money column (or once per row if the entity is mono-currency).
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
For example:
|
|
26
26
|
|
|
27
27
|
```typescript
|
|
28
|
-
// ✅
|
|
28
|
+
// ✅ example cart schema
|
|
29
29
|
export const cartSchema = v.object({
|
|
30
30
|
/* ... */
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
totalCents: v.int().default(0),
|
|
32
|
+
totalItems: v.int().default(0),
|
|
33
33
|
currency: v.string().default("USD"),
|
|
34
34
|
/* ... */
|
|
35
35
|
});
|
|
@@ -38,19 +38,19 @@ export const cartSchema = v.object({
|
|
|
38
38
|
### 1.2 Why never floats
|
|
39
39
|
|
|
40
40
|
```typescript
|
|
41
|
-
0.1 + 0.2 === 0.3
|
|
42
|
-
1234.567 * 100
|
|
41
|
+
0.1 + 0.2 === 0.3; // false
|
|
42
|
+
1234.567 * 100; // 123456.69999999999
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
Compound a million orders' subtotals across a payout report and you'll be off by enough to matter. Integer cents prevents this entire class of bug.
|
|
46
46
|
|
|
47
47
|
### 1.3 The AI-pricing exception
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
A token-pricing model may use a documented decimal or scaled sub-cent unit: AI provider pricing is sub-cent per token (e.g. $0.000002), where integer minor units round to zero. The exception is _only_ AI / token pricing; business money (orders, products, payments, balances, refunds) follows the cents rule.
|
|
50
50
|
|
|
51
51
|
### 1.4 Display formatting at the edge
|
|
52
52
|
|
|
53
|
-
Formatting (`$12.99`, `12.99 SAR`) is the resource layer's job — see `
|
|
53
|
+
Formatting (`$12.99`, `12.99 SAR`) is the resource layer's job — see the `api-design` skill § 5. The model and service work in cents; the resource converts.
|
|
54
54
|
|
|
55
55
|
---
|
|
56
56
|
|
|
@@ -61,24 +61,24 @@ Formatting (`$12.99`, `12.99 SAR`) is the resource layer's job — see `skills/a
|
|
|
61
61
|
- All timestamp columns store **UTC**, full stop.
|
|
62
62
|
- The column type is `date()` in the migration; the schema type is `v.date()` returning `Date`.
|
|
63
63
|
|
|
64
|
-
### 2.2 Naming — `<verb>
|
|
64
|
+
### 2.2 Naming — `<verb>At`
|
|
65
65
|
|
|
66
|
-
Time columns end with `
|
|
66
|
+
Time columns end with `At` and describe what happened, in past tense:
|
|
67
67
|
|
|
68
|
-
| Column
|
|
69
|
-
|
|
|
70
|
-
| `
|
|
71
|
-
| `
|
|
72
|
-
| `
|
|
73
|
-
| `
|
|
74
|
-
| `
|
|
75
|
-
| `
|
|
68
|
+
| Column | Meaning |
|
|
69
|
+
| -------------- | ---------------------------- |
|
|
70
|
+
| `createdAt` | row was created |
|
|
71
|
+
| `updatedAt` | row was last updated |
|
|
72
|
+
| `syncedAt` | row was synced from a source |
|
|
73
|
+
| `checkedOutAt` | cart was checked out |
|
|
74
|
+
| `abandonedAt` | cart was marked abandoned |
|
|
75
|
+
| `expiresAt` | row will become invalid |
|
|
76
76
|
|
|
77
77
|
```typescript
|
|
78
|
-
// ✅
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
// ✅ example cart schema
|
|
79
|
+
syncedAt: v.date().optional(),
|
|
80
|
+
abandonedAt: v.date().optional(),
|
|
81
|
+
checkedOutAt: v.date().optional(),
|
|
82
82
|
```
|
|
83
83
|
|
|
84
84
|
### 2.3 Conversion happens at the response layer
|
|
@@ -100,7 +100,22 @@ The project uses `dayjs` for time arithmetic and formatting (already in dependen
|
|
|
100
100
|
|
|
101
101
|
### 3.2 Project default
|
|
102
102
|
|
|
103
|
-
Cascade
|
|
103
|
+
Cascade defaults to sequential `id` values (a NoSQL-generated integer or the SQL primary key). The `model.uuid` getter only stringifies the primary key; it does not generate a UUID. The starter keeps that internal key for relations and authentication and adds a separate public UUID:
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
import { randomUUID } from "node:crypto";
|
|
107
|
+
import { v } from "@warlock.js/seal";
|
|
108
|
+
|
|
109
|
+
export const orderSchema = v.object({
|
|
110
|
+
publicId: v
|
|
111
|
+
.string()
|
|
112
|
+
.uuid()
|
|
113
|
+
.default(() => randomUUID()),
|
|
114
|
+
totalCents: v.int(),
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Add `publicId: string(36).unique()` in `Migration.create(Order, { ... })`; generate it server-side, and omit it from writable client schemas. A default is applied when the field is absent, not on every update. The resource emits `id: ["publicId", "string"]`. Resolve a validated route ID with `Order.findOne({ publicId: id })`, then write through that model instance. `find(id)` and repository `getCached(id)` still target the internal primary key. For an existing application, backfill a unique UUID per row before requiring this column; changing a resource cast to `"string"` does not make a sequential ID opaque.
|
|
104
119
|
|
|
105
120
|
### 3.3 Why
|
|
106
121
|
|
|
@@ -112,7 +127,7 @@ Sequential keys leak business volume (`/orders/42178` tells anyone you've placed
|
|
|
112
127
|
|
|
113
128
|
### 4.1 Framework-managed
|
|
114
129
|
|
|
115
|
-
Cascade
|
|
130
|
+
Cascade timestamp storage names are driver-dependent: MongoDB defaults to `createdAt` / `updatedAt`, PostgreSQL to `created_at` / `updated_at`. `Migration.create` adds the driver's timestamp columns unless `{ timestamps: false }`; model writes maintain their configured columns. Keep the stored names aligned with the driver or explicitly declare the columns when customizing timestamps. API output keys remain `createdAt` / `updatedAt`: use `["created_at", "date"]` / `["updated_at", "date"]` source-key tuples for a PostgreSQL-backed resource. Do not claim that a camelCase schema key automatically reads a snake_case stored field.
|
|
116
131
|
|
|
117
132
|
### 4.2 Author tracking
|
|
118
133
|
|
|
@@ -120,15 +135,15 @@ For entities where "who created this" matters (most business entities), add auth
|
|
|
120
135
|
|
|
121
136
|
```typescript
|
|
122
137
|
// ✅ from ai-model.resource.ts
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
138
|
+
createdBy: "string",
|
|
139
|
+
updatedBy: "string",
|
|
140
|
+
deletedBy: "string",
|
|
126
141
|
```
|
|
127
142
|
|
|
128
143
|
These store the user-id of the actor at the time of the action. Wire them in the service:
|
|
129
144
|
|
|
130
145
|
```typescript
|
|
131
|
-
await ordersRepository.create({ ...input,
|
|
146
|
+
await ordersRepository.create({ ...input, createdBy: user.get("publicId") });
|
|
132
147
|
```
|
|
133
148
|
|
|
134
149
|
### 4.3 What to audit
|
|
@@ -141,7 +156,7 @@ Every entity that's user-mutable: yes. Read-only reference data (countries, curr
|
|
|
141
156
|
|
|
142
157
|
### 5.1 Default to soft-delete for user-visible entities
|
|
143
158
|
|
|
144
|
-
Soft-delete keeps the row
|
|
159
|
+
Soft-delete keeps the row and sets the configured `deletedAt` column. Actor tracking such as `deletedBy` is app-owned. Cascade does not hide deleted rows automatically: add a global `whereNull("deletedAt")` scope or use the repository's active-read methods when you need active-only results.
|
|
145
160
|
|
|
146
161
|
### 5.2 Hard-delete for ephemeral data
|
|
147
162
|
|
|
@@ -154,7 +169,7 @@ Use hard-delete for:
|
|
|
154
169
|
|
|
155
170
|
### 5.3 Mechanics
|
|
156
171
|
|
|
157
|
-
Configure the delete strategy at the model level — see `@warlock.js/cascade
|
|
172
|
+
Configure the delete strategy at the model level — see the `@warlock.js/cascade` skill's `configure-delete-strategy` topic for the framework's per-model knobs (`deleteStrategy: "soft"`, `"permanent"`, or `"trash"`; per-call `destroy({ strategy })`).
|
|
158
173
|
|
|
159
174
|
### 5.4 Restore is admin-only
|
|
160
175
|
|
|
@@ -170,7 +185,7 @@ Never edit a shipped migration. Once it's on `main`, the only change is a new mi
|
|
|
170
185
|
|
|
171
186
|
### 6.2 File location
|
|
172
187
|
|
|
173
|
-
Migrations live inside the model folder: `src/app/<module>/models/<model>/migrations/<timestamp>-<noun>.migration.ts`. The framework's `
|
|
188
|
+
Migrations live inside the model folder: `src/app/<module>/models/<model>/migrations/<timestamp>-<noun>.migration.ts`. The framework's `npx warlock migrate` discovers them.
|
|
174
189
|
|
|
175
190
|
### 6.3 Definition shape
|
|
176
191
|
|
|
@@ -183,16 +198,16 @@ export default Migration.create(
|
|
|
183
198
|
AiModel,
|
|
184
199
|
{
|
|
185
200
|
provider: text().notNullable(),
|
|
186
|
-
|
|
201
|
+
providerModelId: text().notNullable(),
|
|
187
202
|
name: text().notNullable(),
|
|
188
203
|
context: integer().notNullable(),
|
|
189
|
-
|
|
190
|
-
|
|
204
|
+
isFree: bool().notNullable(),
|
|
205
|
+
inputPrice: double().notNullable(),
|
|
191
206
|
config: json().nullable(),
|
|
192
207
|
features: arrayText().nullable(),
|
|
193
208
|
},
|
|
194
209
|
{
|
|
195
|
-
unique: [{ columns: ["provider", "
|
|
210
|
+
unique: [{ columns: ["provider", "providerModelId"] }],
|
|
196
211
|
},
|
|
197
212
|
);
|
|
198
213
|
```
|
|
@@ -218,9 +233,9 @@ If a backfill is going to touch millions of rows, gate it behind a feature flag
|
|
|
218
233
|
|
|
219
234
|
### 7.1 Every query path has a supporting index
|
|
220
235
|
|
|
221
|
-
-
|
|
236
|
+
- Add an index for a frequently queried foreign key; do not assume declaring a relation adds one.
|
|
222
237
|
- Any column you filter or sort on regularly gets an index.
|
|
223
|
-
- Composite indexes match query order (`WHERE
|
|
238
|
+
- Composite indexes match query order (`WHERE orgId = ? AND status = ?` → index `(orgId, status)`).
|
|
224
239
|
|
|
225
240
|
### 7.2 Add the index alongside the query
|
|
226
241
|
|
|
@@ -228,23 +243,23 @@ If a new query is introduced, the index lands in the same PR. Don't push slow qu
|
|
|
228
243
|
|
|
229
244
|
### 7.3 Tooling
|
|
230
245
|
|
|
231
|
-
|
|
246
|
+
Define indexes alongside the migration using column `.index()` / `.unique()` or the migration options. See the `@warlock.js/cascade` skill's `write-migration` topic.
|
|
232
247
|
|
|
233
248
|
---
|
|
234
249
|
|
|
235
250
|
## 8. Schema conventions
|
|
236
251
|
|
|
237
|
-
### 8.1 Column naming —
|
|
252
|
+
### 8.1 Column naming — camelCase
|
|
238
253
|
|
|
239
|
-
|
|
254
|
+
New model and database column names use `camelCase`: `createdAt`, `totalCents`, `providerModelId`. Resource output also uses camelCase. For legacy snake_case columns, use source-key tuples in the resource as shown in the `api-design` skill; do not rename a stored column merely by changing the output key.
|
|
240
255
|
|
|
241
|
-
### 8.2 Foreign keys — `<noun>
|
|
256
|
+
### 8.2 Foreign keys — `<noun>Id`
|
|
242
257
|
|
|
243
|
-
`
|
|
258
|
+
`userId`, `organizationId`, `cartId`. Always singular, always `Id` suffix. Pass an explicit `foreignKey` to `@BelongsTo("Name", { foreignKey: "userId" })` when wiring a relation; match the stored column and the model schema.
|
|
244
259
|
|
|
245
260
|
### 8.3 Booleans — affirmative
|
|
246
261
|
|
|
247
|
-
`
|
|
262
|
+
`isActive`, `isFree`, `hasShipped`, `wasRefunded`. Never negative (`isNotDeleted`, `isUnverified`) — negative booleans break readability under negation (`!isNotDeleted` reads as a riddle).
|
|
248
263
|
|
|
249
264
|
### 8.4 Enums — string in the column, enum in the schema
|
|
250
265
|
|
|
@@ -272,11 +287,11 @@ src/app/<module>/models/<noun>/
|
|
|
272
287
|
<timestamp>-<noun>.migration.ts
|
|
273
288
|
```
|
|
274
289
|
|
|
275
|
-
### 9.2
|
|
290
|
+
### 9.2 Example model file shape
|
|
276
291
|
|
|
277
292
|
```typescript
|
|
278
293
|
// 1. schema first
|
|
279
|
-
export const cartSchema = v.object({
|
|
294
|
+
export const cartSchema = v.object({/* columns */});
|
|
280
295
|
export type CartSchema = Infer<typeof cartSchema>;
|
|
281
296
|
|
|
282
297
|
// 2. class with decorator
|
|
@@ -286,26 +301,26 @@ export class Cart extends Model<CartSchema> {
|
|
|
286
301
|
public static schema = cartSchema;
|
|
287
302
|
|
|
288
303
|
// 3. relations
|
|
289
|
-
@BelongsTo("Organization")
|
|
304
|
+
@BelongsTo("Organization", { foreignKey: "organizationId" })
|
|
290
305
|
public organization?: Organization;
|
|
291
306
|
|
|
292
|
-
@HasMany("CartItem")
|
|
307
|
+
@HasMany("CartItem", { foreignKey: "cartId" })
|
|
293
308
|
public items?: CartItem[];
|
|
294
309
|
|
|
295
310
|
// 4. typed getters — one per persisted field accessed from app code
|
|
296
311
|
public get totalCents() {
|
|
297
|
-
return this.get<number>("
|
|
312
|
+
return this.get<number>("totalCents", 0);
|
|
298
313
|
}
|
|
299
314
|
}
|
|
300
315
|
```
|
|
301
316
|
|
|
302
317
|
### 9.3 Typed getters over `.get<T>("field")` at call sites
|
|
303
318
|
|
|
304
|
-
This is project policy
|
|
319
|
+
This is project policy. Every column accessed from application code gets a typed getter on the model. One line beats `cart.get<number>("totalCents")` across N call sites.
|
|
305
320
|
|
|
306
321
|
### 9.4 Class-level JSDoc for non-obvious domain logic
|
|
307
322
|
|
|
308
|
-
|
|
323
|
+
Explain identity ownership, source-of-truth rules, and persisted snapshots for any model whose semantics are not obvious from its columns.
|
|
309
324
|
|
|
310
325
|
---
|
|
311
326
|
|
|
@@ -314,16 +329,16 @@ The Cart model has a multi-paragraph JSDoc explaining identity ownership, lead l
|
|
|
314
329
|
Before merging a change that touches a model, schema, or migration:
|
|
315
330
|
|
|
316
331
|
- [ ] Money columns are `integer cents` with `currency` alongside (AI-pricing is the only exception)
|
|
317
|
-
- [ ] Time columns end with `
|
|
332
|
+
- [ ] Time columns end with `At`, store UTC `Date`
|
|
318
333
|
- [ ] No timezone-adjusted timestamps in storage
|
|
319
334
|
- [ ] IDs exposed in URLs / responses are opaque
|
|
320
|
-
- [ ] Audit columns (`
|
|
335
|
+
- [ ] Audit columns (`createdBy`, `updatedBy`) wired where author tracking matters
|
|
321
336
|
- [ ] Soft-delete strategy chosen deliberately, configured via cascade
|
|
322
337
|
- [ ] Migration is forward-only, not editing a shipped file
|
|
323
338
|
- [ ] Backfill is a separate migration / background job from the schema change
|
|
324
339
|
- [ ] Every new query path has a supporting index
|
|
325
|
-
- [ ] Column names
|
|
326
|
-
- [ ] Booleans are affirmative (`
|
|
340
|
+
- [ ] Column names camelCase, foreign keys `<noun>Id`
|
|
341
|
+
- [ ] Booleans are affirmative (`isActive`, never `isNotDeleted`)
|
|
327
342
|
- [ ] Enums defined once in `types/`, never repeated as string literals
|
|
328
343
|
- [ ] Defaults declared in the schema, not only in the migration
|
|
329
344
|
- [ ] Every accessed column has a typed getter on the model
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: git-workflow
|
|
3
|
-
description:
|
|
3
|
+
description: "Project Git conventions, branches, conventional commits, pull requests, review and CI gates. Use when preparing or reviewing commits and PRs. Use code-standards instead for implementation style."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Git workflow
|
|
@@ -200,14 +200,14 @@ A drive-by approve is worse than no approve — it gives false confidence. If yo
|
|
|
200
200
|
|
|
201
201
|
Every PR runs these. All must pass before merge. The exact commands match `package.json` scripts:
|
|
202
202
|
|
|
203
|
-
| Gate | Command
|
|
204
|
-
| ---------------------------- |
|
|
205
|
-
| Type-check | `
|
|
206
|
-
| Lint | `
|
|
207
|
-
| Tests | `
|
|
208
|
-
| Coverage (recommended floor) | `
|
|
209
|
-
| Dependency audit | `
|
|
210
|
-
| Format check | `
|
|
203
|
+
| Gate | Command | Blocks on |
|
|
204
|
+
| ---------------------------- | ------------------------------ | ------------------------------- |
|
|
205
|
+
| Type-check | `npm run tsc` | Any TS error |
|
|
206
|
+
| Lint | `npm run lint:check` | Any Oxlint error |
|
|
207
|
+
| Tests | `npm run test -- --run` | Any failed test |
|
|
208
|
+
| Coverage (recommended floor) | `npm run test:coverage` | TBD per team policy |
|
|
209
|
+
| Dependency audit | `npm audit --audit-level=high` | High / critical vulnerabilities |
|
|
210
|
+
| Format check | `npm run format:check` | Any unformatted file |
|
|
211
211
|
|
|
212
212
|
Additional rules:
|
|
213
213
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: module-boundaries
|
|
3
|
-
description:
|
|
3
|
+
description: "Domain module ownership, dependency direction, app sub-folder exports, cross-module services and events, and shared helpers. Use when adding modules or changing imports between domains. Use code-standards instead for organization within one module."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Module boundaries
|
|
@@ -94,7 +94,7 @@ This is the one absolute rule:
|
|
|
94
94
|
✅ src/app/orders/types/index.ts ← good
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
-
See `
|
|
97
|
+
See the `code-standards` skill § 2.2 for the reasoning.
|
|
98
98
|
|
|
99
99
|
---
|
|
100
100
|
|