@pattern-stack/codegen 0.2.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 +67 -0
- package/README.md +214 -0
- package/dist/runtime/analytics/index.d.ts +6 -0
- package/dist/runtime/analytics/index.js +49 -0
- package/dist/runtime/analytics/index.js.map +1 -0
- package/dist/runtime/analytics/metrics.d.ts +75 -0
- package/dist/runtime/analytics/metrics.js +1 -0
- package/dist/runtime/analytics/metrics.js.map +1 -0
- package/dist/runtime/analytics/packs/crm-entity-measures.d.ts +21 -0
- package/dist/runtime/analytics/packs/crm-entity-measures.js +1 -0
- package/dist/runtime/analytics/packs/crm-entity-measures.js.map +1 -0
- package/dist/runtime/analytics/packs/index.d.ts +3 -0
- package/dist/runtime/analytics/packs/index.js +1 -0
- package/dist/runtime/analytics/packs/index.js.map +1 -0
- package/dist/runtime/analytics/packs/monetary-measures.d.ts +21 -0
- package/dist/runtime/analytics/packs/monetary-measures.js +1 -0
- package/dist/runtime/analytics/packs/monetary-measures.js.map +1 -0
- package/dist/runtime/analytics/specs.d.ts +49 -0
- package/dist/runtime/analytics/specs.js +1 -0
- package/dist/runtime/analytics/specs.js.map +1 -0
- package/dist/runtime/analytics/types.d.ts +85 -0
- package/dist/runtime/analytics/types.js +49 -0
- package/dist/runtime/analytics/types.js.map +1 -0
- package/dist/runtime/base-classes/activity-entity-repository.d.ts +26 -0
- package/dist/runtime/base-classes/activity-entity-repository.js +195 -0
- package/dist/runtime/base-classes/activity-entity-repository.js.map +1 -0
- package/dist/runtime/base-classes/activity-entity-service.d.ts +39 -0
- package/dist/runtime/base-classes/activity-entity-service.js +214 -0
- package/dist/runtime/base-classes/activity-entity-service.js.map +1 -0
- package/dist/runtime/base-classes/base-read-use-cases.d.ts +68 -0
- package/dist/runtime/base-classes/base-read-use-cases.js +32 -0
- package/dist/runtime/base-classes/base-read-use-cases.js.map +1 -0
- package/dist/runtime/base-classes/base-repository.d.ts +99 -0
- package/dist/runtime/base-classes/base-repository.js +160 -0
- package/dist/runtime/base-classes/base-repository.js.map +1 -0
- package/dist/runtime/base-classes/base-service.d.ts +98 -0
- package/dist/runtime/base-classes/base-service.js +186 -0
- package/dist/runtime/base-classes/base-service.js.map +1 -0
- package/dist/runtime/base-classes/index.d.ts +18 -0
- package/dist/runtime/base-classes/index.js +617 -0
- package/dist/runtime/base-classes/index.js.map +1 -0
- package/dist/runtime/base-classes/knowledge-entity-repository.d.ts +17 -0
- package/dist/runtime/base-classes/knowledge-entity-repository.js +166 -0
- package/dist/runtime/base-classes/knowledge-entity-repository.js.map +1 -0
- package/dist/runtime/base-classes/knowledge-entity-service.d.ts +15 -0
- package/dist/runtime/base-classes/knowledge-entity-service.js +192 -0
- package/dist/runtime/base-classes/knowledge-entity-service.js.map +1 -0
- package/dist/runtime/base-classes/lifecycle-events.d.ts +49 -0
- package/dist/runtime/base-classes/lifecycle-events.js +76 -0
- package/dist/runtime/base-classes/lifecycle-events.js.map +1 -0
- package/dist/runtime/base-classes/metadata-entity-repository.d.ts +27 -0
- package/dist/runtime/base-classes/metadata-entity-repository.js +212 -0
- package/dist/runtime/base-classes/metadata-entity-repository.js.map +1 -0
- package/dist/runtime/base-classes/metadata-entity-service.d.ts +39 -0
- package/dist/runtime/base-classes/metadata-entity-service.js +214 -0
- package/dist/runtime/base-classes/metadata-entity-service.js.map +1 -0
- package/dist/runtime/base-classes/synced-entity-repository.d.ts +32 -0
- package/dist/runtime/base-classes/synced-entity-repository.js +203 -0
- package/dist/runtime/base-classes/synced-entity-repository.js.map +1 -0
- package/dist/runtime/base-classes/synced-entity-service.d.ts +41 -0
- package/dist/runtime/base-classes/synced-entity-service.js +215 -0
- package/dist/runtime/base-classes/synced-entity-service.js.map +1 -0
- package/dist/runtime/base-classes/with-analytics.d.ts +18 -0
- package/dist/runtime/base-classes/with-analytics.js +11 -0
- package/dist/runtime/base-classes/with-analytics.js.map +1 -0
- package/dist/runtime/constants/tokens.d.ts +29 -0
- package/dist/runtime/constants/tokens.js +8 -0
- package/dist/runtime/constants/tokens.js.map +1 -0
- package/dist/runtime/subsystems/analytics/analytics-query.protocol.d.ts +30 -0
- package/dist/runtime/subsystems/analytics/analytics-query.protocol.js +1 -0
- package/dist/runtime/subsystems/analytics/analytics-query.protocol.js.map +1 -0
- package/dist/runtime/subsystems/analytics/analytics.module.d.ts +34 -0
- package/dist/runtime/subsystems/analytics/analytics.module.js +117 -0
- package/dist/runtime/subsystems/analytics/analytics.module.js.map +1 -0
- package/dist/runtime/subsystems/analytics/analytics.tokens.d.ts +24 -0
- package/dist/runtime/subsystems/analytics/analytics.tokens.js +10 -0
- package/dist/runtime/subsystems/analytics/analytics.tokens.js.map +1 -0
- package/dist/runtime/subsystems/analytics/cube-backend.d.ts +28 -0
- package/dist/runtime/subsystems/analytics/cube-backend.js +71 -0
- package/dist/runtime/subsystems/analytics/cube-backend.js.map +1 -0
- package/dist/runtime/subsystems/analytics/index.d.ts +6 -0
- package/dist/runtime/subsystems/analytics/index.js +122 -0
- package/dist/runtime/subsystems/analytics/index.js.map +1 -0
- package/dist/runtime/subsystems/analytics/noop-backend.d.ts +7 -0
- package/dist/runtime/subsystems/analytics/noop-backend.js +25 -0
- package/dist/runtime/subsystems/analytics/noop-backend.js.map +1 -0
- package/dist/runtime/subsystems/cache/cache.drizzle-backend.d.ts +43 -0
- package/dist/runtime/subsystems/cache/cache.drizzle-backend.js +133 -0
- package/dist/runtime/subsystems/cache/cache.drizzle-backend.js.map +1 -0
- package/dist/runtime/subsystems/cache/cache.memory-backend.d.ts +21 -0
- package/dist/runtime/subsystems/cache/cache.memory-backend.js +100 -0
- package/dist/runtime/subsystems/cache/cache.memory-backend.js.map +1 -0
- package/dist/runtime/subsystems/cache/cache.module.d.ts +37 -0
- package/dist/runtime/subsystems/cache/cache.module.js +272 -0
- package/dist/runtime/subsystems/cache/cache.module.js.map +1 -0
- package/dist/runtime/subsystems/cache/cache.protocol.d.ts +42 -0
- package/dist/runtime/subsystems/cache/cache.protocol.js +1 -0
- package/dist/runtime/subsystems/cache/cache.protocol.js.map +1 -0
- package/dist/runtime/subsystems/cache/cache.schema.d.ts +64 -0
- package/dist/runtime/subsystems/cache/cache.schema.js +18 -0
- package/dist/runtime/subsystems/cache/cache.schema.js.map +1 -0
- package/dist/runtime/subsystems/cache/cache.tokens.d.ts +18 -0
- package/dist/runtime/subsystems/cache/cache.tokens.js +8 -0
- package/dist/runtime/subsystems/cache/cache.tokens.js.map +1 -0
- package/dist/runtime/subsystems/cache/index.d.ts +11 -0
- package/dist/runtime/subsystems/cache/index.js +277 -0
- package/dist/runtime/subsystems/cache/index.js.map +1 -0
- package/dist/runtime/subsystems/events/domain-events.schema.d.ts +187 -0
- package/dist/runtime/subsystems/events/domain-events.schema.js +32 -0
- package/dist/runtime/subsystems/events/domain-events.schema.js.map +1 -0
- package/dist/runtime/subsystems/events/event-bus.drizzle-backend.d.ts +38 -0
- package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js +199 -0
- package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js.map +1 -0
- package/dist/runtime/subsystems/events/event-bus.memory-backend.d.ts +18 -0
- package/dist/runtime/subsystems/events/event-bus.memory-backend.js +71 -0
- package/dist/runtime/subsystems/events/event-bus.memory-backend.js.map +1 -0
- package/dist/runtime/subsystems/events/event-bus.protocol.d.ts +52 -0
- package/dist/runtime/subsystems/events/event-bus.protocol.js +1 -0
- package/dist/runtime/subsystems/events/event-bus.protocol.js.map +1 -0
- package/dist/runtime/subsystems/events/event-bus.redis-backend.d.ts +95 -0
- package/dist/runtime/subsystems/events/event-bus.redis-backend.js +229 -0
- package/dist/runtime/subsystems/events/event-bus.redis-backend.js.map +1 -0
- package/dist/runtime/subsystems/events/events.module.d.ts +46 -0
- package/dist/runtime/subsystems/events/events.module.js +531 -0
- package/dist/runtime/subsystems/events/events.module.js.map +1 -0
- package/dist/runtime/subsystems/events/events.tokens.d.ts +19 -0
- package/dist/runtime/subsystems/events/events.tokens.js +8 -0
- package/dist/runtime/subsystems/events/events.tokens.js.map +1 -0
- package/dist/runtime/subsystems/events/index.d.ts +12 -0
- package/dist/runtime/subsystems/events/index.js +536 -0
- package/dist/runtime/subsystems/events/index.js.map +1 -0
- package/dist/runtime/subsystems/index.d.ts +24 -0
- package/dist/runtime/subsystems/index.js +1643 -0
- package/dist/runtime/subsystems/index.js.map +1 -0
- package/dist/runtime/subsystems/jobs/index.d.ts +14 -0
- package/dist/runtime/subsystems/jobs/index.js +680 -0
- package/dist/runtime/subsystems/jobs/index.js.map +1 -0
- package/dist/runtime/subsystems/jobs/job-queue.bullmq-backend.d.ts +54 -0
- package/dist/runtime/subsystems/jobs/job-queue.bullmq-backend.js +186 -0
- package/dist/runtime/subsystems/jobs/job-queue.bullmq-backend.js.map +1 -0
- package/dist/runtime/subsystems/jobs/job-queue.drizzle-backend.d.ts +38 -0
- package/dist/runtime/subsystems/jobs/job-queue.drizzle-backend.js +228 -0
- package/dist/runtime/subsystems/jobs/job-queue.drizzle-backend.js.map +1 -0
- package/dist/runtime/subsystems/jobs/job-queue.memory-backend.d.ts +12 -0
- package/dist/runtime/subsystems/jobs/job-queue.memory-backend.js +44 -0
- package/dist/runtime/subsystems/jobs/job-queue.memory-backend.js.map +1 -0
- package/dist/runtime/subsystems/jobs/job-queue.protocol.d.ts +48 -0
- package/dist/runtime/subsystems/jobs/job-queue.protocol.js +1 -0
- package/dist/runtime/subsystems/jobs/job-queue.protocol.js.map +1 -0
- package/dist/runtime/subsystems/jobs/job-queue.redis-backend.d.ts +46 -0
- package/dist/runtime/subsystems/jobs/job-queue.redis-backend.js +187 -0
- package/dist/runtime/subsystems/jobs/job-queue.redis-backend.js.map +1 -0
- package/dist/runtime/subsystems/jobs/job-queue.schema.d.ts +237 -0
- package/dist/runtime/subsystems/jobs/job-queue.schema.js +44 -0
- package/dist/runtime/subsystems/jobs/job-queue.schema.js.map +1 -0
- package/dist/runtime/subsystems/jobs/jobs.module.d.ts +18 -0
- package/dist/runtime/subsystems/jobs/jobs.module.js +676 -0
- package/dist/runtime/subsystems/jobs/jobs.module.js.map +1 -0
- package/dist/runtime/subsystems/jobs/jobs.tokens.d.ts +13 -0
- package/dist/runtime/subsystems/jobs/jobs.tokens.js +8 -0
- package/dist/runtime/subsystems/jobs/jobs.tokens.js.map +1 -0
- package/dist/runtime/subsystems/storage/index.d.ts +6 -0
- package/dist/runtime/subsystems/storage/index.js +204 -0
- package/dist/runtime/subsystems/storage/index.js.map +1 -0
- package/dist/runtime/subsystems/storage/storage.local-backend.d.ts +18 -0
- package/dist/runtime/subsystems/storage/storage.local-backend.js +108 -0
- package/dist/runtime/subsystems/storage/storage.local-backend.js.map +1 -0
- package/dist/runtime/subsystems/storage/storage.memory-backend.d.ts +28 -0
- package/dist/runtime/subsystems/storage/storage.memory-backend.js +72 -0
- package/dist/runtime/subsystems/storage/storage.memory-backend.js.map +1 -0
- package/dist/runtime/subsystems/storage/storage.module.d.ts +40 -0
- package/dist/runtime/subsystems/storage/storage.module.js +201 -0
- package/dist/runtime/subsystems/storage/storage.module.js.map +1 -0
- package/dist/runtime/subsystems/storage/storage.protocol.d.ts +69 -0
- package/dist/runtime/subsystems/storage/storage.protocol.js +1 -0
- package/dist/runtime/subsystems/storage/storage.protocol.js.map +1 -0
- package/dist/runtime/subsystems/storage/storage.tokens.d.ts +11 -0
- package/dist/runtime/subsystems/storage/storage.tokens.js +6 -0
- package/dist/runtime/subsystems/storage/storage.tokens.js.map +1 -0
- package/dist/runtime/subsystems/storage/storage.utils.d.ts +9 -0
- package/dist/runtime/subsystems/storage/storage.utils.js +18 -0
- package/dist/runtime/subsystems/storage/storage.utils.js.map +1 -0
- package/dist/runtime/types/drizzle.d.ts +17 -0
- package/dist/runtime/types/drizzle.js +1 -0
- package/dist/runtime/types/drizzle.js.map +1 -0
- package/dist/src/cli/index.d.ts +1 -0
- package/dist/src/cli/index.js +7365 -0
- package/dist/src/cli/index.js.map +1 -0
- package/dist/src/index.d.ts +2384 -0
- package/dist/src/index.js +2198 -0
- package/dist/src/index.js.map +1 -0
- package/package.json +114 -0
- package/templates/broadcast/new/backend-interface.ejs.t +47 -0
- package/templates/broadcast/new/bridge-listener.ejs.t +67 -0
- package/templates/broadcast/new/channel.ejs.t +77 -0
- package/templates/broadcast/new/index.ejs.t +21 -0
- package/templates/broadcast/new/memory-backend.ejs.t +87 -0
- package/templates/broadcast/new/module.ejs.t +57 -0
- package/templates/broadcast/new/prompt.js +268 -0
- package/templates/broadcast/new/websocket-backend.ejs.t +259 -0
- package/templates/entity/new/backend/application/commands/create.ejs.t +55 -0
- package/templates/entity/new/backend/application/commands/delete.ejs.t +45 -0
- package/templates/entity/new/backend/application/commands/grouped-index.ejs.t +149 -0
- package/templates/entity/new/backend/application/commands/index.ejs.t +15 -0
- package/templates/entity/new/backend/application/commands/update.ejs.t +58 -0
- package/templates/entity/new/backend/application/queries/declarative-queries.ejs.t +36 -0
- package/templates/entity/new/backend/application/queries/get-by-id.ejs.t +42 -0
- package/templates/entity/new/backend/application/queries/grouped-index.ejs.t +81 -0
- package/templates/entity/new/backend/application/queries/index.ejs.t +14 -0
- package/templates/entity/new/backend/application/queries/list.ejs.t +36 -0
- package/templates/entity/new/backend/application/schemas/_inject-index.ejs.t +7 -0
- package/templates/entity/new/backend/application/schemas/dto.ejs.t +45 -0
- package/templates/entity/new/backend/database/_inject-index.ejs.t +7 -0
- package/templates/entity/new/backend/database/electric-migration.ejs.t +21 -0
- package/templates/entity/new/backend/database/repository.ejs.t +450 -0
- package/templates/entity/new/backend/database/schema.ejs.t +248 -0
- package/templates/entity/new/backend/domain/_inject-index.ejs.t +12 -0
- package/templates/entity/new/backend/domain/entity.ejs.t +108 -0
- package/templates/entity/new/backend/domain/grouped-index.ejs.t +163 -0
- package/templates/entity/new/backend/domain/index.ejs.t +15 -0
- package/templates/entity/new/backend/domain/repository-interface.ejs.t +71 -0
- package/templates/entity/new/backend/modules/core/_ensure-anchor-tokens.ejs.t +10 -0
- package/templates/entity/new/backend/modules/core/_inject-token.ejs.t +7 -0
- package/templates/entity/new/backend/modules/core/module.ejs.t +67 -0
- package/templates/entity/new/backend/modules/trpc/module.ejs.t +67 -0
- package/templates/entity/new/backend/presentation/controller.ejs.t +201 -0
- package/templates/entity/new/clean-lite-ps/controller.ejs.t +37 -0
- package/templates/entity/new/clean-lite-ps/dto/create.ejs.t +17 -0
- package/templates/entity/new/clean-lite-ps/dto/output.ejs.t +25 -0
- package/templates/entity/new/clean-lite-ps/dto/update.ejs.t +11 -0
- package/templates/entity/new/clean-lite-ps/entity.ejs.t +52 -0
- package/templates/entity/new/clean-lite-ps/index.ejs.t +20 -0
- package/templates/entity/new/clean-lite-ps/module.ejs.t +43 -0
- package/templates/entity/new/clean-lite-ps/prompt-extension.js +617 -0
- package/templates/entity/new/clean-lite-ps/repository.ejs.t +62 -0
- package/templates/entity/new/clean-lite-ps/service.ejs.t +34 -0
- package/templates/entity/new/clean-lite-ps/use-cases/declarative-queries.ejs.t +34 -0
- package/templates/entity/new/clean-lite-ps/use-cases/find-by-id.ejs.t +16 -0
- package/templates/entity/new/clean-lite-ps/use-cases/list.ejs.t +16 -0
- package/templates/entity/new/frontend/_inject-entities-entry.ejs.t +7 -0
- package/templates/entity/new/frontend/_inject-entities-import.ejs.t +7 -0
- package/templates/entity/new/frontend/collections/_ensure-anchor-collections.ejs.t +10 -0
- package/templates/entity/new/frontend/collections/_inject-index.ejs.t +9 -0
- package/templates/entity/new/frontend/collections/_inject-schema-import.ejs.t +9 -0
- package/templates/entity/new/frontend/collections/collection.ejs.t +61 -0
- package/templates/entity/new/frontend/collections/collections-base.ejs.t +24 -0
- package/templates/entity/new/frontend/entity/collection.ejs.t +172 -0
- package/templates/entity/new/frontend/entity/combined.ejs.t +474 -0
- package/templates/entity/new/frontend/entity/fields.ejs.t +104 -0
- package/templates/entity/new/frontend/entity/hooks.ejs.t +73 -0
- package/templates/entity/new/frontend/entity/index.ejs.t +21 -0
- package/templates/entity/new/frontend/entity/mutation-hooks.ejs.t +84 -0
- package/templates/entity/new/frontend/entity/mutations.ejs.t +38 -0
- package/templates/entity/new/frontend/entity/types.ejs.t +59 -0
- package/templates/entity/new/frontend/generated/_inject-index-export.ejs.t +7 -0
- package/templates/entity/new/frontend/generated/_inject-index-import.ejs.t +7 -0
- package/templates/entity/new/frontend/generated/_inject-index-registry.ejs.t +7 -0
- package/templates/entity/new/frontend/store/_inject-collection-import.ejs.t +9 -0
- package/templates/entity/new/frontend/store/_inject-collections.ejs.t +9 -0
- package/templates/entity/new/frontend/store/_inject-entity.ejs.t +9 -0
- package/templates/entity/new/frontend/store/_inject-import.ejs.t +9 -0
- package/templates/entity/new/frontend/store/_inject-lookups.ejs.t +9 -0
- package/templates/entity/new/frontend/store/_inject-resolve.ejs.t +10 -0
- package/templates/entity/new/frontend/store/hooks.ejs.t +72 -0
- package/templates/entity/new/frontend/unified-entity.ejs.t +28 -0
- package/templates/entity/new/prompt.js +1421 -0
- package/templates/relationship/new/controller.ejs.t +36 -0
- package/templates/relationship/new/dto/create.ejs.t +41 -0
- package/templates/relationship/new/dto/output.ejs.t +44 -0
- package/templates/relationship/new/dto/update.ejs.t +10 -0
- package/templates/relationship/new/entity.ejs.t +98 -0
- package/templates/relationship/new/index.ejs.t +19 -0
- package/templates/relationship/new/module.ejs.t +35 -0
- package/templates/relationship/new/prompt.js +682 -0
- package/templates/relationship/new/repository.ejs.t +54 -0
- package/templates/relationship/new/service.ejs.t +31 -0
- package/templates/relationship/new/use-cases/declarative-queries.ejs.t +34 -0
- package/templates/relationship/new/use-cases/find-by-id.ejs.t +16 -0
- package/templates/relationship/new/use-cases/list.ejs.t +16 -0
|
@@ -0,0 +1,1421 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hygen prompt.js - Loads entity YAML and prepares template locals
|
|
3
|
+
*
|
|
4
|
+
* Usage: bunx hygen entity new --yaml entities/opportunity.yaml
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import yaml from "yaml";
|
|
10
|
+
import {
|
|
11
|
+
BACKEND_LAYERS,
|
|
12
|
+
BASE_PATHS,
|
|
13
|
+
FOLDER_STRUCTURES,
|
|
14
|
+
FILE_GROUPINGS,
|
|
15
|
+
LOCATIONS,
|
|
16
|
+
getEntityPaths,
|
|
17
|
+
getEntityFileNames,
|
|
18
|
+
getImportPaths,
|
|
19
|
+
getLayoutConfig,
|
|
20
|
+
getDatabaseDialect,
|
|
21
|
+
getProjectConfig,
|
|
22
|
+
getPipelinesConfig,
|
|
23
|
+
getGenerateConfig,
|
|
24
|
+
} from "../../../src/config/paths.mjs";
|
|
25
|
+
import { getNamingConfig } from "../../../src/config/naming-config.mjs";
|
|
26
|
+
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// Behavior Registry (inline to avoid import issues with Hygen)
|
|
29
|
+
// ============================================================================
|
|
30
|
+
|
|
31
|
+
const behaviorRegistry = {
|
|
32
|
+
timestamps: {
|
|
33
|
+
name: "timestamps",
|
|
34
|
+
fields: [
|
|
35
|
+
{
|
|
36
|
+
name: "created_at",
|
|
37
|
+
camelName: "createdAt",
|
|
38
|
+
type: "datetime",
|
|
39
|
+
tsType: "Date",
|
|
40
|
+
drizzleType: "timestamp",
|
|
41
|
+
zodType: "z.coerce.date()",
|
|
42
|
+
nullable: false,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "updated_at",
|
|
46
|
+
camelName: "updatedAt",
|
|
47
|
+
type: "datetime",
|
|
48
|
+
tsType: "Date",
|
|
49
|
+
drizzleType: "timestamp",
|
|
50
|
+
zodType: "z.coerce.date()",
|
|
51
|
+
nullable: false,
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
drizzleImports: ["timestamp"],
|
|
55
|
+
configKey: "timestamps",
|
|
56
|
+
},
|
|
57
|
+
soft_delete: {
|
|
58
|
+
name: "soft_delete",
|
|
59
|
+
fields: [
|
|
60
|
+
{
|
|
61
|
+
name: "deleted_at",
|
|
62
|
+
camelName: "deletedAt",
|
|
63
|
+
type: "datetime",
|
|
64
|
+
tsType: "Date | null",
|
|
65
|
+
drizzleType: "timestamp",
|
|
66
|
+
zodType: "z.coerce.date().nullable()",
|
|
67
|
+
nullable: true,
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
drizzleImports: ["timestamp"],
|
|
71
|
+
configKey: "softDelete",
|
|
72
|
+
},
|
|
73
|
+
user_tracking: {
|
|
74
|
+
name: "user_tracking",
|
|
75
|
+
fields: [
|
|
76
|
+
{
|
|
77
|
+
name: "created_by",
|
|
78
|
+
camelName: "createdBy",
|
|
79
|
+
type: "uuid",
|
|
80
|
+
tsType: "string | null",
|
|
81
|
+
drizzleType: "uuid",
|
|
82
|
+
zodType: "z.string().uuid().nullable()",
|
|
83
|
+
nullable: true,
|
|
84
|
+
foreignKey: "users.id",
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: "updated_by",
|
|
88
|
+
camelName: "updatedBy",
|
|
89
|
+
type: "uuid",
|
|
90
|
+
tsType: "string | null",
|
|
91
|
+
drizzleType: "uuid",
|
|
92
|
+
zodType: "z.string().uuid().nullable()",
|
|
93
|
+
nullable: true,
|
|
94
|
+
foreignKey: "users.id",
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
drizzleImports: ["uuid"],
|
|
98
|
+
configKey: "userTracking",
|
|
99
|
+
},
|
|
100
|
+
temporal_validity: {
|
|
101
|
+
name: "temporal_validity",
|
|
102
|
+
fields: [
|
|
103
|
+
{
|
|
104
|
+
name: "valid_from",
|
|
105
|
+
camelName: "validFrom",
|
|
106
|
+
type: "datetime",
|
|
107
|
+
tsType: "Date | null",
|
|
108
|
+
drizzleType: "timestamp",
|
|
109
|
+
zodType: "z.coerce.date().nullable()",
|
|
110
|
+
nullable: true,
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: "valid_to",
|
|
114
|
+
camelName: "validTo",
|
|
115
|
+
type: "datetime",
|
|
116
|
+
tsType: "Date | null",
|
|
117
|
+
drizzleType: "timestamp",
|
|
118
|
+
zodType: "z.coerce.date().nullable()",
|
|
119
|
+
nullable: true,
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: "is_active",
|
|
123
|
+
camelName: "isActive",
|
|
124
|
+
type: "boolean",
|
|
125
|
+
tsType: "boolean",
|
|
126
|
+
drizzleType: "boolean",
|
|
127
|
+
zodType: "z.boolean()",
|
|
128
|
+
nullable: false,
|
|
129
|
+
default: true,
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
drizzleImports: ["timestamp", "boolean"],
|
|
133
|
+
configKey: "temporalValidity",
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Load codegen config from codegen.config.yaml
|
|
139
|
+
*/
|
|
140
|
+
function loadCodegenConfig(cwd) {
|
|
141
|
+
const configPath = path.resolve(cwd, "codegen.config.yaml");
|
|
142
|
+
const defaultConfig = { behaviors: { strategy: "inline" } };
|
|
143
|
+
|
|
144
|
+
if (!fs.existsSync(configPath)) {
|
|
145
|
+
return defaultConfig;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
150
|
+
const parsed = yaml.parse(content);
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
behaviors: {
|
|
154
|
+
strategy: parsed?.behaviors?.strategy || "inline",
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
} catch {
|
|
158
|
+
return defaultConfig;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Normalize behavior config (string or object with name/options)
|
|
164
|
+
*/
|
|
165
|
+
function normalizeBehaviorConfig(config) {
|
|
166
|
+
if (typeof config === "string") {
|
|
167
|
+
return { name: config, options: {} };
|
|
168
|
+
}
|
|
169
|
+
return { name: config.name, options: config.options || {} };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Resolve behaviors from entity YAML
|
|
174
|
+
*/
|
|
175
|
+
function resolveBehaviors(behaviorConfigs) {
|
|
176
|
+
const configs = (behaviorConfigs || []).map(normalizeBehaviorConfig);
|
|
177
|
+
const fields = [];
|
|
178
|
+
const drizzleImports = new Set();
|
|
179
|
+
const addedFieldNames = new Set();
|
|
180
|
+
|
|
181
|
+
const enabledNames = new Set(configs.map((c) => c.name));
|
|
182
|
+
|
|
183
|
+
for (const config of configs) {
|
|
184
|
+
const behavior = behaviorRegistry[config.name];
|
|
185
|
+
if (!behavior) continue;
|
|
186
|
+
|
|
187
|
+
for (const field of behavior.fields) {
|
|
188
|
+
if (!addedFieldNames.has(field.name)) {
|
|
189
|
+
fields.push(field);
|
|
190
|
+
addedFieldNames.add(field.name);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
for (const imp of behavior.drizzleImports) {
|
|
195
|
+
drizzleImports.add(imp);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const hasTimestamps = enabledNames.has("timestamps");
|
|
200
|
+
const hasSoftDelete = enabledNames.has("soft_delete");
|
|
201
|
+
const hasUserTracking = enabledNames.has("user_tracking");
|
|
202
|
+
const hasTemporalValidity = enabledNames.has("temporal_validity");
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
configs,
|
|
206
|
+
fields,
|
|
207
|
+
drizzleImports: Array.from(drizzleImports).sort(),
|
|
208
|
+
repositoryConfig: {
|
|
209
|
+
timestamps: hasTimestamps,
|
|
210
|
+
softDelete: hasSoftDelete,
|
|
211
|
+
userTracking: hasUserTracking,
|
|
212
|
+
temporalValidity: hasTemporalValidity,
|
|
213
|
+
versionable: false,
|
|
214
|
+
},
|
|
215
|
+
hasBehaviors: configs.length > 0,
|
|
216
|
+
hasTimestamps,
|
|
217
|
+
hasSoftDelete,
|
|
218
|
+
hasUserTracking,
|
|
219
|
+
hasTemporalValidity,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export default {
|
|
224
|
+
prompt: async ({ args }) => {
|
|
225
|
+
const yamlPath = args.yaml;
|
|
226
|
+
if (!yamlPath) {
|
|
227
|
+
throw new Error(
|
|
228
|
+
"Missing --yaml argument. Usage: bunx hygen entity new --yaml entities/opportunity.yaml",
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Load and parse YAML
|
|
233
|
+
const fullPath = path.resolve(process.cwd(), yamlPath);
|
|
234
|
+
if (!fs.existsSync(fullPath)) {
|
|
235
|
+
throw new Error(`File not found: ${fullPath}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
239
|
+
const definition = yaml.parse(content);
|
|
240
|
+
|
|
241
|
+
// Load global codegen config
|
|
242
|
+
const codegenConfig = loadCodegenConfig(process.cwd());
|
|
243
|
+
|
|
244
|
+
// Load frontend config from project config (used for auth, sync, parsers)
|
|
245
|
+
const frontendConfig = getProjectConfig()?.frontend ?? {};
|
|
246
|
+
const frontendSync = frontendConfig.sync ?? {};
|
|
247
|
+
|
|
248
|
+
// Prepare locals for templates
|
|
249
|
+
const entity = definition.entity;
|
|
250
|
+
const fields = definition.fields || {};
|
|
251
|
+
const relationships = definition.relationships || {};
|
|
252
|
+
const behaviors = definition.behaviors || [];
|
|
253
|
+
|
|
254
|
+
// v2 blocks (optional — absent in v1 entities)
|
|
255
|
+
const queriesBlock = definition.queries || null;
|
|
256
|
+
const syncBlock = definition.sync || null;
|
|
257
|
+
const eventsBlock = definition.events || null;
|
|
258
|
+
|
|
259
|
+
// Helper functions
|
|
260
|
+
const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
261
|
+
const camelCase = (s) => s.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
262
|
+
const pascalCase = (s) => capitalize(camelCase(s));
|
|
263
|
+
const pluralize = (s) => {
|
|
264
|
+
if (s.endsWith("y")) return s.slice(0, -1) + "ies";
|
|
265
|
+
if (
|
|
266
|
+
s.endsWith("s") ||
|
|
267
|
+
s.endsWith("x") ||
|
|
268
|
+
s.endsWith("ch") ||
|
|
269
|
+
s.endsWith("sh")
|
|
270
|
+
)
|
|
271
|
+
return s + "es";
|
|
272
|
+
return s + "s";
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// ============================================================================
|
|
276
|
+
// UI Metadata Inference Functions
|
|
277
|
+
// ============================================================================
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Format field name as human-readable label
|
|
281
|
+
* e.g., "created_at" -> "Created At", "account_id" -> "Account Id"
|
|
282
|
+
*/
|
|
283
|
+
const formatLabel = (fieldName) => {
|
|
284
|
+
return fieldName
|
|
285
|
+
.replace(/_/g, " ")
|
|
286
|
+
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Infer UI type from field definition
|
|
291
|
+
* Considers explicit ui_type, field type, choices, foreign keys, and name patterns
|
|
292
|
+
*/
|
|
293
|
+
const inferUiType = (fieldName, field) => {
|
|
294
|
+
// If explicit ui_type provided, use it
|
|
295
|
+
if (field.ui_type) return field.ui_type;
|
|
296
|
+
|
|
297
|
+
// Check for choices (enum)
|
|
298
|
+
if (Array.isArray(field.choices) && field.choices.length > 0)
|
|
299
|
+
return "enum";
|
|
300
|
+
|
|
301
|
+
// Check for foreign key (reference)
|
|
302
|
+
if (field.foreign_key) return "reference";
|
|
303
|
+
|
|
304
|
+
// Check field name patterns
|
|
305
|
+
const nameLower = fieldName.toLowerCase();
|
|
306
|
+
if (nameLower.includes("email")) return "email";
|
|
307
|
+
if (nameLower.includes("url") || nameLower.includes("website"))
|
|
308
|
+
return "url";
|
|
309
|
+
if (nameLower.includes("password")) return "password";
|
|
310
|
+
if (
|
|
311
|
+
nameLower.includes("price") ||
|
|
312
|
+
nameLower.includes("amount") ||
|
|
313
|
+
nameLower.includes("cost") ||
|
|
314
|
+
nameLower.includes("value") ||
|
|
315
|
+
nameLower.includes("revenue")
|
|
316
|
+
)
|
|
317
|
+
return "money";
|
|
318
|
+
if (nameLower.includes("percent") || nameLower.includes("rate"))
|
|
319
|
+
return "percentage";
|
|
320
|
+
|
|
321
|
+
// Infer from field type
|
|
322
|
+
const typeMap = {
|
|
323
|
+
string:
|
|
324
|
+
field.max_length && field.max_length > 500 ? "textarea" : "text",
|
|
325
|
+
integer: "number",
|
|
326
|
+
decimal: "number",
|
|
327
|
+
boolean: "boolean",
|
|
328
|
+
uuid: "text",
|
|
329
|
+
date: "date",
|
|
330
|
+
datetime: "datetime",
|
|
331
|
+
json: "json",
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
return typeMap[field.type] || "text";
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Infer UI group from field name patterns
|
|
339
|
+
*/
|
|
340
|
+
const inferUiGroup = (fieldName, field) => {
|
|
341
|
+
if (field.ui_group) return field.ui_group;
|
|
342
|
+
|
|
343
|
+
const nameLower = fieldName.toLowerCase();
|
|
344
|
+
|
|
345
|
+
// Common field groupings
|
|
346
|
+
if (["id", "uuid"].includes(nameLower)) return "identification";
|
|
347
|
+
if (["created_at", "updated_at", "deleted_at"].includes(nameLower))
|
|
348
|
+
return "metadata";
|
|
349
|
+
if (
|
|
350
|
+
nameLower.includes("price") ||
|
|
351
|
+
nameLower.includes("amount") ||
|
|
352
|
+
nameLower.includes("cost") ||
|
|
353
|
+
nameLower.includes("value") ||
|
|
354
|
+
nameLower.includes("revenue")
|
|
355
|
+
)
|
|
356
|
+
return "financial";
|
|
357
|
+
if (nameLower.includes("email") || nameLower.includes("phone"))
|
|
358
|
+
return "contact";
|
|
359
|
+
if (nameLower.includes("name") || nameLower.includes("title"))
|
|
360
|
+
return "identification";
|
|
361
|
+
if (nameLower.includes("description") || nameLower.includes("notes"))
|
|
362
|
+
return "content";
|
|
363
|
+
if (
|
|
364
|
+
nameLower.includes("status") ||
|
|
365
|
+
nameLower.includes("state") ||
|
|
366
|
+
nameLower.includes("stage")
|
|
367
|
+
)
|
|
368
|
+
return "status";
|
|
369
|
+
if (field.foreign_key) return "relationships";
|
|
370
|
+
|
|
371
|
+
return "general";
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Infer UI importance from field properties
|
|
376
|
+
*/
|
|
377
|
+
const inferUiImportance = (fieldName, field) => {
|
|
378
|
+
if (field.ui_importance) return field.ui_importance;
|
|
379
|
+
|
|
380
|
+
const nameLower = fieldName.toLowerCase();
|
|
381
|
+
|
|
382
|
+
// Auto-generated fields are tertiary
|
|
383
|
+
if (["id", "created_at", "updated_at", "deleted_at"].includes(nameLower))
|
|
384
|
+
return "tertiary";
|
|
385
|
+
|
|
386
|
+
// Foreign keys that are likely internal references
|
|
387
|
+
if (field.foreign_key && nameLower.endsWith("_id")) return "secondary";
|
|
388
|
+
|
|
389
|
+
// Required fields are primary by default
|
|
390
|
+
if (field.required) return "primary";
|
|
391
|
+
|
|
392
|
+
// Name/title fields are typically primary
|
|
393
|
+
if (nameLower.includes("name") || nameLower.includes("title"))
|
|
394
|
+
return "primary";
|
|
395
|
+
|
|
396
|
+
return "secondary";
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
// Entity name variations
|
|
400
|
+
const name = entity.name; // opportunity
|
|
401
|
+
const plural = entity.plural; // opportunities
|
|
402
|
+
const table = entity.table; // opportunities
|
|
403
|
+
const className = pascalCase(name); // Opportunity
|
|
404
|
+
const classNamePlural = pascalCase(plural); // Opportunities
|
|
405
|
+
const camelName = camelCase(name); // opportunity
|
|
406
|
+
const repositoryToken = `${pascalCase(name).toUpperCase()}_REPOSITORY`; // OPPORTUNITY_REPOSITORY
|
|
407
|
+
|
|
408
|
+
// Frontend store naming
|
|
409
|
+
const singularCamelName = camelCase(name); // "dealState" from "deal_state"
|
|
410
|
+
const pluralCamelName = camelCase(plural); // "dealStates" from "deal_states"
|
|
411
|
+
const collectionVarName = singularCamelName + "Collection"; // "dealStateCollection"
|
|
412
|
+
const collectionVarNamePlural = pluralCamelName + "Collection"; // "dealStatesCollection"
|
|
413
|
+
|
|
414
|
+
// Layout configuration (folder structure + file grouping)
|
|
415
|
+
// See tools/codegen/config/paths.js for options
|
|
416
|
+
const layout = getLayoutConfig(entity);
|
|
417
|
+
const { folderStructure, fileGrouping, isNested, isGrouped } = layout;
|
|
418
|
+
|
|
419
|
+
// Behavior strategy (base_class vs inline)
|
|
420
|
+
// Per-entity override takes precedence over global config
|
|
421
|
+
const behaviorStrategy =
|
|
422
|
+
entity.behavior_strategy || codegenConfig.behaviors.strategy;
|
|
423
|
+
|
|
424
|
+
// Resolve behaviors
|
|
425
|
+
const resolvedBehaviors = resolveBehaviors(behaviors);
|
|
426
|
+
|
|
427
|
+
// Compute paths using centralized config
|
|
428
|
+
// See tools/codegen/config/paths.js for path definitions
|
|
429
|
+
const paths = getEntityPaths({ name, plural, isNested, isGrouped });
|
|
430
|
+
|
|
431
|
+
// Load naming configuration for file naming
|
|
432
|
+
const namingConfig = getNamingConfig();
|
|
433
|
+
|
|
434
|
+
// File names using centralized config with naming configuration
|
|
435
|
+
const fileNames = getEntityFileNames({ name, plural, isNested, isGrouped, namingConfig });
|
|
436
|
+
|
|
437
|
+
// Terminology-aware class name suffixes
|
|
438
|
+
// Supports 'command' vs 'use-case' naming for application layer
|
|
439
|
+
const applicationLayerSuffix = namingConfig.terminology.command === 'use-case' ? 'UseCase' : 'Command';
|
|
440
|
+
const queryLayerSuffix = namingConfig.terminology.query === 'use-case' ? 'UseCase' : 'Query';
|
|
441
|
+
|
|
442
|
+
// Pre-computed class names using configured terminology
|
|
443
|
+
const createCommandClass = `Create${className}${applicationLayerSuffix}`;
|
|
444
|
+
const updateCommandClass = `Update${className}${applicationLayerSuffix}`;
|
|
445
|
+
const deleteCommandClass = `Delete${className}${applicationLayerSuffix}`;
|
|
446
|
+
const getByIdQueryClass = `Get${className}ById${queryLayerSuffix}`;
|
|
447
|
+
const listQueryClass = `List${classNamePlural}${queryLayerSuffix}`;
|
|
448
|
+
|
|
449
|
+
// Step 1: Compute all possible output paths
|
|
450
|
+
const src = BASE_PATHS.backendSrc;
|
|
451
|
+
const allPaths = {
|
|
452
|
+
// Domain layer
|
|
453
|
+
entity: `${src}/${paths.domain}/${fileNames.entity}`,
|
|
454
|
+
repositoryInterface: `${src}/${paths.domain}/${fileNames.repositoryInterface}`,
|
|
455
|
+
domainGroupedIndex: `${src}/${paths.domain}/index.ts`,
|
|
456
|
+
|
|
457
|
+
// Application layer - commands
|
|
458
|
+
createCommand: `${src}/${paths.commands}/${fileNames.createCommand}`,
|
|
459
|
+
updateCommand: `${src}/${paths.commands}/${fileNames.updateCommand}`,
|
|
460
|
+
deleteCommand: `${src}/${paths.commands}/${fileNames.deleteCommand}`,
|
|
461
|
+
commandsIndex: `${src}/${paths.commands}/index.ts`,
|
|
462
|
+
|
|
463
|
+
// Application layer - queries
|
|
464
|
+
getByIdQuery: `${src}/${paths.queries}/${fileNames.getByIdQuery}`,
|
|
465
|
+
listQuery: `${src}/${paths.queries}/${fileNames.listQuery}`,
|
|
466
|
+
queriesIndex: `${src}/${paths.queries}/index.ts`,
|
|
467
|
+
|
|
468
|
+
// Application layer - schemas (always generated)
|
|
469
|
+
dto: `${src}/${paths.schemas}/${fileNames.dto}`,
|
|
470
|
+
|
|
471
|
+
// Infrastructure layer (always generated)
|
|
472
|
+
drizzleSchema: `${src}/${paths.drizzle}/${fileNames.schema}`,
|
|
473
|
+
repository: `${src}/${paths.repositories}/${fileNames.repository}`,
|
|
474
|
+
|
|
475
|
+
// Presentation layer (always generated)
|
|
476
|
+
controller: `${src}/${paths.controllers}/${fileNames.controller}`,
|
|
477
|
+
|
|
478
|
+
// Modules (always generated)
|
|
479
|
+
module: `${src}/${paths.modules}/${fileNames.module}`,
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
// Step 2: Apply mode filter (null = skip generation)
|
|
483
|
+
const outputPaths = {
|
|
484
|
+
// Domain: separate files OR grouped index
|
|
485
|
+
entity: !isGrouped ? allPaths.entity : null,
|
|
486
|
+
repositoryInterface: !isGrouped ? allPaths.repositoryInterface : null,
|
|
487
|
+
domainGroupedIndex: isGrouped ? allPaths.domainGroupedIndex : null,
|
|
488
|
+
|
|
489
|
+
// Commands: separate files OR grouped index
|
|
490
|
+
createCommand: !isGrouped ? allPaths.createCommand : null,
|
|
491
|
+
updateCommand: !isGrouped ? allPaths.updateCommand : null,
|
|
492
|
+
deleteCommand: !isGrouped ? allPaths.deleteCommand : null,
|
|
493
|
+
commandsIndex: (!isGrouped && isNested) ? allPaths.commandsIndex : null,
|
|
494
|
+
commandsGroupedIndex: isGrouped ? allPaths.commandsIndex : null,
|
|
495
|
+
|
|
496
|
+
// Queries: separate files OR grouped index
|
|
497
|
+
getByIdQuery: !isGrouped ? allPaths.getByIdQuery : null,
|
|
498
|
+
listQuery: !isGrouped ? allPaths.listQuery : null,
|
|
499
|
+
queriesIndex: (!isGrouped && isNested) ? allPaths.queriesIndex : null,
|
|
500
|
+
queriesGroupedIndex: isGrouped ? allPaths.queriesIndex : null,
|
|
501
|
+
|
|
502
|
+
// Always generated (mode-independent)
|
|
503
|
+
dto: allPaths.dto,
|
|
504
|
+
drizzleSchema: allPaths.drizzleSchema,
|
|
505
|
+
repository: allPaths.repository,
|
|
506
|
+
controller: allPaths.controller,
|
|
507
|
+
module: allPaths.module,
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
// Import paths using centralized config
|
|
511
|
+
// See tools/codegen/config/paths.js for path definitions
|
|
512
|
+
const importHelpers = getImportPaths({ isNested });
|
|
513
|
+
const imports = {
|
|
514
|
+
// From commands/queries to other locations
|
|
515
|
+
constants: importHelpers.constants(name),
|
|
516
|
+
domain: importHelpers.domain(name),
|
|
517
|
+
schemas: importHelpers.schemas(name),
|
|
518
|
+
// From domain to other domain files (same folder when nested)
|
|
519
|
+
domainEntity: importHelpers.domainEntity(name),
|
|
520
|
+
// From module (modules/) to commands/queries
|
|
521
|
+
moduleToGetByIdQuery: importHelpers.moduleToQuery(name, fileNames.getByIdQuery.replace('.ts', '')),
|
|
522
|
+
moduleToListQuery: importHelpers.moduleToQuery(name, fileNames.listQuery.replace('.ts', '')),
|
|
523
|
+
moduleToDeclarativeQueries: importHelpers.moduleToQuery(name, 'declarative-queries'),
|
|
524
|
+
moduleToCreateCommand: importHelpers.moduleToCommand(name, fileNames.createCommand.replace('.ts', '')),
|
|
525
|
+
moduleToUpdateCommand: importHelpers.moduleToCommand(name, fileNames.updateCommand.replace('.ts', '')),
|
|
526
|
+
moduleToDeleteCommand: importHelpers.moduleToCommand(name, fileNames.deleteCommand.replace('.ts', '')),
|
|
527
|
+
moduleToRepository: importHelpers.moduleToRepository(fileNames.repository.replace('.ts', '')),
|
|
528
|
+
moduleToConstants: importHelpers.moduleToConstants(),
|
|
529
|
+
moduleToDatabaseModule: importHelpers.moduleToDatabaseModule(),
|
|
530
|
+
moduleToController: importHelpers.moduleToController(fileNames.controller.replace('.ts', '')),
|
|
531
|
+
// From controller (presentation/rest/) to queries/commands
|
|
532
|
+
controllerToGetByIdQuery: importHelpers.controllerToQuery(name, fileNames.getByIdQuery.replace('.ts', '')),
|
|
533
|
+
controllerToListQuery: importHelpers.controllerToQuery(name, fileNames.listQuery.replace('.ts', '')),
|
|
534
|
+
controllerToCreateCommand: importHelpers.controllerToCommand(name, fileNames.createCommand.replace('.ts', '')),
|
|
535
|
+
controllerToUpdateCommand: importHelpers.controllerToCommand(name, fileNames.updateCommand.replace('.ts', '')),
|
|
536
|
+
controllerToDeleteCommand: importHelpers.controllerToCommand(name, fileNames.deleteCommand.replace('.ts', '')),
|
|
537
|
+
controllerToSchemas: importHelpers.controllerToSchemas(),
|
|
538
|
+
controllerToDomain: importHelpers.controllerToDomain(),
|
|
539
|
+
// From app.module.ts to modules
|
|
540
|
+
appModuleToModule: importHelpers.appModuleToModule(fileNames.module.replace('.ts', '')),
|
|
541
|
+
appModuleToTrpcModule: importHelpers.appModuleToTrpcModule(`${name}-trpc.module`),
|
|
542
|
+
// From repository to constants (relative path)
|
|
543
|
+
repositoryToConstants: importHelpers.repositoryToConstants(),
|
|
544
|
+
// For domain/index.ts export
|
|
545
|
+
domainExport: isNested ? `./${name}` : null,
|
|
546
|
+
// Electric-related imports
|
|
547
|
+
controllerToAuthGuard: importHelpers.controllerToAuthGuard(),
|
|
548
|
+
controllerToCurrentUser: importHelpers.controllerToCurrentUser(),
|
|
549
|
+
controllerToElectricService: importHelpers.controllerToElectricService(),
|
|
550
|
+
moduleToElectricModule: importHelpers.moduleToElectricModule(),
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
// Type mappings
|
|
554
|
+
const tsTypes = {
|
|
555
|
+
string: "string",
|
|
556
|
+
integer: "number",
|
|
557
|
+
decimal: "number",
|
|
558
|
+
boolean: "boolean",
|
|
559
|
+
uuid: "string",
|
|
560
|
+
date: "Date",
|
|
561
|
+
datetime: "Date",
|
|
562
|
+
json: "unknown",
|
|
563
|
+
string_array: "string[]",
|
|
564
|
+
entity_ref: "EntityRef", // Placeholder - handled specially
|
|
565
|
+
enum: "string", // Actual type generated from choices
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
const drizzleTypes = {
|
|
569
|
+
string: "varchar",
|
|
570
|
+
integer: "integer",
|
|
571
|
+
decimal: "decimal",
|
|
572
|
+
boolean: "boolean",
|
|
573
|
+
uuid: "uuid",
|
|
574
|
+
date: "date",
|
|
575
|
+
datetime: "timestamp",
|
|
576
|
+
json: "jsonb",
|
|
577
|
+
string_array: "text_array",
|
|
578
|
+
entity_ref: "entity_ref", // Placeholder - handled specially
|
|
579
|
+
enum: "enum", // Placeholder - pgEnum generated
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
const zodTypes = {
|
|
583
|
+
string: "z.string()",
|
|
584
|
+
integer: "z.number().int()",
|
|
585
|
+
decimal: "z.number()",
|
|
586
|
+
boolean: "z.boolean()",
|
|
587
|
+
uuid: "z.string().uuid()",
|
|
588
|
+
date: "z.coerce.date()",
|
|
589
|
+
datetime: "z.coerce.date()",
|
|
590
|
+
json: "z.unknown()",
|
|
591
|
+
string_array: "z.array(z.string())",
|
|
592
|
+
entity_ref: "entity_ref", // Placeholder - handled specially
|
|
593
|
+
enum: "z.enum()", // Placeholder - choices added
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Load choices from an external YAML file (for choices_from option)
|
|
598
|
+
*/
|
|
599
|
+
const loadChoicesFromFile = (choicesFromPath, yamlDir) => {
|
|
600
|
+
// Try relative to entities directory first
|
|
601
|
+
const entitiesPath = path.resolve(
|
|
602
|
+
process.cwd(),
|
|
603
|
+
"entities",
|
|
604
|
+
choicesFromPath,
|
|
605
|
+
);
|
|
606
|
+
if (fs.existsSync(entitiesPath)) {
|
|
607
|
+
const content = fs.readFileSync(entitiesPath, "utf-8");
|
|
608
|
+
const parsed = yaml.parse(content);
|
|
609
|
+
// For relationship_types.yaml, extract keys from relationship_types section
|
|
610
|
+
if (parsed.relationship_types) {
|
|
611
|
+
return Object.keys(parsed.relationship_types);
|
|
612
|
+
}
|
|
613
|
+
// Otherwise extract top-level keys
|
|
614
|
+
return Object.keys(parsed);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Try relative to the YAML file directory
|
|
618
|
+
const relativePath = path.resolve(yamlDir, choicesFromPath);
|
|
619
|
+
if (fs.existsSync(relativePath)) {
|
|
620
|
+
const content = fs.readFileSync(relativePath, "utf-8");
|
|
621
|
+
const parsed = yaml.parse(content);
|
|
622
|
+
if (parsed.relationship_types) {
|
|
623
|
+
return Object.keys(parsed.relationship_types);
|
|
624
|
+
}
|
|
625
|
+
return Object.keys(parsed);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
throw new Error(`choices_from file not found: ${choicesFromPath}`);
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
// Process fields for templates
|
|
632
|
+
const processedFields = [];
|
|
633
|
+
const entityRefFields = []; // Track entity_ref fields for special handling
|
|
634
|
+
|
|
635
|
+
for (const [fieldName, field] of Object.entries(fields)) {
|
|
636
|
+
// Skip 'id' field - it's always added explicitly in templates to avoid duplicates
|
|
637
|
+
if (fieldName === 'id') continue;
|
|
638
|
+
|
|
639
|
+
// Handle entity_ref type specially - generates TWO fields
|
|
640
|
+
if (field.type === "entity_ref") {
|
|
641
|
+
const allowedTypes = field.allowed_types || [];
|
|
642
|
+
const baseName = fieldName;
|
|
643
|
+
const baseCamel = camelCase(fieldName);
|
|
644
|
+
|
|
645
|
+
// Track for later use (composite indexes, query methods)
|
|
646
|
+
entityRefFields.push({
|
|
647
|
+
name: baseName,
|
|
648
|
+
camelName: baseCamel,
|
|
649
|
+
pascalName: pascalCase(baseName),
|
|
650
|
+
allowedTypes,
|
|
651
|
+
required: field.required ?? false,
|
|
652
|
+
nullable: field.nullable ?? false,
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
// Generate the type field (enum)
|
|
656
|
+
processedFields.push({
|
|
657
|
+
name: `${baseName}_entity_type`,
|
|
658
|
+
camelName: `${baseCamel}EntityType`,
|
|
659
|
+
type: "entity_ref_type",
|
|
660
|
+
tsType: "EntityType",
|
|
661
|
+
drizzleType: "entity_type_enum",
|
|
662
|
+
zodType: "entityTypeSchema",
|
|
663
|
+
required: field.required ?? false,
|
|
664
|
+
nullable: field.nullable ?? false,
|
|
665
|
+
isEntityRefType: true,
|
|
666
|
+
entityRefBase: baseName,
|
|
667
|
+
allowedTypes,
|
|
668
|
+
// UI metadata for entity ref
|
|
669
|
+
ui_type: "enum",
|
|
670
|
+
ui_label: formatLabel(`${baseName} type`),
|
|
671
|
+
ui_importance: "secondary",
|
|
672
|
+
ui_group: "relationships",
|
|
673
|
+
ui_visible: false,
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
// Generate the id field (uuid)
|
|
677
|
+
processedFields.push({
|
|
678
|
+
name: `${baseName}_entity_id`,
|
|
679
|
+
camelName: `${baseCamel}EntityId`,
|
|
680
|
+
type: "entity_ref_id",
|
|
681
|
+
tsType: "string",
|
|
682
|
+
drizzleType: "uuid",
|
|
683
|
+
zodType: "z.string().uuid()",
|
|
684
|
+
required: field.required ?? false,
|
|
685
|
+
nullable: field.nullable ?? false,
|
|
686
|
+
isEntityRefId: true,
|
|
687
|
+
entityRefBase: baseName,
|
|
688
|
+
// UI metadata
|
|
689
|
+
ui_type: "text",
|
|
690
|
+
ui_label: formatLabel(`${baseName} id`),
|
|
691
|
+
ui_importance: "secondary",
|
|
692
|
+
ui_group: "relationships",
|
|
693
|
+
ui_visible: false,
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
continue; // Skip normal processing
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// Handle enum type with choices or choices_from
|
|
700
|
+
let choices = field.choices;
|
|
701
|
+
if (field.type === "enum" && field.choices_from) {
|
|
702
|
+
try {
|
|
703
|
+
choices = loadChoicesFromFile(field.choices_from, path.dirname(fullPath));
|
|
704
|
+
} catch (e) {
|
|
705
|
+
console.warn(
|
|
706
|
+
`Warning: Could not load choices from ${field.choices_from}: ${e.message}`,
|
|
707
|
+
);
|
|
708
|
+
choices = [];
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const hasChoices = Array.isArray(choices) && choices.length > 0;
|
|
713
|
+
|
|
714
|
+
// For choice fields, generate literal union type instead of string
|
|
715
|
+
let tsType = tsTypes[field.type] || "unknown";
|
|
716
|
+
if (hasChoices) {
|
|
717
|
+
tsType = choices.map((c) => `'${c}'`).join(" | ");
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// For choice fields, we'll use pgEnum instead of varchar
|
|
721
|
+
let drizzleType = drizzleTypes[field.type] || "varchar";
|
|
722
|
+
if (hasChoices || field.type === "enum") {
|
|
723
|
+
drizzleType = "enum"; // Special marker for enum handling
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
let zodType = zodTypes[field.type] || "z.unknown()";
|
|
727
|
+
if (hasChoices) {
|
|
728
|
+
zodType = `z.enum([${choices.map((c) => `'${c}'`).join(", ")}])`;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// Generate enum name for Drizzle pgEnum (camelCase + 'Enum')
|
|
732
|
+
const enumName = hasChoices ? camelCase(fieldName) + "Enum" : null;
|
|
733
|
+
|
|
734
|
+
// Infer UI metadata with defaults
|
|
735
|
+
const ui_type = inferUiType(fieldName, field);
|
|
736
|
+
const ui_label = field.ui_label || formatLabel(fieldName);
|
|
737
|
+
const ui_importance = inferUiImportance(fieldName, field);
|
|
738
|
+
const ui_group = inferUiGroup(fieldName, field);
|
|
739
|
+
const ui_sortable = field.ui_sortable ?? false;
|
|
740
|
+
const ui_filterable = field.ui_filterable ?? false;
|
|
741
|
+
// Default visibility: hide id and timestamp fields
|
|
742
|
+
const ui_visible =
|
|
743
|
+
field.ui_visible ??
|
|
744
|
+
!["id", "created_at", "updated_at", "deleted_at"].includes(fieldName);
|
|
745
|
+
|
|
746
|
+
processedFields.push({
|
|
747
|
+
name: fieldName,
|
|
748
|
+
camelName: camelCase(fieldName),
|
|
749
|
+
type: field.type,
|
|
750
|
+
tsType,
|
|
751
|
+
drizzleType,
|
|
752
|
+
zodType,
|
|
753
|
+
required: field.required ?? false,
|
|
754
|
+
nullable: field.nullable ?? false,
|
|
755
|
+
maxLength: field.max_length,
|
|
756
|
+
minLength: field.min_length,
|
|
757
|
+
min: field.min,
|
|
758
|
+
max: field.max,
|
|
759
|
+
choices,
|
|
760
|
+
choicesFrom: field.choices_from,
|
|
761
|
+
hasChoices,
|
|
762
|
+
enumName,
|
|
763
|
+
default: field.default,
|
|
764
|
+
index: field.index ?? false,
|
|
765
|
+
unique: field.unique ?? false,
|
|
766
|
+
foreignKey: field.foreign_key,
|
|
767
|
+
// UI metadata
|
|
768
|
+
ui_type,
|
|
769
|
+
ui_label,
|
|
770
|
+
ui_importance,
|
|
771
|
+
ui_group,
|
|
772
|
+
ui_sortable,
|
|
773
|
+
ui_filterable,
|
|
774
|
+
ui_visible,
|
|
775
|
+
ui_placeholder: field.ui_placeholder,
|
|
776
|
+
ui_help: field.ui_help,
|
|
777
|
+
ui_format: field.ui_format,
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Collect enum fields for Drizzle pgEnum generation
|
|
782
|
+
const enumFields = processedFields.filter((f) => f.hasChoices);
|
|
783
|
+
|
|
784
|
+
// Process relationships by type
|
|
785
|
+
const belongsToRelations = Object.entries(relationships)
|
|
786
|
+
.filter(([_, rel]) => rel.type === "belongs_to")
|
|
787
|
+
.map(([relName, rel]) => ({
|
|
788
|
+
name: relName,
|
|
789
|
+
type: "belongs_to",
|
|
790
|
+
target: rel.target,
|
|
791
|
+
targetClass: pascalCase(rel.target),
|
|
792
|
+
targetPlural: pluralize(rel.target),
|
|
793
|
+
targetPluralClass: pascalCase(pluralize(rel.target)),
|
|
794
|
+
foreignKey: rel.foreign_key,
|
|
795
|
+
foreignKeyCamel: camelCase(rel.foreign_key),
|
|
796
|
+
foreignKeyPascal: pascalCase(rel.foreign_key),
|
|
797
|
+
}));
|
|
798
|
+
|
|
799
|
+
const hasManyRelations = Object.entries(relationships)
|
|
800
|
+
.filter(([_, rel]) => rel.type === "has_many")
|
|
801
|
+
.map(([relName, rel]) => ({
|
|
802
|
+
name: relName,
|
|
803
|
+
type: "has_many",
|
|
804
|
+
target: rel.target,
|
|
805
|
+
targetClass: pascalCase(rel.target),
|
|
806
|
+
targetPlural: pluralize(rel.target),
|
|
807
|
+
targetPluralClass: pascalCase(pluralize(rel.target)),
|
|
808
|
+
inverseForeignKey: rel.foreign_key,
|
|
809
|
+
inverseForeignKeyCamel: camelCase(rel.foreign_key),
|
|
810
|
+
}));
|
|
811
|
+
|
|
812
|
+
const hasOneRelations = Object.entries(relationships)
|
|
813
|
+
.filter(([_, rel]) => rel.type === "has_one")
|
|
814
|
+
.map(([relName, rel]) => ({
|
|
815
|
+
name: relName,
|
|
816
|
+
type: "has_one",
|
|
817
|
+
target: rel.target,
|
|
818
|
+
targetClass: pascalCase(rel.target),
|
|
819
|
+
targetPlural: pluralize(rel.target),
|
|
820
|
+
inverseForeignKey: rel.foreign_key,
|
|
821
|
+
inverseForeignKeyCamel: camelCase(rel.foreign_key),
|
|
822
|
+
}));
|
|
823
|
+
|
|
824
|
+
// All relationships combined
|
|
825
|
+
const allRelationships = [
|
|
826
|
+
...belongsToRelations,
|
|
827
|
+
...hasManyRelations,
|
|
828
|
+
...hasOneRelations,
|
|
829
|
+
];
|
|
830
|
+
|
|
831
|
+
// Check which related entities have generated domain files
|
|
832
|
+
// This allows the repository to skip importing entities that don't exist yet
|
|
833
|
+
const checkEntityExists = (targetName) => {
|
|
834
|
+
const domainBase = `${BASE_PATHS.backendSrc}/domain`;
|
|
835
|
+
const nestedPath = path.resolve(
|
|
836
|
+
process.cwd(),
|
|
837
|
+
`${domainBase}/${targetName}/${targetName}.entity.ts`,
|
|
838
|
+
);
|
|
839
|
+
const flatPath = path.resolve(
|
|
840
|
+
process.cwd(),
|
|
841
|
+
`${domainBase}/${targetName}.entity.ts`,
|
|
842
|
+
);
|
|
843
|
+
return fs.existsSync(nestedPath) || fs.existsSync(flatPath);
|
|
844
|
+
};
|
|
845
|
+
|
|
846
|
+
// Mark each relationship with whether its target entity exists
|
|
847
|
+
for (const rel of allRelationships) {
|
|
848
|
+
rel.targetExists = checkEntityExists(rel.target);
|
|
849
|
+
}
|
|
850
|
+
for (const rel of belongsToRelations) {
|
|
851
|
+
rel.targetExists = checkEntityExists(rel.target);
|
|
852
|
+
}
|
|
853
|
+
for (const rel of hasManyRelations) {
|
|
854
|
+
rel.targetExists = checkEntityExists(rel.target);
|
|
855
|
+
}
|
|
856
|
+
for (const rel of hasOneRelations) {
|
|
857
|
+
rel.targetExists = checkEntityExists(rel.target);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Filter to only relationships with existing targets for repository imports
|
|
861
|
+
const existingRelationships = allRelationships.filter(
|
|
862
|
+
(r) => r.targetExists,
|
|
863
|
+
);
|
|
864
|
+
const existingBelongsTo = belongsToRelations.filter((r) => r.targetExists);
|
|
865
|
+
const existingHasMany = hasManyRelations.filter((r) => r.targetExists);
|
|
866
|
+
const existingHasOne = hasOneRelations.filter((r) => r.targetExists);
|
|
867
|
+
|
|
868
|
+
// Convenience flags
|
|
869
|
+
const hasRelationships = allRelationships.length > 0;
|
|
870
|
+
const hasExistingRelationships = existingRelationships.length > 0;
|
|
871
|
+
const hasBelongsTo = belongsToRelations.length > 0;
|
|
872
|
+
const hasHasMany = hasManyRelations.length > 0;
|
|
873
|
+
const hasHasOne = hasOneRelations.length > 0;
|
|
874
|
+
|
|
875
|
+
// Legacy format for backward compatibility
|
|
876
|
+
const processedRelationships = allRelationships;
|
|
877
|
+
|
|
878
|
+
// Separate required vs optional fields for DTOs
|
|
879
|
+
const requiredFields = processedFields.filter((f) => f.required);
|
|
880
|
+
const optionalFields = processedFields.filter((f) => !f.required);
|
|
881
|
+
|
|
882
|
+
// Compute which Drizzle imports are needed (always need pgTable, uuid for id)
|
|
883
|
+
// Note: timestamp is NOT always needed - only if behaviors include timestamps or soft_delete
|
|
884
|
+
const drizzleImportsNeeded = new Set(["pgTable", "uuid"]);
|
|
885
|
+
|
|
886
|
+
// Add pgEnum if we have any enum fields
|
|
887
|
+
if (enumFields.length > 0) {
|
|
888
|
+
drizzleImportsNeeded.add("pgEnum");
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// Check if we have entity_ref fields (need to import entity type enum)
|
|
892
|
+
const hasEntityRefFields = entityRefFields.length > 0;
|
|
893
|
+
if (hasEntityRefFields) {
|
|
894
|
+
drizzleImportsNeeded.add("pgEnum");
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
for (const field of processedFields) {
|
|
898
|
+
// Map drizzle type to import name (skip 'enum' as it's handled via pgEnum)
|
|
899
|
+
const importMap = {
|
|
900
|
+
varchar: "varchar",
|
|
901
|
+
integer: "integer",
|
|
902
|
+
decimal: "numeric",
|
|
903
|
+
boolean: "boolean",
|
|
904
|
+
uuid: "uuid",
|
|
905
|
+
date: "date",
|
|
906
|
+
timestamp: "timestamp",
|
|
907
|
+
jsonb: "jsonb",
|
|
908
|
+
text_array: "text",
|
|
909
|
+
};
|
|
910
|
+
const importName = importMap[field.drizzleType];
|
|
911
|
+
if (importName) {
|
|
912
|
+
drizzleImportsNeeded.add(importName);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// Add Drizzle imports from behaviors
|
|
917
|
+
for (const imp of resolvedBehaviors.drizzleImports) {
|
|
918
|
+
drizzleImportsNeeded.add(imp);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
const drizzleImports = Array.from(drizzleImportsNeeded).sort();
|
|
922
|
+
|
|
923
|
+
// Get database dialect from config
|
|
924
|
+
const databaseDialect = getDatabaseDialect();
|
|
925
|
+
|
|
926
|
+
// Derive Electric where clause FK field from entity fields
|
|
927
|
+
// Look for foreign_key to users or tenants
|
|
928
|
+
let electricWhereColumn = 'tenant_id'; // fallback
|
|
929
|
+
let electricWhereValue = 'user.tenantId'; // fallback
|
|
930
|
+
|
|
931
|
+
for (const field of processedFields) {
|
|
932
|
+
if (field.foreignKey) {
|
|
933
|
+
// Check if it references users table
|
|
934
|
+
if (field.foreignKey.startsWith('users.')) {
|
|
935
|
+
electricWhereColumn = field.name;
|
|
936
|
+
electricWhereValue = `user.${field.camelName}`;
|
|
937
|
+
break;
|
|
938
|
+
}
|
|
939
|
+
// Check if it references tenants table
|
|
940
|
+
if (field.foreignKey.startsWith('tenants.')) {
|
|
941
|
+
electricWhereColumn = field.name;
|
|
942
|
+
electricWhereValue = `user.${field.camelName}`;
|
|
943
|
+
// Don't break - users FK takes precedence
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// ============================================================================
|
|
949
|
+
// Architecture Target + pipeline gates (from generate config)
|
|
950
|
+
//
|
|
951
|
+
// `generate.architecture` is the single source of truth for which backend
|
|
952
|
+
// template set runs. Values:
|
|
953
|
+
// - 'clean' → templates/entity/new/backend/ (Clean Architecture)
|
|
954
|
+
// - 'clean-lite-ps' → templates/entity/new/clean-lite-ps/ (modules/ layout)
|
|
955
|
+
//
|
|
956
|
+
// `generate.frontend` gates the frontend pipeline entirely.
|
|
957
|
+
// ============================================================================
|
|
958
|
+
|
|
959
|
+
const generateConfig = getGenerateConfig();
|
|
960
|
+
const architectureTarget = generateConfig.architecture;
|
|
961
|
+
const isCleanArchitecture = architectureTarget === 'clean';
|
|
962
|
+
const isCleanLitePs = architectureTarget === 'clean-lite-ps';
|
|
963
|
+
const frontendEnabled = generateConfig.frontend === true;
|
|
964
|
+
|
|
965
|
+
// ============================================================================
|
|
966
|
+
// v2: Family
|
|
967
|
+
// ============================================================================
|
|
968
|
+
|
|
969
|
+
const FAMILY_REPOSITORY_MAP = {
|
|
970
|
+
'synced': 'SyncedEntityRepository',
|
|
971
|
+
'activity': 'ActivityEntityRepository',
|
|
972
|
+
'knowledge': 'KnowledgeEntityRepository',
|
|
973
|
+
'metadata': 'MetadataEntityRepository',
|
|
974
|
+
};
|
|
975
|
+
|
|
976
|
+
const FAMILY_SERVICE_MAP = {
|
|
977
|
+
'synced': 'SyncedEntityService',
|
|
978
|
+
'activity': 'ActivityEntityService',
|
|
979
|
+
'knowledge': 'KnowledgeEntityService',
|
|
980
|
+
'metadata': 'MetadataEntityService',
|
|
981
|
+
};
|
|
982
|
+
|
|
983
|
+
const family = entity.family ?? null;
|
|
984
|
+
const hasFamily = family != null;
|
|
985
|
+
const familyBaseRepository = family ? (FAMILY_REPOSITORY_MAP[family] ?? null) : null;
|
|
986
|
+
const familyBaseService = family ? (FAMILY_SERVICE_MAP[family] ?? null) : null;
|
|
987
|
+
|
|
988
|
+
// ============================================================================
|
|
989
|
+
// v2: Queries
|
|
990
|
+
// ============================================================================
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* Derive a camelCase method name from a query spec.
|
|
994
|
+
*
|
|
995
|
+
* Rules:
|
|
996
|
+
* select present → findXsByY (e.g., select:[email], by:[opportunity_id] → findEmailsByOpportunityId)
|
|
997
|
+
* otherwise → findByX (e.g., by:[user_id] → findByUserId)
|
|
998
|
+
* (e.g., by:[user_id, account_id] → findByUserIdAndAccountId)
|
|
999
|
+
*/
|
|
1000
|
+
function deriveQueryMethodName(query) {
|
|
1001
|
+
const byFields = Array.isArray(query.by) ? query.by : [];
|
|
1002
|
+
const selectFields = Array.isArray(query.select) ? query.select : [];
|
|
1003
|
+
|
|
1004
|
+
// Convert snake_case field list to PascalCase joined by "And"
|
|
1005
|
+
const byPart = byFields.map((f) => pascalCase(f)).join('And');
|
|
1006
|
+
|
|
1007
|
+
if (selectFields.length > 0) {
|
|
1008
|
+
// findEmailsByOpportunityId — select fields come first (plural implied)
|
|
1009
|
+
const selectPart = selectFields.map((f) => pascalCase(f)).join('And') + 's';
|
|
1010
|
+
return `find${selectPart}By${byPart}`;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
return `findBy${byPart}`;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
const hasQueries = queriesBlock != null && queriesBlock.length > 0;
|
|
1017
|
+
|
|
1018
|
+
// Build a lookup of field name → TS type for query param resolution
|
|
1019
|
+
const fieldTypeMap = {};
|
|
1020
|
+
for (const pf of processedFields) {
|
|
1021
|
+
fieldTypeMap[pf.name] = pf.tsType;
|
|
1022
|
+
fieldTypeMap[pf.camelName] = pf.tsType;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
const processedQueries = hasQueries
|
|
1026
|
+
? queriesBlock.map((q) => {
|
|
1027
|
+
const byFields = Array.isArray(q.by) ? q.by : [];
|
|
1028
|
+
const selectFields = Array.isArray(q.select) ? q.select : [];
|
|
1029
|
+
const isUnique = q.unique ?? false;
|
|
1030
|
+
const viaTable = q.via ?? null;
|
|
1031
|
+
|
|
1032
|
+
// Build typed params from by fields
|
|
1033
|
+
const params = byFields.map((f) => ({
|
|
1034
|
+
name: f,
|
|
1035
|
+
camelName: camelCase(f),
|
|
1036
|
+
tsType: fieldTypeMap[f] || fieldTypeMap[camelCase(f)] || 'string',
|
|
1037
|
+
}));
|
|
1038
|
+
|
|
1039
|
+
// Parse order: "created_at desc" → { column, direction }
|
|
1040
|
+
let orderBy = null;
|
|
1041
|
+
let orderDirection = null;
|
|
1042
|
+
if (q.order) {
|
|
1043
|
+
const parts = q.order.trim().split(/\s+/);
|
|
1044
|
+
orderBy = camelCase(parts[0]);
|
|
1045
|
+
orderDirection = parts[1] || 'asc';
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// Derive method name
|
|
1049
|
+
const methodName = deriveQueryMethodName(q);
|
|
1050
|
+
|
|
1051
|
+
// Derive return type
|
|
1052
|
+
let returnType;
|
|
1053
|
+
if (isUnique) {
|
|
1054
|
+
returnType = `${className} | null`;
|
|
1055
|
+
} else if (selectFields.length > 0) {
|
|
1056
|
+
// Projection — return picked fields
|
|
1057
|
+
const camelFields = selectFields.map((f) => camelCase(f));
|
|
1058
|
+
returnType = selectFields.length === 1
|
|
1059
|
+
? `${fieldTypeMap[selectFields[0]] || fieldTypeMap[camelFields[0]] || 'string'}[]`
|
|
1060
|
+
: `Pick<${className}, ${camelFields.map((f) => `'${f}'`).join(' | ')}>[]`;
|
|
1061
|
+
} else {
|
|
1062
|
+
returnType = `${className}[]`;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// Use case class name
|
|
1066
|
+
const useCaseClassName = pascalCase(methodName) + queryLayerSuffix;
|
|
1067
|
+
|
|
1068
|
+
return {
|
|
1069
|
+
// Raw YAML fields
|
|
1070
|
+
by: byFields,
|
|
1071
|
+
unique: isUnique,
|
|
1072
|
+
select: selectFields,
|
|
1073
|
+
order: q.order ?? null,
|
|
1074
|
+
limit: q.limit ?? null,
|
|
1075
|
+
via: viaTable,
|
|
1076
|
+
// Derived
|
|
1077
|
+
methodName,
|
|
1078
|
+
returnType,
|
|
1079
|
+
params,
|
|
1080
|
+
isUnique,
|
|
1081
|
+
orderBy,
|
|
1082
|
+
orderDirection,
|
|
1083
|
+
viaTable,
|
|
1084
|
+
viaTableCamel: viaTable ? camelCase(viaTable) : null,
|
|
1085
|
+
selectFields: selectFields.map((f) => camelCase(f)),
|
|
1086
|
+
useCaseClassName,
|
|
1087
|
+
// Convenience flags
|
|
1088
|
+
hasVia: viaTable != null,
|
|
1089
|
+
hasSelect: selectFields.length > 0,
|
|
1090
|
+
hasOrder: q.order != null,
|
|
1091
|
+
hasLimit: q.limit != null,
|
|
1092
|
+
hasMultipleParams: params.length > 1,
|
|
1093
|
+
};
|
|
1094
|
+
})
|
|
1095
|
+
: [];
|
|
1096
|
+
|
|
1097
|
+
const hasDeclarativeQueries = processedQueries.length > 0;
|
|
1098
|
+
const declarativeQueryClasses = processedQueries.map((q) => q.useCaseClassName);
|
|
1099
|
+
|
|
1100
|
+
// Check if any query needs 'and' import (multi-field WHERE)
|
|
1101
|
+
const hasMultiFieldQuery = processedQueries.some((q) => q.hasMultipleParams);
|
|
1102
|
+
// Check if any query needs 'desc'/'asc' import (ordered)
|
|
1103
|
+
const hasOrderedQuery = processedQueries.some((q) => q.hasOrder);
|
|
1104
|
+
|
|
1105
|
+
// ============================================================================
|
|
1106
|
+
// v2: Sync
|
|
1107
|
+
// ============================================================================
|
|
1108
|
+
|
|
1109
|
+
const hasSyncBlock = syncBlock != null;
|
|
1110
|
+
const syncElectric = hasSyncBlock ? (syncBlock.electric ?? false) : false;
|
|
1111
|
+
const rawSyncProviders = hasSyncBlock ? (syncBlock.providers ?? {}) : {};
|
|
1112
|
+
const hasSyncProviders = Object.keys(rawSyncProviders).length > 0;
|
|
1113
|
+
|
|
1114
|
+
const syncProviders = hasSyncProviders
|
|
1115
|
+
? Object.entries(rawSyncProviders).map(([providerName, cfg]) => {
|
|
1116
|
+
// Normalize field_mapping: { local: key, remote: value }[]
|
|
1117
|
+
const rawMapping = cfg.field_mapping ?? {};
|
|
1118
|
+
const fieldMapping = Object.entries(rawMapping).map(([local, remote]) => ({
|
|
1119
|
+
local,
|
|
1120
|
+
remote,
|
|
1121
|
+
}));
|
|
1122
|
+
|
|
1123
|
+
return {
|
|
1124
|
+
name: providerName,
|
|
1125
|
+
remoteEntity: cfg.remote_entity ?? null,
|
|
1126
|
+
direction: cfg.direction ?? 'bidirectional',
|
|
1127
|
+
cdc: cfg.cdc ?? false,
|
|
1128
|
+
fieldMapping,
|
|
1129
|
+
readOnlyFields: cfg.read_only_fields ?? [],
|
|
1130
|
+
};
|
|
1131
|
+
})
|
|
1132
|
+
: [];
|
|
1133
|
+
|
|
1134
|
+
// ============================================================================
|
|
1135
|
+
// v2: Events
|
|
1136
|
+
// ============================================================================
|
|
1137
|
+
|
|
1138
|
+
const hasEvents = eventsBlock != null && eventsBlock.length > 0;
|
|
1139
|
+
const processedEvents = hasEvents
|
|
1140
|
+
? eventsBlock.map((ev) => {
|
|
1141
|
+
// Convert body: { field: type } to array of { field, type }
|
|
1142
|
+
const rawBody = ev.body ?? {};
|
|
1143
|
+
const body = Object.entries(rawBody).map(([field, type]) => ({ field, type }));
|
|
1144
|
+
|
|
1145
|
+
// Derive class names from event name (snake_case → PascalCase + Event)
|
|
1146
|
+
const className = pascalCase(ev.name) + 'Event';
|
|
1147
|
+
const handlerClassName = pascalCase(ev.name) + 'Handler';
|
|
1148
|
+
|
|
1149
|
+
return {
|
|
1150
|
+
name: ev.name,
|
|
1151
|
+
queue: ev.queue ?? null,
|
|
1152
|
+
body,
|
|
1153
|
+
generateHandler: ev.generate_handler ?? false,
|
|
1154
|
+
className,
|
|
1155
|
+
handlerClassName,
|
|
1156
|
+
};
|
|
1157
|
+
})
|
|
1158
|
+
: [];
|
|
1159
|
+
|
|
1160
|
+
const locals = {
|
|
1161
|
+
// Database configuration
|
|
1162
|
+
databaseDialect,
|
|
1163
|
+
schemaDir: BASE_PATHS.schemaDir,
|
|
1164
|
+
|
|
1165
|
+
// Entity names
|
|
1166
|
+
name,
|
|
1167
|
+
plural,
|
|
1168
|
+
table,
|
|
1169
|
+
className,
|
|
1170
|
+
classNamePlural,
|
|
1171
|
+
camelName,
|
|
1172
|
+
repositoryToken,
|
|
1173
|
+
|
|
1174
|
+
// Frontend store naming
|
|
1175
|
+
singularCamelName,
|
|
1176
|
+
pluralCamelName,
|
|
1177
|
+
collectionVarName,
|
|
1178
|
+
collectionVarNamePlural,
|
|
1179
|
+
|
|
1180
|
+
// Fields
|
|
1181
|
+
fields: processedFields,
|
|
1182
|
+
requiredFields,
|
|
1183
|
+
optionalFields,
|
|
1184
|
+
enumFields,
|
|
1185
|
+
|
|
1186
|
+
// Entity reference fields (polymorphic refs)
|
|
1187
|
+
entityRefFields,
|
|
1188
|
+
hasEntityRefFields,
|
|
1189
|
+
|
|
1190
|
+
// Relationships - separated by type
|
|
1191
|
+
relationships: allRelationships,
|
|
1192
|
+
belongsToRelations,
|
|
1193
|
+
hasManyRelations,
|
|
1194
|
+
hasOneRelations,
|
|
1195
|
+
|
|
1196
|
+
// Relationship flags
|
|
1197
|
+
hasRelationships,
|
|
1198
|
+
hasExistingRelationships,
|
|
1199
|
+
hasBelongsTo,
|
|
1200
|
+
hasHasMany,
|
|
1201
|
+
hasHasOne,
|
|
1202
|
+
|
|
1203
|
+
// Filtered relationships (only those with existing target entities)
|
|
1204
|
+
existingRelationships,
|
|
1205
|
+
existingBelongsTo,
|
|
1206
|
+
existingHasMany,
|
|
1207
|
+
existingHasOne,
|
|
1208
|
+
|
|
1209
|
+
// Drizzle imports (only what's needed)
|
|
1210
|
+
drizzleImports,
|
|
1211
|
+
|
|
1212
|
+
// Layout configuration
|
|
1213
|
+
// folder_structure: "nested" | "flat" - controls directory nesting
|
|
1214
|
+
// file_grouping: "separate" | "grouped" - controls file organization
|
|
1215
|
+
layout,
|
|
1216
|
+
folderStructure,
|
|
1217
|
+
fileGrouping,
|
|
1218
|
+
isNested,
|
|
1219
|
+
isGrouped,
|
|
1220
|
+
paths,
|
|
1221
|
+
fileNames,
|
|
1222
|
+
imports,
|
|
1223
|
+
|
|
1224
|
+
// Base paths for templates (from centralized config)
|
|
1225
|
+
basePaths: BASE_PATHS,
|
|
1226
|
+
backendLayers: BACKEND_LAYERS,
|
|
1227
|
+
|
|
1228
|
+
// Unified locations (path + import alias)
|
|
1229
|
+
// Usage: locations.dbEntities.path, locations.dbEntities.import
|
|
1230
|
+
locations: LOCATIONS,
|
|
1231
|
+
|
|
1232
|
+
// Frontend configuration
|
|
1233
|
+
// Note: Use hasOwnProperty checks for values where null is meaningful (disables the feature)
|
|
1234
|
+
frontend: {
|
|
1235
|
+
auth: {
|
|
1236
|
+
// null means "no auth function" - don't fall back to default
|
|
1237
|
+
function: frontendConfig.auth?.hasOwnProperty?.('function')
|
|
1238
|
+
? frontendConfig.auth.function
|
|
1239
|
+
: 'getAuthorizationHeader',
|
|
1240
|
+
},
|
|
1241
|
+
sync: {
|
|
1242
|
+
shapeUrl: frontendSync.shapeUrl ?? '/v1/shape',
|
|
1243
|
+
useTableParam: frontendSync.useTableParam ?? true,
|
|
1244
|
+
// Column mapper for snake_case to camelCase conversion (e.g., 'snakeCamelMapper')
|
|
1245
|
+
// Set to null/undefined if DB columns already match JS property names
|
|
1246
|
+
columnMapper: frontendSync.hasOwnProperty?.('columnMapper')
|
|
1247
|
+
? frontendSync.columnMapper
|
|
1248
|
+
: 'snakeCamelMapper',
|
|
1249
|
+
// Whether to wrap shapeUrl in new URL() constructor
|
|
1250
|
+
wrapInUrlConstructor: frontendSync.wrapInUrlConstructor ?? true,
|
|
1251
|
+
// Whether columnMapper needs () to call (true for functions, false for objects)
|
|
1252
|
+
columnMapperNeedsCall: frontendSync.columnMapperNeedsCall ?? true,
|
|
1253
|
+
// Import path for API_BASE_URL (if needed)
|
|
1254
|
+
apiBaseUrlImport: frontendSync.apiBaseUrlImport ?? null,
|
|
1255
|
+
},
|
|
1256
|
+
parsers: frontendConfig.parsers ?? {
|
|
1257
|
+
timestamptz: '(date: string) => new Date(date)',
|
|
1258
|
+
},
|
|
1259
|
+
collections: {
|
|
1260
|
+
// Schema prefix: 'schema.' for namespace import, '' for direct import
|
|
1261
|
+
schemaPrefix: frontendConfig.collections?.schemaPrefix ?? 'schema.',
|
|
1262
|
+
},
|
|
1263
|
+
},
|
|
1264
|
+
|
|
1265
|
+
// Naming configuration (for templates that need it)
|
|
1266
|
+
namingConfig,
|
|
1267
|
+
applicationLayerSuffix,
|
|
1268
|
+
queryLayerSuffix,
|
|
1269
|
+
|
|
1270
|
+
// Pre-computed class names with configured terminology
|
|
1271
|
+
createCommandClass,
|
|
1272
|
+
updateCommandClass,
|
|
1273
|
+
deleteCommandClass,
|
|
1274
|
+
getByIdQueryClass,
|
|
1275
|
+
listQueryClass,
|
|
1276
|
+
|
|
1277
|
+
// Generation toggles (what to generate)
|
|
1278
|
+
generate: {
|
|
1279
|
+
fieldMetadata: getProjectConfig()?.generate?.fieldMetadata ?? true,
|
|
1280
|
+
collections: getProjectConfig()?.generate?.collections ?? true,
|
|
1281
|
+
// Whether to generate index.ts in collections folder (for multi-file collection structure)
|
|
1282
|
+
collectionsIndex: getProjectConfig()?.generate?.collectionsIndex ?? false,
|
|
1283
|
+
hooks: getProjectConfig()?.generate?.hooks ?? true,
|
|
1284
|
+
mutations: getProjectConfig()?.generate?.mutations ?? true,
|
|
1285
|
+
// Backend toggles
|
|
1286
|
+
drizzleSchema: getProjectConfig()?.generate?.drizzleSchema ?? true,
|
|
1287
|
+
commands: getProjectConfig()?.generate?.commands ?? true,
|
|
1288
|
+
queries: getProjectConfig()?.generate?.queries ?? true,
|
|
1289
|
+
dtos: getProjectConfig()?.generate?.dtos ?? true,
|
|
1290
|
+
schemaServer: getProjectConfig()?.generate?.schemaServer ?? false,
|
|
1291
|
+
schemaClient: getProjectConfig()?.generate?.schemaClient ?? false,
|
|
1292
|
+
electricMigrations: getProjectConfig()?.generate?.electricMigrations ?? false,
|
|
1293
|
+
// Hook style: 'collection' uses collection.useMany(), 'useLiveQuery' uses TanStack DB pattern
|
|
1294
|
+
hookStyle: getProjectConfig()?.generate?.hookStyle ?? 'collection',
|
|
1295
|
+
// Output structure mode: 'entity-first' | 'concern-first' | 'monolithic'
|
|
1296
|
+
// entity-first: generated/{entity}/types.ts, collection.ts, hooks.ts...
|
|
1297
|
+
// concern-first: generated/types/{entity}.ts, collections/{entity}.ts...
|
|
1298
|
+
// monolithic: generated/{entity}.ts (single file per entity)
|
|
1299
|
+
structure: getProjectConfig()?.generate?.structure ?? 'monolithic',
|
|
1300
|
+
// Type naming: 'plain' = Opportunity, 'entity' = OpportunityEntity
|
|
1301
|
+
typeNaming: getProjectConfig()?.generate?.typeNaming ?? 'plain',
|
|
1302
|
+
// FK resolution: true = import related collections, false = skip (useful when collections don't exist)
|
|
1303
|
+
fkResolution: getProjectConfig()?.generate?.fkResolution ?? true,
|
|
1304
|
+
// Collection variable naming: 'singular' = opportunityCollection, 'plural' = opportunitiesCollection
|
|
1305
|
+
collectionNaming: getProjectConfig()?.generate?.collectionNaming ?? 'singular',
|
|
1306
|
+
// File naming: 'singular' = opportunity.ts, 'plural' = opportunities.ts
|
|
1307
|
+
fileNaming: getProjectConfig()?.generate?.fileNaming ?? 'singular',
|
|
1308
|
+
// Hook return style: 'generic' = { data }, 'named' = { opportunities }
|
|
1309
|
+
hookReturnStyle: getProjectConfig()?.generate?.hookReturnStyle ?? 'generic',
|
|
1310
|
+
},
|
|
1311
|
+
|
|
1312
|
+
// Pre-computed output paths for templates (avoids ternary in YAML frontmatter)
|
|
1313
|
+
outputPaths,
|
|
1314
|
+
|
|
1315
|
+
// Behavior strategy and resolved behaviors
|
|
1316
|
+
behaviorStrategy,
|
|
1317
|
+
behaviors: resolvedBehaviors,
|
|
1318
|
+
behaviorFields: resolvedBehaviors.fields,
|
|
1319
|
+
hasBehaviors: resolvedBehaviors.hasBehaviors,
|
|
1320
|
+
hasTimestamps: resolvedBehaviors.hasTimestamps,
|
|
1321
|
+
hasSoftDelete: resolvedBehaviors.hasSoftDelete,
|
|
1322
|
+
hasUserTracking: resolvedBehaviors.hasUserTracking,
|
|
1323
|
+
hasTemporalValidity: resolvedBehaviors.hasTemporalValidity,
|
|
1324
|
+
repositoryBehaviorConfig: resolvedBehaviors.repositoryConfig,
|
|
1325
|
+
|
|
1326
|
+
// Expose configuration (which layers to generate)
|
|
1327
|
+
expose: entity.expose || ["repository", "rest", "trpc"],
|
|
1328
|
+
exposeRepository: (
|
|
1329
|
+
entity.expose || ["repository", "rest", "trpc"]
|
|
1330
|
+
).includes("repository"),
|
|
1331
|
+
exposeRest: (entity.expose || ["repository", "rest", "trpc"]).includes(
|
|
1332
|
+
"rest",
|
|
1333
|
+
),
|
|
1334
|
+
exposeTrpc: (entity.expose || ["repository", "rest", "trpc"]).includes(
|
|
1335
|
+
"trpc",
|
|
1336
|
+
),
|
|
1337
|
+
exposeElectric: (
|
|
1338
|
+
entity.expose || ["repository", "rest", "trpc"]
|
|
1339
|
+
).includes("electric"),
|
|
1340
|
+
|
|
1341
|
+
// Electric SQL where clause (derived from entity FK fields)
|
|
1342
|
+
electricWhereColumn,
|
|
1343
|
+
electricWhereValue,
|
|
1344
|
+
|
|
1345
|
+
// ======================================================================
|
|
1346
|
+
// v2 variables
|
|
1347
|
+
// ======================================================================
|
|
1348
|
+
|
|
1349
|
+
// Architecture target (from generate.architecture config)
|
|
1350
|
+
architectureTarget,
|
|
1351
|
+
isCleanArchitecture,
|
|
1352
|
+
isCleanLitePs,
|
|
1353
|
+
frontendEnabled,
|
|
1354
|
+
|
|
1355
|
+
// Family
|
|
1356
|
+
family,
|
|
1357
|
+
hasFamily,
|
|
1358
|
+
familyBaseRepository,
|
|
1359
|
+
familyBaseService,
|
|
1360
|
+
|
|
1361
|
+
// Queries
|
|
1362
|
+
hasQueries,
|
|
1363
|
+
processedQueries,
|
|
1364
|
+
hasDeclarativeQueries,
|
|
1365
|
+
declarativeQueryClasses,
|
|
1366
|
+
hasMultiFieldQuery,
|
|
1367
|
+
hasOrderedQuery,
|
|
1368
|
+
|
|
1369
|
+
// Sync
|
|
1370
|
+
hasSyncBlock,
|
|
1371
|
+
syncElectric,
|
|
1372
|
+
hasSyncProviders,
|
|
1373
|
+
syncProviders,
|
|
1374
|
+
|
|
1375
|
+
// Events
|
|
1376
|
+
hasEvents,
|
|
1377
|
+
processedEvents,
|
|
1378
|
+
};
|
|
1379
|
+
|
|
1380
|
+
// ========================================================================
|
|
1381
|
+
// Clean-Lite-PS template locals
|
|
1382
|
+
//
|
|
1383
|
+
// Populated only when `generate.architecture === 'clean-lite-ps'`.
|
|
1384
|
+
// When the architecture is 'clean', stub locals are injected so CLP
|
|
1385
|
+
// template bodies can render without crashing; their `to:` guards resolve
|
|
1386
|
+
// to null which causes Hygen to skip file writing.
|
|
1387
|
+
// ========================================================================
|
|
1388
|
+
if (isCleanLitePs) {
|
|
1389
|
+
const { buildCleanLitePsLocals } = await import('./clean-lite-ps/prompt-extension.js');
|
|
1390
|
+
Object.assign(locals, buildCleanLitePsLocals(definition, locals));
|
|
1391
|
+
} else {
|
|
1392
|
+
// Inject safe stub locals so CLP template bodies can render without crashing.
|
|
1393
|
+
// The to: guard resolves to "null" which causes Hygen to skip file writing.
|
|
1394
|
+
const _n = definition.entity?.name || '';
|
|
1395
|
+
const _p = definition.entity?.plural || _n + 's';
|
|
1396
|
+
Object.assign(locals, {
|
|
1397
|
+
clpOutputPaths: undefined,
|
|
1398
|
+
entityName: _n,
|
|
1399
|
+
entityNamePlural: _p,
|
|
1400
|
+
entityNamePascal: _n,
|
|
1401
|
+
entityNamePluralPascal: _p,
|
|
1402
|
+
classNames: {},
|
|
1403
|
+
clpDrizzleImports: [],
|
|
1404
|
+
clpProcessedFields: [],
|
|
1405
|
+
clpCreateDtoFields: [],
|
|
1406
|
+
clpOutputDtoFields: [],
|
|
1407
|
+
clpBelongsTo: [],
|
|
1408
|
+
clpBelongsToFkFields: [],
|
|
1409
|
+
clpHasRelationsBlock: false,
|
|
1410
|
+
repositoryBaseClass: '',
|
|
1411
|
+
serviceBaseClass: '',
|
|
1412
|
+
repositoryBaseImport: '',
|
|
1413
|
+
serviceBaseImport: '',
|
|
1414
|
+
repositoryInheritedMethods: [],
|
|
1415
|
+
serviceInheritedMethods: [],
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
return locals;
|
|
1420
|
+
},
|
|
1421
|
+
};
|