@c0x12c/ai-toolkit 1.15.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/.claude-plugin/marketplace.json +16 -0
- package/.claude-plugin/plugin.json +12 -0
- package/README.md +439 -0
- package/VERSION +1 -0
- package/agents/design-critic.md +127 -0
- package/agents/idea-killer.md +72 -0
- package/agents/infrastructure-expert.md +49 -0
- package/agents/micronaut-backend-expert.md +45 -0
- package/agents/phase-reviewer.md +150 -0
- package/agents/research-planner.md +70 -0
- package/agents/solution-architect-cto.md +49 -0
- package/agents/sre-architect.md +49 -0
- package/agents/team-coordinator.md +111 -0
- package/bin/cli.js +780 -0
- package/claude-md/00-header.md +39 -0
- package/claude-md/01-core.md +105 -0
- package/claude-md/05-database.md +20 -0
- package/claude-md/11-backend-micronaut.md +19 -0
- package/claude-md/20-frontend-react.md +44 -0
- package/claude-md/25-ux-design.md +56 -0
- package/claude-md/30-infrastructure.md +24 -0
- package/claude-md/30-project-mgmt.md +119 -0
- package/claude-md/40-product.md +39 -0
- package/claude-md/50-ops.md +34 -0
- package/claude-md/60-research.md +27 -0
- package/claude-md/90-footer.md +21 -0
- package/commands/spartan/brainstorm.md +134 -0
- package/commands/spartan/brownfield.md +157 -0
- package/commands/spartan/build.md +435 -0
- package/commands/spartan/careful.md +94 -0
- package/commands/spartan/commit-message.md +112 -0
- package/commands/spartan/content.md +17 -0
- package/commands/spartan/context-save.md +161 -0
- package/commands/spartan/contribute.md +140 -0
- package/commands/spartan/daily.md +42 -0
- package/commands/spartan/debug.md +308 -0
- package/commands/spartan/deep-dive.md +55 -0
- package/commands/spartan/deploy.md +207 -0
- package/commands/spartan/e2e.md +264 -0
- package/commands/spartan/env-setup.md +166 -0
- package/commands/spartan/epic.md +199 -0
- package/commands/spartan/fe-review.md +181 -0
- package/commands/spartan/figma-to-code.md +260 -0
- package/commands/spartan/forensics.md +46 -0
- package/commands/spartan/freeze.md +84 -0
- package/commands/spartan/fundraise.md +53 -0
- package/commands/spartan/gate-review.md +229 -0
- package/commands/spartan/gsd-upgrade.md +376 -0
- package/commands/spartan/guard.md +42 -0
- package/commands/spartan/init-project.md +178 -0
- package/commands/spartan/init-rules.md +298 -0
- package/commands/spartan/interview.md +154 -0
- package/commands/spartan/kickoff.md +73 -0
- package/commands/spartan/kotlin-service.md +109 -0
- package/commands/spartan/lean-canvas.md +222 -0
- package/commands/spartan/lint-rules.md +122 -0
- package/commands/spartan/map-codebase.md +124 -0
- package/commands/spartan/migration.md +82 -0
- package/commands/spartan/next-app.md +317 -0
- package/commands/spartan/next-feature.md +212 -0
- package/commands/spartan/onboard.md +326 -0
- package/commands/spartan/outreach.md +16 -0
- package/commands/spartan/phase.md +142 -0
- package/commands/spartan/pitch.md +18 -0
- package/commands/spartan/plan.md +210 -0
- package/commands/spartan/pr-ready.md +202 -0
- package/commands/spartan/project.md +106 -0
- package/commands/spartan/qa.md +222 -0
- package/commands/spartan/research.md +254 -0
- package/commands/spartan/review.md +132 -0
- package/commands/spartan/scan-rules.md +173 -0
- package/commands/spartan/sessions.md +143 -0
- package/commands/spartan/spec.md +131 -0
- package/commands/spartan/startup.md +257 -0
- package/commands/spartan/team.md +570 -0
- package/commands/spartan/teardown.md +161 -0
- package/commands/spartan/testcontainer.md +97 -0
- package/commands/spartan/tf-cost.md +123 -0
- package/commands/spartan/tf-deploy.md +116 -0
- package/commands/spartan/tf-drift.md +100 -0
- package/commands/spartan/tf-import.md +107 -0
- package/commands/spartan/tf-module.md +121 -0
- package/commands/spartan/tf-plan.md +100 -0
- package/commands/spartan/tf-review.md +106 -0
- package/commands/spartan/tf-scaffold.md +109 -0
- package/commands/spartan/tf-security.md +147 -0
- package/commands/spartan/think.md +221 -0
- package/commands/spartan/unfreeze.md +13 -0
- package/commands/spartan/update.md +134 -0
- package/commands/spartan/ux.md +1233 -0
- package/commands/spartan/validate.md +193 -0
- package/commands/spartan/web-to-prd.md +706 -0
- package/commands/spartan/workstreams.md +109 -0
- package/commands/spartan/write.md +16 -0
- package/commands/spartan.md +386 -0
- package/frameworks/00-framework-comparison-guide.md +317 -0
- package/frameworks/01-lean-canvas.md +196 -0
- package/frameworks/02-design-sprint.md +304 -0
- package/frameworks/03-foundation-sprint.md +337 -0
- package/frameworks/04-business-model-canvas.md +391 -0
- package/frameworks/05-customer-development.md +426 -0
- package/frameworks/06-jobs-to-be-done.md +358 -0
- package/frameworks/07-mom-test.md +392 -0
- package/frameworks/08-value-proposition-canvas.md +488 -0
- package/frameworks/09-javelin-board.md +428 -0
- package/frameworks/10-build-measure-learn.md +467 -0
- package/frameworks/11-mvp-approaches.md +533 -0
- package/frameworks/think-before-build.md +593 -0
- package/lib/assembler.js +197 -0
- package/lib/assembler.test.js +159 -0
- package/lib/detector.js +166 -0
- package/lib/detector.test.js +221 -0
- package/lib/packs.js +16 -0
- package/lib/resolver.js +272 -0
- package/lib/resolver.test.js +298 -0
- package/lib/worktree.sh +104 -0
- package/package.json +50 -0
- package/packs/backend-micronaut.yaml +35 -0
- package/packs/backend-nodejs.yaml +15 -0
- package/packs/backend-python.yaml +15 -0
- package/packs/core.yaml +37 -0
- package/packs/database.yaml +21 -0
- package/packs/frontend-react.yaml +24 -0
- package/packs/infrastructure.yaml +40 -0
- package/packs/ops.yaml +16 -0
- package/packs/packs.compiled.json +371 -0
- package/packs/product.yaml +22 -0
- package/packs/project-mgmt.yaml +24 -0
- package/packs/research.yaml +39 -0
- package/packs/shared-backend.yaml +14 -0
- package/packs/ux-design.yaml +21 -0
- package/rules/backend-micronaut/API_DESIGN.md +313 -0
- package/rules/backend-micronaut/BATCH_PROCESSING.md +92 -0
- package/rules/backend-micronaut/CONTROLLERS.md +388 -0
- package/rules/backend-micronaut/KOTLIN.md +414 -0
- package/rules/backend-micronaut/RETROFIT_PLACEMENT.md +290 -0
- package/rules/backend-micronaut/SERVICES_AND_BEANS.md +325 -0
- package/rules/core/NAMING_CONVENTIONS.md +208 -0
- package/rules/core/SKILL_AUTHORING.md +174 -0
- package/rules/core/TIMEZONE.md +316 -0
- package/rules/database/ORM_AND_REPO.md +289 -0
- package/rules/database/SCHEMA.md +146 -0
- package/rules/database/TRANSACTIONS.md +311 -0
- package/rules/frontend-react/FRONTEND.md +344 -0
- package/rules/infrastructure/MODULES.md +260 -0
- package/rules/infrastructure/NAMING.md +196 -0
- package/rules/infrastructure/PROVIDERS.md +309 -0
- package/rules/infrastructure/SECURITY.md +310 -0
- package/rules/infrastructure/STATE_AND_BACKEND.md +237 -0
- package/rules/infrastructure/STRUCTURE.md +234 -0
- package/rules/infrastructure/VARIABLES.md +285 -0
- package/rules/shared-backend/ARCHITECTURE.md +46 -0
- package/rules/ux-design/DESIGN_PROCESS.md +176 -0
- package/skills/api-endpoint-creator/SKILL.md +455 -0
- package/skills/api-endpoint-creator/error-handling-guide.md +244 -0
- package/skills/api-endpoint-creator/examples.md +522 -0
- package/skills/api-endpoint-creator/testing-patterns.md +302 -0
- package/skills/article-writing/SKILL.md +109 -0
- package/skills/article-writing/examples.md +59 -0
- package/skills/backend-api-design/SKILL.md +84 -0
- package/skills/backend-api-design/code-patterns.md +138 -0
- package/skills/brainstorm/SKILL.md +95 -0
- package/skills/browser-qa/SKILL.md +87 -0
- package/skills/browser-qa/playwright-snippets.md +110 -0
- package/skills/ci-cd-patterns/SKILL.md +108 -0
- package/skills/ci-cd-patterns/workflows.md +149 -0
- package/skills/competitive-teardown/SKILL.md +93 -0
- package/skills/competitive-teardown/example-analysis.md +50 -0
- package/skills/content-engine/SKILL.md +131 -0
- package/skills/content-engine/examples.md +72 -0
- package/skills/database-patterns/SKILL.md +72 -0
- package/skills/database-patterns/code-templates.md +114 -0
- package/skills/database-table-creator/SKILL.md +141 -0
- package/skills/database-table-creator/examples.md +552 -0
- package/skills/database-table-creator/kotlin-templates.md +400 -0
- package/skills/database-table-creator/migration-template.sql +68 -0
- package/skills/database-table-creator/validation-checklist.md +337 -0
- package/skills/deep-research/SKILL.md +80 -0
- package/skills/design-intelligence/SKILL.md +268 -0
- package/skills/design-workflow/SKILL.md +127 -0
- package/skills/design-workflow/checklists.md +45 -0
- package/skills/idea-validation/SKILL.md +129 -0
- package/skills/idea-validation/example-report.md +50 -0
- package/skills/investor-materials/SKILL.md +122 -0
- package/skills/investor-materials/example-outline.md +70 -0
- package/skills/investor-outreach/SKILL.md +112 -0
- package/skills/investor-outreach/examples.md +76 -0
- package/skills/kotlin-best-practices/SKILL.md +58 -0
- package/skills/kotlin-best-practices/code-patterns.md +132 -0
- package/skills/market-research/SKILL.md +99 -0
- package/skills/security-checklist/SKILL.md +65 -0
- package/skills/security-checklist/audit-reference.md +95 -0
- package/skills/service-debugging/SKILL.md +116 -0
- package/skills/service-debugging/common-issues.md +65 -0
- package/skills/startup-pipeline/SKILL.md +152 -0
- package/skills/terraform-best-practices/SKILL.md +244 -0
- package/skills/terraform-module-creator/SKILL.md +284 -0
- package/skills/terraform-review/SKILL.md +222 -0
- package/skills/terraform-security-audit/SKILL.md +280 -0
- package/skills/terraform-service-scaffold/SKILL.md +574 -0
- package/skills/testing-strategies/SKILL.md +116 -0
- package/skills/testing-strategies/examples.md +103 -0
- package/skills/testing-strategies/integration-test-setup.md +71 -0
- package/skills/ui-ux-pro-max/SKILL.md +238 -0
- package/skills/ui-ux-pro-max/data/charts.csv +26 -0
- package/skills/ui-ux-pro-max/data/colors.csv +97 -0
- package/skills/ui-ux-pro-max/data/icons.csv +101 -0
- package/skills/ui-ux-pro-max/data/landing.csv +31 -0
- package/skills/ui-ux-pro-max/data/products.csv +97 -0
- package/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
- package/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
- package/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/skills/ui-ux-pro-max/data/styles.csv +68 -0
- package/skills/ui-ux-pro-max/data/typography.csv +58 -0
- package/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
- package/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
- package/skills/ui-ux-pro-max/python-setup.md +146 -0
- package/skills/ui-ux-pro-max/scripts/core.py +253 -0
- package/skills/ui-ux-pro-max/scripts/design_system.py +1067 -0
- package/skills/ui-ux-pro-max/scripts/search.py +114 -0
- package/skills/web-to-prd/SKILL.md +478 -0
- package/templates/build-config.yaml +44 -0
- package/templates/commands-config.yaml +55 -0
- package/templates/competitor-analysis.md +60 -0
- package/templates/content/AGENT_TEMPLATE.md +47 -0
- package/templates/content/COMMAND_TEMPLATE.md +27 -0
- package/templates/content/RULE_TEMPLATE.md +40 -0
- package/templates/content/SKILL_TEMPLATE.md +41 -0
- package/templates/design-config.md +105 -0
- package/templates/design-doc.md +207 -0
- package/templates/epic.md +100 -0
- package/templates/feature-spec.md +181 -0
- package/templates/idea-canvas.md +47 -0
- package/templates/implementation-plan.md +159 -0
- package/templates/prd-template.md +86 -0
- package/templates/preamble.md +89 -0
- package/templates/project-readme.md +35 -0
- package/templates/quality-gates.md +230 -0
- package/templates/spartan-config.yaml +164 -0
- package/templates/user-interview.md +69 -0
- package/templates/validation-checklist.md +108 -0
- package/templates/workflow-backend-micronaut.md +409 -0
- package/templates/workflow-frontend-react.md +233 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
# Timezone Rules
|
|
2
|
+
|
|
3
|
+
## One Rule: Everything is UTC
|
|
4
|
+
|
|
5
|
+
**Server stores UTC. API sends UTC. API receives UTC. No exceptions.**
|
|
6
|
+
|
|
7
|
+
The frontend is the only place that converts to/from local time — for display only.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Database (TIMESTAMPTZ/UTC) → Backend (Instant/UTC) → API JSON (ISO 8601 Z) → Frontend (UTC) → Display (local)
|
|
11
|
+
← Send (local → UTC) ← Input
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Database
|
|
17
|
+
|
|
18
|
+
### Use `TIMESTAMPTZ` — Not `TIMESTAMP`
|
|
19
|
+
|
|
20
|
+
**Always use `TIMESTAMPTZ` (with timezone).** PostgreSQL docs and wiki both say this.
|
|
21
|
+
|
|
22
|
+
Why: `TIMESTAMPTZ` converts to UTC on insert and converts back on read. If a connection has a non-UTC session timezone (DBA tools, connection pool quirks, migration scripts), `TIMESTAMPTZ` still stores the correct UTC value. `TIMESTAMP` without timezone silently stores whatever you give it — if the session isn't UTC, you get wrong data and can't tell.
|
|
23
|
+
|
|
24
|
+
```sql
|
|
25
|
+
-- CORRECT — TIMESTAMPTZ is the safe default
|
|
26
|
+
CREATE TABLE events (
|
|
27
|
+
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
28
|
+
starts_at TIMESTAMPTZ NOT NULL,
|
|
29
|
+
ends_at TIMESTAMPTZ NOT NULL,
|
|
30
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
31
|
+
updated_at TIMESTAMPTZ,
|
|
32
|
+
deleted_at TIMESTAMPTZ
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
-- WRONG — TIMESTAMP without timezone is fragile
|
|
36
|
+
CREATE TABLE events (
|
|
37
|
+
id UUID PRIMARY KEY,
|
|
38
|
+
starts_at TIMESTAMP NOT NULL, -- Breaks if session timezone isn't UTC
|
|
39
|
+
created_at TIMESTAMP DEFAULT NOW()
|
|
40
|
+
);
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Both types use 8 bytes internally. No storage difference.
|
|
44
|
+
|
|
45
|
+
### Server Must Run in UTC
|
|
46
|
+
|
|
47
|
+
The database server, application server, and all containers must run in UTC:
|
|
48
|
+
|
|
49
|
+
```yaml
|
|
50
|
+
# application.yml
|
|
51
|
+
datasources:
|
|
52
|
+
default:
|
|
53
|
+
connection-properties:
|
|
54
|
+
timezone: UTC
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```sql
|
|
58
|
+
-- PostgreSQL: verify
|
|
59
|
+
SHOW timezone; -- Should return 'UTC'
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```dockerfile
|
|
63
|
+
# Dockerfile
|
|
64
|
+
ENV TZ=UTC
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```yaml
|
|
68
|
+
# Kubernetes pod spec
|
|
69
|
+
env:
|
|
70
|
+
- name: TZ
|
|
71
|
+
value: "UTC"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Backend (Kotlin)
|
|
77
|
+
|
|
78
|
+
### Always Use `Instant` — Never `LocalDateTime`
|
|
79
|
+
|
|
80
|
+
`Instant` is UTC by definition. `LocalDateTime` has no timezone info and causes bugs.
|
|
81
|
+
|
|
82
|
+
```kotlin
|
|
83
|
+
// CORRECT — Instant is always UTC
|
|
84
|
+
val now: Instant = Instant.now()
|
|
85
|
+
val expiresAt: Instant = Instant.now().plusSeconds(3600)
|
|
86
|
+
|
|
87
|
+
// WRONG — LocalDateTime has no timezone, ambiguous
|
|
88
|
+
val now: LocalDateTime = LocalDateTime.now() // What timezone? Nobody knows.
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### `ZonedDateTime` — Only at Computation Boundaries
|
|
92
|
+
|
|
93
|
+
Never put `ZonedDateTime` in entities, DTOs, or API payloads. It's OK for:
|
|
94
|
+
- Scheduling logic (computing "next 9 AM in user's timezone")
|
|
95
|
+
- DST-aware date arithmetic ("add 1 day" at DST boundary)
|
|
96
|
+
- Generating reports in a specific timezone
|
|
97
|
+
|
|
98
|
+
Always convert back to `Instant` before passing to other layers.
|
|
99
|
+
|
|
100
|
+
```kotlin
|
|
101
|
+
// CORRECT — entities and DTOs use Instant
|
|
102
|
+
data class UserEntity(
|
|
103
|
+
val createdAt: Instant,
|
|
104
|
+
val lastLoginAt: Instant?,
|
|
105
|
+
val subscriptionExpiresAt: Instant?
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
// CORRECT — ZonedDateTime only for scheduling computation
|
|
109
|
+
fun nextNotificationTime(userTimezone: String, localTime: LocalTime): Instant {
|
|
110
|
+
val zone = ZoneId.of(userTimezone)
|
|
111
|
+
val nextLocal = ZonedDateTime.now(zone).with(localTime)
|
|
112
|
+
return nextLocal.toInstant() // Convert back to Instant
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// WRONG — ZonedDateTime in entity
|
|
116
|
+
data class UserEntity(
|
|
117
|
+
val createdAt: ZonedDateTime // NO — keep entities in Instant
|
|
118
|
+
)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Jackson Serialization
|
|
122
|
+
|
|
123
|
+
Jackson must serialize `Instant` as ISO 8601 with the `Z` suffix:
|
|
124
|
+
|
|
125
|
+
```kotlin
|
|
126
|
+
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
|
|
127
|
+
// Output: "2024-01-15T10:30:00Z"
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Never output offsets like `+07:00` or timezone names in API responses.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## API Contract
|
|
135
|
+
|
|
136
|
+
### All Datetime Fields Are ISO 8601 UTC
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
{
|
|
140
|
+
"created_at": "2024-01-15T10:30:00Z",
|
|
141
|
+
"updated_at": "2024-01-15T14:22:33Z",
|
|
142
|
+
"expires_at": "2024-02-15T00:00:00Z"
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Request Bodies — Frontend Sends UTC
|
|
147
|
+
|
|
148
|
+
```json
|
|
149
|
+
{
|
|
150
|
+
"starts_at": "2024-01-20T09:00:00Z",
|
|
151
|
+
"ends_at": "2024-01-20T17:00:00Z"
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Query Parameters — Also UTC
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
GET /events?from=2024-01-01T00:00:00Z&to=2024-01-31T23:59:59Z
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### No Timezone Fields in Timestamp Payloads
|
|
162
|
+
|
|
163
|
+
Don't put timezone info alongside timestamps. The exception is user preferences (see below).
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
// WRONG — timezone alongside a timestamp
|
|
167
|
+
{ "starts_at": "2024-01-20T09:00:00Z", "timezone": "America/New_York" }
|
|
168
|
+
|
|
169
|
+
// CORRECT — just UTC
|
|
170
|
+
{ "starts_at": "2024-01-20T09:00:00Z" }
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Frontend
|
|
176
|
+
|
|
177
|
+
### Receive UTC, Convert for Display
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
// Convert at display time
|
|
181
|
+
function formatDate(utcString: string): string {
|
|
182
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
183
|
+
dateStyle: 'medium',
|
|
184
|
+
timeStyle: 'short',
|
|
185
|
+
}).format(new Date(utcString))
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Send UTC to Server
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
// Convert local input to UTC before API call
|
|
193
|
+
const localDate = new Date(userInput)
|
|
194
|
+
const utcString = localDate.toISOString() // "2024-01-20T02:00:00.000Z"
|
|
195
|
+
|
|
196
|
+
await api.post('/events', { startsAt: utcString })
|
|
197
|
+
|
|
198
|
+
// WRONG — no Z suffix, ambiguous
|
|
199
|
+
await api.post('/events', { startsAt: '2024-01-20T09:00:00' })
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Use Browser Timezone at Render Time
|
|
203
|
+
|
|
204
|
+
Don't track the user's timezone in frontend state. The browser already knows it.
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
// CORRECT — use at render time
|
|
208
|
+
const userTz = Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
209
|
+
|
|
210
|
+
// WRONG — storing timezone in state
|
|
211
|
+
const [timezone, setTimezone] = useState('America/New_York')
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## When You DO Need Timezone
|
|
217
|
+
|
|
218
|
+
There are cases where storing a user's IANA timezone is correct. The rule is:
|
|
219
|
+
|
|
220
|
+
**Past events (created_at, login_at, order_placed_at):** Never store timezone. `TIMESTAMPTZ` (UTC) is enough.
|
|
221
|
+
|
|
222
|
+
**User preferences (notification time, business hours):** Store the user's IANA timezone as a separate column. Don't mix it with timestamps.
|
|
223
|
+
|
|
224
|
+
```sql
|
|
225
|
+
-- CORRECT — timezone as a user preference, not part of timestamps
|
|
226
|
+
CREATE TABLE user_preferences (
|
|
227
|
+
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
228
|
+
user_id UUID NOT NULL,
|
|
229
|
+
timezone TEXT NOT NULL DEFAULT 'UTC', -- IANA timezone: 'America/New_York'
|
|
230
|
+
notification_time TEXT NOT NULL DEFAULT '09:00', -- local time, not a timestamp
|
|
231
|
+
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
-- Then compute UTC fire time dynamically in code:
|
|
235
|
+
-- nextFire = ZonedDateTime.of(today, LocalTime.parse("09:00"), ZoneId.of("America/New_York")).toInstant()
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
**Why dynamic computation?** Because DST shifts change the UTC offset. "9 AM New York" is `14:00 UTC` in winter but `13:00 UTC` in summer. Storing a fixed UTC value would drift by an hour.
|
|
239
|
+
|
|
240
|
+
**Never use fixed offsets as timezone identifiers.** `+05:30` is an offset, not a timezone. It changes with DST. Use IANA names: `Asia/Kolkata`, `America/New_York`.
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## Microservices
|
|
245
|
+
|
|
246
|
+
### Inter-Service Communication
|
|
247
|
+
|
|
248
|
+
All service-to-service datetime fields use ISO 8601 UTC, same as external APIs.
|
|
249
|
+
|
|
250
|
+
### Event Streaming (Kafka, RabbitMQ)
|
|
251
|
+
|
|
252
|
+
- Use epoch-based types: Avro `timestamp-millis`, Protobuf `google.protobuf.Timestamp`
|
|
253
|
+
- These are UTC by definition — no timezone ambiguity
|
|
254
|
+
- Document in your schema that all timestamps are UTC epoch
|
|
255
|
+
|
|
256
|
+
### Logging
|
|
257
|
+
|
|
258
|
+
All services must log in UTC. If services in different timezones log in local time, correlating logs across services is a nightmare.
|
|
259
|
+
|
|
260
|
+
```xml
|
|
261
|
+
<!-- logback.xml — force UTC -->
|
|
262
|
+
<timestamp key="timestamp" datePattern="yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" timeReference="UTC"/>
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Distributed Tracing
|
|
266
|
+
|
|
267
|
+
OpenTelemetry spans use nanosecond UTC timestamps internally. No action needed — but make sure NTP is configured on all nodes. Clock skew (not timezone) is the bigger concern.
|
|
268
|
+
|
|
269
|
+
### Cron / Scheduler Jobs
|
|
270
|
+
|
|
271
|
+
Cron expressions are timezone-sensitive. DST transitions can cause jobs to fire twice, or not at all, in the 1-3 AM window.
|
|
272
|
+
|
|
273
|
+
```kotlin
|
|
274
|
+
// CORRECT — schedule in UTC to avoid DST issues
|
|
275
|
+
@Scheduled(cron = "0 0 14 * * *", zone = "UTC") // 2 PM UTC, not "2 PM local"
|
|
276
|
+
fun dailyDigest() { ... }
|
|
277
|
+
|
|
278
|
+
// If the job MUST fire at local wall-clock time, use IANA timezone explicitly:
|
|
279
|
+
@Scheduled(cron = "0 0 9 * * *", zone = "America/New_York") // 9 AM New York, DST-aware
|
|
280
|
+
fun morningNotification() { ... }
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
Avoid scheduling jobs in the 1:00-3:00 AM local time window for any timezone with DST.
|
|
284
|
+
|
|
285
|
+
### IANA Timezone Database Updates
|
|
286
|
+
|
|
287
|
+
Governments change DST rules. Your JVM and OS timezone databases need updating. If you run long-lived JVMs, update the JDK or use the TZUpdater tool.
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## Quick Reference
|
|
292
|
+
|
|
293
|
+
| Layer | Type | Format | Example |
|
|
294
|
+
|-------|------|--------|---------|
|
|
295
|
+
| Database | Column type | `TIMESTAMPTZ` | `2024-01-15 10:30:00+00` |
|
|
296
|
+
| Backend (Kotlin) | Property type | `Instant` | `Instant.now()` |
|
|
297
|
+
| API JSON | String | ISO 8601 + Z | `"2024-01-15T10:30:00Z"` |
|
|
298
|
+
| Frontend (receive) | Parse | `new Date(utcString)` | `new Date("2024-01-15T10:30:00Z")` |
|
|
299
|
+
| Frontend (display) | Format | `Intl.DateTimeFormat` | `"Jan 15, 2024, 5:30 PM"` |
|
|
300
|
+
| Frontend (send) | Serialize | `toISOString()` | `"2024-01-15T10:30:00.000Z"` |
|
|
301
|
+
| Events (Kafka) | Type | epoch millis | `1705312200000` |
|
|
302
|
+
| Cron jobs | Zone | IANA or UTC | `zone = "UTC"` |
|
|
303
|
+
| User preference | Column | IANA timezone | `America/New_York` |
|
|
304
|
+
|
|
305
|
+
## What NOT to Do
|
|
306
|
+
|
|
307
|
+
- Don't use `TIMESTAMP` without timezone — use `TIMESTAMPTZ`
|
|
308
|
+
- Don't use `LocalDateTime` in Kotlin — use `Instant`
|
|
309
|
+
- Don't put `ZonedDateTime` in entities or DTOs
|
|
310
|
+
- Don't use fixed offsets (`+05:30`) as timezone identifiers — use IANA names
|
|
311
|
+
- Don't store timezone alongside timestamps for past events
|
|
312
|
+
- Don't convert to local time on the backend — that's the frontend's job
|
|
313
|
+
- Don't format dates on the server for display — return UTC, let the client format
|
|
314
|
+
- Don't schedule cron jobs in the 1-3 AM DST window
|
|
315
|
+
- Don't assume the host timezone is UTC — set `TZ=UTC` in containers
|
|
316
|
+
- Don't log in local time — all services log UTC
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# Exposed ORM and Repository Patterns
|
|
2
|
+
|
|
3
|
+
> Full guide: use /database-patterns skill
|
|
4
|
+
|
|
5
|
+
## Exposed ORM Patterns (CRITICAL)
|
|
6
|
+
|
|
7
|
+
### Only `id` Uses `.value` — All Other UUID Columns Do NOT
|
|
8
|
+
|
|
9
|
+
`UUIDTable.id` returns `EntityID<UUID>` which needs `.value` to get the raw `UUID`.
|
|
10
|
+
All other `uuid()` columns return `UUID` directly — using `.value` on them causes compilation errors.
|
|
11
|
+
|
|
12
|
+
```kotlin
|
|
13
|
+
// CORRECT
|
|
14
|
+
fun toEntity(row: ResultRow) = MyEntity(
|
|
15
|
+
id = row[MyTable.id].value, // id -> EntityID<UUID> -> needs .value
|
|
16
|
+
projectId = row[MyTable.projectId], // uuid() column -> UUID directly
|
|
17
|
+
userId = row[MyTable.userId], // uuid() column -> UUID directly
|
|
18
|
+
name = row[MyTable.name], // text() column -> String directly
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
// WRONG — causes "Unresolved reference 'value'"
|
|
22
|
+
fun toEntity(row: ResultRow) = MyEntity(
|
|
23
|
+
id = row[MyTable.id].value,
|
|
24
|
+
projectId = row[MyTable.projectId].value, // WRONG! uuid() returns UUID, not EntityID
|
|
25
|
+
userId = row[MyTable.userId].value, // WRONG!
|
|
26
|
+
)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Rule**: In `toEntity()` / `convert()` methods, ONLY `row[Table.id].value` gets `.value`. Every other column maps directly without `.value`.
|
|
30
|
+
|
|
31
|
+
### SoftDeleteTable Base Class
|
|
32
|
+
|
|
33
|
+
All tables extend `SoftDeleteTable` which gives these columns automatically:
|
|
34
|
+
- `id` (from UUIDTable) — `EntityID<UUID>`
|
|
35
|
+
- `createdAt` — `Instant`
|
|
36
|
+
- `updatedAt` — `Instant?`
|
|
37
|
+
- `deletedAt` — `Instant?`
|
|
38
|
+
|
|
39
|
+
**Do NOT re-declare these columns in table definitions.**
|
|
40
|
+
|
|
41
|
+
```kotlin
|
|
42
|
+
// CORRECT
|
|
43
|
+
object MyTable : SoftDeleteTable("my_table") {
|
|
44
|
+
val projectId = uuid("project_id")
|
|
45
|
+
val name = text("name")
|
|
46
|
+
// createdAt, updatedAt, deletedAt are inherited
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// WRONG — re-declares inherited columns
|
|
50
|
+
object MyTable : SoftDeleteTable("my_table") {
|
|
51
|
+
val projectId = uuid("project_id")
|
|
52
|
+
val name = text("name")
|
|
53
|
+
val createdAt = timestamp("created_at") // Already in SoftDeleteTable!
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Entity Must Include All SoftDeleteTable Fields
|
|
58
|
+
|
|
59
|
+
Entity data classes MUST implement `Entity<Instant>` and include all fields from `SoftDeleteTable`:
|
|
60
|
+
|
|
61
|
+
```kotlin
|
|
62
|
+
data class MyEntity(
|
|
63
|
+
override val id: UUID = UUID.randomUUID(),
|
|
64
|
+
val projectId: UUID,
|
|
65
|
+
val name: String,
|
|
66
|
+
// ... domain fields ...
|
|
67
|
+
override val createdAt: Instant = Instant.now(),
|
|
68
|
+
override val updatedAt: Instant? = null,
|
|
69
|
+
override val deletedAt: Instant? = null
|
|
70
|
+
) : Entity<Instant>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### toEntity() Must Map ALL Fields
|
|
74
|
+
|
|
75
|
+
When writing `toEntity()` / `convert()`, map EVERY column including SoftDeleteTable fields:
|
|
76
|
+
|
|
77
|
+
```kotlin
|
|
78
|
+
private fun toEntity(row: ResultRow) = MyEntity(
|
|
79
|
+
id = row[MyTable.id].value, // .value ONLY for id
|
|
80
|
+
projectId = row[MyTable.projectId], // NO .value
|
|
81
|
+
name = row[MyTable.name],
|
|
82
|
+
createdAt = row[MyTable.createdAt],
|
|
83
|
+
updatedAt = row[MyTable.updatedAt],
|
|
84
|
+
deletedAt = row[MyTable.deletedAt]
|
|
85
|
+
)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Always Filter by `deletedAt.isNull()` for Soft Delete
|
|
89
|
+
|
|
90
|
+
Every SELECT query on a SoftDeleteTable MUST filter out soft-deleted records:
|
|
91
|
+
|
|
92
|
+
```kotlin
|
|
93
|
+
// CORRECT
|
|
94
|
+
fun byId(id: UUID): MyEntity? {
|
|
95
|
+
return transaction(db.replica) {
|
|
96
|
+
MyTable.selectAll()
|
|
97
|
+
.where { MyTable.id eq id and MyTable.deletedAt.isNull() }
|
|
98
|
+
.singleOrNull()
|
|
99
|
+
?.let { toEntity(it) }
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// WRONG — returns soft-deleted records
|
|
104
|
+
fun byId(id: UUID): MyEntity? {
|
|
105
|
+
return transaction(db.replica) {
|
|
106
|
+
MyTable.selectAll()
|
|
107
|
+
.where { MyTable.id eq id } // Missing deletedAt.isNull()!
|
|
108
|
+
.singleOrNull()
|
|
109
|
+
?.let { toEntity(it) }
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### orderBy Uses `SortOrder.DESC` / `SortOrder.ASC`
|
|
115
|
+
|
|
116
|
+
```kotlin
|
|
117
|
+
import org.jetbrains.exposed.sql.SortOrder
|
|
118
|
+
|
|
119
|
+
// CORRECT
|
|
120
|
+
.orderBy(MyTable.createdAt, SortOrder.DESC)
|
|
121
|
+
|
|
122
|
+
// WRONG — causes compilation error
|
|
123
|
+
.orderBy(MyTable.createdAt to false)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Chaining WHERE Conditions
|
|
127
|
+
|
|
128
|
+
Calling `.where {}` twice **replaces** the first condition. Use `.andWhere {}` to add conditions:
|
|
129
|
+
|
|
130
|
+
```kotlin
|
|
131
|
+
// CORRECT — andWhere adds to existing condition
|
|
132
|
+
MyTable.selectAll()
|
|
133
|
+
.where { MyTable.deletedAt.isNull() }
|
|
134
|
+
.andWhere { MyTable.status eq "active" }
|
|
135
|
+
|
|
136
|
+
// WRONG — second where replaces the deletedAt filter!
|
|
137
|
+
MyTable.selectAll()
|
|
138
|
+
.where { MyTable.deletedAt.isNull() }
|
|
139
|
+
.where { MyTable.status eq "active" } // REPLACES the first where!
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Transaction Usage
|
|
143
|
+
|
|
144
|
+
- **Reads**: `transaction(db.replica) { ... }`
|
|
145
|
+
- **Writes**: `transaction(db.primary) { ... }`
|
|
146
|
+
|
|
147
|
+
### insert() Must Include ALL Required Entity Fields
|
|
148
|
+
|
|
149
|
+
When inserting, map EVERY field from the entity to the table columns, including `deletedAt`:
|
|
150
|
+
|
|
151
|
+
```kotlin
|
|
152
|
+
fun insert(entity: MyEntity): MyEntity {
|
|
153
|
+
return transaction(db.primary) {
|
|
154
|
+
MyTable.insert {
|
|
155
|
+
it[id] = entity.id
|
|
156
|
+
it[projectId] = entity.projectId
|
|
157
|
+
it[name] = entity.name
|
|
158
|
+
// ... all domain fields ...
|
|
159
|
+
it[createdAt] = entity.createdAt
|
|
160
|
+
it[updatedAt] = entity.updatedAt
|
|
161
|
+
it[deletedAt] = entity.deletedAt
|
|
162
|
+
}
|
|
163
|
+
entity
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Field Names Must Match Entity/Table Exactly
|
|
169
|
+
|
|
170
|
+
When writing repositories, ALWAYS cross-reference the Table and Entity files:
|
|
171
|
+
- Table file: `module-repository/.../table/MyTable.kt` — defines column names
|
|
172
|
+
- Entity file: `module-repository/.../entity/MyEntity.kt` — defines property names
|
|
173
|
+
|
|
174
|
+
**Common mistakes to avoid:**
|
|
175
|
+
- Using field names from a different service (e.g., `triggeredBy` when the table uses `deployedBy`)
|
|
176
|
+
- Guessing field names instead of reading the actual Table/Entity definitions
|
|
177
|
+
- Missing fields that exist in the Entity but weren't mapped in insert/toEntity
|
|
178
|
+
|
|
179
|
+
### Ktlint Rules for DB Code
|
|
180
|
+
|
|
181
|
+
- **Always use braces for if/else** — Ktlint enforces braces on all `if`/`else` blocks, including single-expression ones:
|
|
182
|
+
|
|
183
|
+
```kotlin
|
|
184
|
+
// CORRECT
|
|
185
|
+
val result = if (condition) {
|
|
186
|
+
valueA
|
|
187
|
+
} else {
|
|
188
|
+
valueB
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// WRONG — ktlint error: "Missing { ... }"
|
|
192
|
+
val result = if (condition) {
|
|
193
|
+
valueA
|
|
194
|
+
} else valueB
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
- **Data classes must have at least one parameter** — If a request/DTO truly has no fields, use a regular class or add an optional field:
|
|
198
|
+
|
|
199
|
+
```kotlin
|
|
200
|
+
// CORRECT
|
|
201
|
+
data class StartRequest(
|
|
202
|
+
val notes: String? = null
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
// WRONG — compilation error
|
|
206
|
+
data class StartRequest(
|
|
207
|
+
// empty body
|
|
208
|
+
)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Repository Pattern
|
|
214
|
+
|
|
215
|
+
### Creating a Repository
|
|
216
|
+
You MUST create a repository for each table with:
|
|
217
|
+
|
|
218
|
+
1. **Interface Definition**
|
|
219
|
+
- Define all CRUD operations needed
|
|
220
|
+
- Use domain-specific method names (e.g., `byEmail`, `byToken`)
|
|
221
|
+
- Return entities, not raw database rows
|
|
222
|
+
- Support soft delete operations
|
|
223
|
+
|
|
224
|
+
2. **Implementation Class**
|
|
225
|
+
- Name: `Default{TableName}Repository`
|
|
226
|
+
- Constructor: Accept `DatabaseContext`
|
|
227
|
+
- Use transactions: `db.primary` for writes, `db.replica` for reads
|
|
228
|
+
- Use soft delete (update `deletedAt`) not hard delete
|
|
229
|
+
|
|
230
|
+
3. **Standard Methods**
|
|
231
|
+
```kotlin
|
|
232
|
+
fun insert(entity: Entity): Entity
|
|
233
|
+
fun update(id: UUID, ...fields): Entity?
|
|
234
|
+
fun byId(id: UUID): Entity?
|
|
235
|
+
fun byIds(ids: List<UUID>): List<Entity>
|
|
236
|
+
fun deleteById(id: UUID): Entity? // Soft delete
|
|
237
|
+
fun restoreById(id: UUID): Entity? // Restore soft deleted
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
4. **Query Patterns**
|
|
241
|
+
- Always check `deletedAt.isNull()` for active records
|
|
242
|
+
- Update `updatedAt` on every modification
|
|
243
|
+
- Use `convert()` method to transform ResultRow to Entity
|
|
244
|
+
- Handle empty lists gracefully
|
|
245
|
+
|
|
246
|
+
### Repository File Structure
|
|
247
|
+
```kotlin
|
|
248
|
+
interface {TableName}Repository {
|
|
249
|
+
// Method declarations
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
class Default{TableName}Repository(
|
|
253
|
+
private val db: DatabaseContext
|
|
254
|
+
) : {TableName}Repository {
|
|
255
|
+
|
|
256
|
+
// Method implementations
|
|
257
|
+
|
|
258
|
+
private fun convert(row: ResultRow): Entity = Entity(
|
|
259
|
+
// Map all columns to entity properties
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## Testing
|
|
267
|
+
|
|
268
|
+
> Full guide: use `/testing-strategies` skill
|
|
269
|
+
|
|
270
|
+
**Key rules:**
|
|
271
|
+
- Extend `AbstractRepositoryTest`. Name: `Default{TableName}RepositoryTest`
|
|
272
|
+
- Required coverage: insert, update, byId (exists/not exists/soft deleted), deleteById, restoreById
|
|
273
|
+
- Use `dummyEntity()` helper with defaults. Use AssertJ assertions.
|
|
274
|
+
- Always run `./gradlew :module-repository:test` after modifying repository code
|
|
275
|
+
- Test edge cases: null optionals, empty lists, non-existent IDs, already-deleted entities
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## Checklist Before Writing Repository Code
|
|
280
|
+
|
|
281
|
+
- [ ] Read the Table file to know exact column names and types
|
|
282
|
+
- [ ] Read the Entity file to know exact property names and types
|
|
283
|
+
- [ ] Only `.value` on `id` column, never on other uuid columns
|
|
284
|
+
- [ ] Include `deletedAt.isNull()` in ALL select queries
|
|
285
|
+
- [ ] Use `SortOrder.DESC`/`SortOrder.ASC` for orderBy (import `org.jetbrains.exposed.sql.SortOrder`)
|
|
286
|
+
- [ ] Map ALL fields in `insert()` including `createdAt`, `updatedAt`, `deletedAt`
|
|
287
|
+
- [ ] Map ALL fields in `toEntity()` including `createdAt`, `updatedAt`, `deletedAt`
|
|
288
|
+
- [ ] Use `db.primary` for writes, `db.replica` for reads
|
|
289
|
+
- [ ] Always use braces for if/else blocks (ktlint)
|