@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,2198 @@
|
|
|
1
|
+
// src/parser/load-entities.ts
|
|
2
|
+
import { readdirSync } from "fs";
|
|
3
|
+
import { join, resolve } from "path";
|
|
4
|
+
|
|
5
|
+
// src/utils/yaml-loader.ts
|
|
6
|
+
import { readFileSync, existsSync } from "fs";
|
|
7
|
+
import { parse as parseYaml } from "yaml";
|
|
8
|
+
|
|
9
|
+
// src/schema/entity-definition.schema.ts
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
var FieldTypeSchema = z.enum([
|
|
12
|
+
"string",
|
|
13
|
+
"integer",
|
|
14
|
+
"decimal",
|
|
15
|
+
"boolean",
|
|
16
|
+
"uuid",
|
|
17
|
+
"date",
|
|
18
|
+
"datetime",
|
|
19
|
+
"json",
|
|
20
|
+
"entity_ref",
|
|
21
|
+
// Polymorphic reference: generates {field}EntityType + {field}EntityId columns
|
|
22
|
+
"string_array",
|
|
23
|
+
// Array of strings: generates text[] column
|
|
24
|
+
"enum"
|
|
25
|
+
// Enum type with choices or choices_from
|
|
26
|
+
]);
|
|
27
|
+
var UiTypeSchema = z.enum([
|
|
28
|
+
"text",
|
|
29
|
+
"textarea",
|
|
30
|
+
"number",
|
|
31
|
+
"money",
|
|
32
|
+
"percentage",
|
|
33
|
+
"email",
|
|
34
|
+
"url",
|
|
35
|
+
"date",
|
|
36
|
+
"datetime",
|
|
37
|
+
"boolean",
|
|
38
|
+
"enum",
|
|
39
|
+
"reference",
|
|
40
|
+
"json",
|
|
41
|
+
"badge",
|
|
42
|
+
"password"
|
|
43
|
+
]);
|
|
44
|
+
var UiImportanceSchema = z.enum(["primary", "secondary", "tertiary"]);
|
|
45
|
+
var AnalyticsAggregationSchema = z.enum([
|
|
46
|
+
"sum",
|
|
47
|
+
"min",
|
|
48
|
+
"max",
|
|
49
|
+
"count",
|
|
50
|
+
"count_distinct",
|
|
51
|
+
"average",
|
|
52
|
+
"median",
|
|
53
|
+
"percentile",
|
|
54
|
+
"sum_boolean"
|
|
55
|
+
]);
|
|
56
|
+
var AnalyticsDimensionTypeSchema = z.enum(["categorical", "time"]);
|
|
57
|
+
var AnalyticsEntityTypeSchema = z.enum(["primary", "unique", "foreign", "natural"]);
|
|
58
|
+
var AnalyticsTimeGranularitySchema = z.enum(["day", "week", "month", "quarter", "year"]);
|
|
59
|
+
var AnalyticsVisibilitySchema = z.enum(["internal", "agent", "public"]);
|
|
60
|
+
var NonAdditiveDimensionSchema = z.union([
|
|
61
|
+
z.string(),
|
|
62
|
+
z.object({
|
|
63
|
+
name: z.string(),
|
|
64
|
+
window_choice: z.string().optional(),
|
|
65
|
+
window_groupings: z.array(z.string()).optional()
|
|
66
|
+
})
|
|
67
|
+
]);
|
|
68
|
+
var SemanticMetadataSchema = z.object({
|
|
69
|
+
measure: z.boolean().optional(),
|
|
70
|
+
analytics_aggregation: AnalyticsAggregationSchema.optional(),
|
|
71
|
+
agg_time_dimension: z.string().optional(),
|
|
72
|
+
non_additive_dimension: NonAdditiveDimensionSchema.optional(),
|
|
73
|
+
dimension: z.boolean().optional(),
|
|
74
|
+
dimension_type: AnalyticsDimensionTypeSchema.optional(),
|
|
75
|
+
time_granularity: AnalyticsTimeGranularitySchema.optional(),
|
|
76
|
+
is_partition: z.boolean().optional(),
|
|
77
|
+
entity: z.boolean().optional(),
|
|
78
|
+
entity_type: AnalyticsEntityTypeSchema.optional(),
|
|
79
|
+
entity_role: z.string().optional(),
|
|
80
|
+
analytics_visibility: AnalyticsVisibilitySchema.optional(),
|
|
81
|
+
semantic_expr: z.string().optional(),
|
|
82
|
+
semantic_label: z.string().optional()
|
|
83
|
+
});
|
|
84
|
+
var UiMetadataSchema = z.object({
|
|
85
|
+
ui_label: z.string().optional(),
|
|
86
|
+
ui_type: UiTypeSchema.optional(),
|
|
87
|
+
ui_importance: UiImportanceSchema.optional(),
|
|
88
|
+
ui_group: z.string().optional(),
|
|
89
|
+
ui_sortable: z.boolean().optional(),
|
|
90
|
+
ui_filterable: z.boolean().optional(),
|
|
91
|
+
ui_visible: z.boolean().optional(),
|
|
92
|
+
ui_placeholder: z.string().optional(),
|
|
93
|
+
ui_help: z.string().optional(),
|
|
94
|
+
ui_format: z.record(z.unknown()).optional()
|
|
95
|
+
});
|
|
96
|
+
var BaseFieldSchema = z.object({
|
|
97
|
+
type: FieldTypeSchema,
|
|
98
|
+
required: z.boolean().optional().default(false),
|
|
99
|
+
nullable: z.boolean().optional().default(false),
|
|
100
|
+
// String constraints
|
|
101
|
+
max_length: z.number().int().positive().optional(),
|
|
102
|
+
min_length: z.number().int().nonnegative().optional(),
|
|
103
|
+
// Numeric constraints
|
|
104
|
+
min: z.number().optional(),
|
|
105
|
+
max: z.number().optional(),
|
|
106
|
+
// Enum/choices (inline definition)
|
|
107
|
+
choices: z.array(z.string()).optional(),
|
|
108
|
+
// Enum/choices from external file (e.g., "relationship_types.yaml")
|
|
109
|
+
// Mutually exclusive with choices - parser loads file and extracts keys
|
|
110
|
+
choices_from: z.string().optional(),
|
|
111
|
+
// Entity reference: allowed entity types for polymorphic refs
|
|
112
|
+
// Required when type is 'entity_ref'
|
|
113
|
+
allowed_types: z.array(z.string()).optional(),
|
|
114
|
+
// Default value
|
|
115
|
+
default: z.unknown().optional(),
|
|
116
|
+
// Indexing
|
|
117
|
+
index: z.boolean().optional(),
|
|
118
|
+
unique: z.boolean().optional(),
|
|
119
|
+
// Foreign key reference (e.g., "accounts.id")
|
|
120
|
+
foreign_key: z.string().optional()
|
|
121
|
+
});
|
|
122
|
+
var FieldDefinitionSchema = BaseFieldSchema.merge(UiMetadataSchema).merge(SemanticMetadataSchema).refine((data) => !(data.required === true && data.nullable === true), {
|
|
123
|
+
message: "'required: true' and 'nullable: true' cannot both be set. A required field cannot be null.",
|
|
124
|
+
path: ["required"]
|
|
125
|
+
}).refine(
|
|
126
|
+
(data) => {
|
|
127
|
+
if (data.min_length !== void 0 && data.type !== "string") {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
return true;
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
message: "'min_length' can only be used with type 'string'",
|
|
134
|
+
path: ["min_length"]
|
|
135
|
+
}
|
|
136
|
+
).refine(
|
|
137
|
+
(data) => {
|
|
138
|
+
if (data.max_length !== void 0 && data.type !== "string") {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
return true;
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
message: "'max_length' can only be used with type 'string'",
|
|
145
|
+
path: ["max_length"]
|
|
146
|
+
}
|
|
147
|
+
).refine(
|
|
148
|
+
(data) => {
|
|
149
|
+
if (data.min !== void 0 && !["integer", "decimal"].includes(data.type)) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
return true;
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
message: "'min' can only be used with numeric types",
|
|
156
|
+
path: ["min"]
|
|
157
|
+
}
|
|
158
|
+
).refine(
|
|
159
|
+
(data) => {
|
|
160
|
+
if (data.max !== void 0 && !["integer", "decimal"].includes(data.type)) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
return true;
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
message: "'max' can only be used with numeric types",
|
|
167
|
+
path: ["max"]
|
|
168
|
+
}
|
|
169
|
+
).refine(
|
|
170
|
+
(data) => {
|
|
171
|
+
if (data.type === "entity_ref" && !data.allowed_types?.length) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
return true;
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
message: "'entity_ref' type requires 'allowed_types' to be specified",
|
|
178
|
+
path: ["allowed_types"]
|
|
179
|
+
}
|
|
180
|
+
).refine(
|
|
181
|
+
(data) => {
|
|
182
|
+
if (data.allowed_types !== void 0 && data.type !== "entity_ref") {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
return true;
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
message: "'allowed_types' can only be used with type 'entity_ref'",
|
|
189
|
+
path: ["allowed_types"]
|
|
190
|
+
}
|
|
191
|
+
).refine(
|
|
192
|
+
(data) => {
|
|
193
|
+
if (data.choices !== void 0 && data.choices_from !== void 0) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
return true;
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
message: "'choices' and 'choices_from' cannot both be specified",
|
|
200
|
+
path: ["choices_from"]
|
|
201
|
+
}
|
|
202
|
+
).refine(
|
|
203
|
+
(data) => {
|
|
204
|
+
if (data.type === "enum" && !data.choices?.length && !data.choices_from) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
return true;
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
message: "'enum' type requires either 'choices' or 'choices_from'",
|
|
211
|
+
path: ["choices"]
|
|
212
|
+
}
|
|
213
|
+
).refine(
|
|
214
|
+
(data) => {
|
|
215
|
+
if (data.measure === true && !data.analytics_aggregation) {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
return true;
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
message: "When 'measure' is true, 'analytics_aggregation' must be specified",
|
|
222
|
+
path: ["analytics_aggregation"]
|
|
223
|
+
}
|
|
224
|
+
);
|
|
225
|
+
var RelationshipTypeSchema = z.enum(["belongs_to", "has_many", "has_one"]);
|
|
226
|
+
var RelationshipSchema = z.object({
|
|
227
|
+
type: RelationshipTypeSchema,
|
|
228
|
+
target: z.string(),
|
|
229
|
+
// Target entity name (e.g., "account")
|
|
230
|
+
foreign_key: z.string(),
|
|
231
|
+
// FK field name (e.g., "account_id")
|
|
232
|
+
through: z.string().optional(),
|
|
233
|
+
// For transitive: "owned_opportunities.updates"
|
|
234
|
+
inverse: z.string().optional()
|
|
235
|
+
// Name of inverse relationship on target entity
|
|
236
|
+
}).strict();
|
|
237
|
+
var BehaviorConfigSchema = z.union([
|
|
238
|
+
z.string(),
|
|
239
|
+
z.object({
|
|
240
|
+
name: z.string(),
|
|
241
|
+
options: z.record(z.unknown()).optional()
|
|
242
|
+
})
|
|
243
|
+
]);
|
|
244
|
+
var BehaviorStrategySchema = z.enum(["base_class", "inline"]);
|
|
245
|
+
var FolderStructureSchema = z.enum(["nested", "flat"]).default("nested");
|
|
246
|
+
var FileGroupingSchema = z.enum(["separate", "grouped"]).default("separate");
|
|
247
|
+
var ExposeLayerSchema = z.enum(["repository", "rest", "trpc", "electric"]);
|
|
248
|
+
var EntityConfigSchema = z.object({
|
|
249
|
+
name: z.string().regex(
|
|
250
|
+
/^[a-z][a-z0-9_]*$/,
|
|
251
|
+
"Entity name must be lowercase with underscores (e.g., 'opportunity')"
|
|
252
|
+
),
|
|
253
|
+
plural: z.string().regex(/^[a-z][a-z0-9_]*$/, "Plural must be lowercase"),
|
|
254
|
+
table: z.string().regex(/^[a-z][a-z0-9_]*$/, "Table must be lowercase"),
|
|
255
|
+
// Layout options (orthogonal concerns)
|
|
256
|
+
// folder_structure: controls directory nesting
|
|
257
|
+
// file_grouping: controls file organization
|
|
258
|
+
folder_structure: FolderStructureSchema.optional(),
|
|
259
|
+
file_grouping: FileGroupingSchema.optional(),
|
|
260
|
+
// Per-entity behavior strategy override (overrides codegen.config.yaml)
|
|
261
|
+
behavior_strategy: BehaviorStrategySchema.optional(),
|
|
262
|
+
// Which layers to generate (default: all)
|
|
263
|
+
expose: z.array(ExposeLayerSchema).optional().default(["repository", "rest", "trpc"]),
|
|
264
|
+
// v2: Entity family classification (ADR-005)
|
|
265
|
+
// Determines which base class hierarchy the entity inherits from
|
|
266
|
+
family: z.enum(["base", "synced", "activity", "knowledge", "metadata"]).optional()
|
|
267
|
+
}).strict();
|
|
268
|
+
var QueryDeclarationSchema = z.object({
|
|
269
|
+
by: z.array(z.string()).min(1),
|
|
270
|
+
unique: z.boolean().optional(),
|
|
271
|
+
select: z.array(z.string()).optional(),
|
|
272
|
+
order: z.string().optional(),
|
|
273
|
+
limit: z.boolean().optional(),
|
|
274
|
+
via: z.string().optional()
|
|
275
|
+
});
|
|
276
|
+
var SyncDirectionSchema = z.enum([
|
|
277
|
+
"inbound",
|
|
278
|
+
"outbound",
|
|
279
|
+
"bidirectional"
|
|
280
|
+
]);
|
|
281
|
+
var ProviderSyncSchema = z.object({
|
|
282
|
+
remote_entity: z.string(),
|
|
283
|
+
direction: SyncDirectionSchema,
|
|
284
|
+
cdc: z.boolean().optional().default(false),
|
|
285
|
+
field_mapping: z.record(z.string(), z.string()).optional(),
|
|
286
|
+
read_only_fields: z.array(z.string()).optional()
|
|
287
|
+
});
|
|
288
|
+
var SyncConfigSchema = z.object({
|
|
289
|
+
electric: z.boolean().optional().default(false),
|
|
290
|
+
providers: z.record(z.string(), ProviderSyncSchema).optional()
|
|
291
|
+
});
|
|
292
|
+
var EventDeclarationSchema = z.object({
|
|
293
|
+
name: z.string().regex(/^[a-z][a-z0-9_]*$/, "Event name must be snake_case"),
|
|
294
|
+
queue: z.string(),
|
|
295
|
+
body: z.record(z.string(), z.string()),
|
|
296
|
+
generate_handler: z.boolean().optional().default(false)
|
|
297
|
+
});
|
|
298
|
+
var SimpleMetricSchema = z.object({
|
|
299
|
+
type: z.literal("simple"),
|
|
300
|
+
measure: z.string(),
|
|
301
|
+
agg: AnalyticsAggregationSchema.optional(),
|
|
302
|
+
filter: z.string().optional(),
|
|
303
|
+
description: z.string().optional(),
|
|
304
|
+
label: z.string().optional()
|
|
305
|
+
});
|
|
306
|
+
var DerivedMetricSchema = z.object({
|
|
307
|
+
type: z.literal("derived"),
|
|
308
|
+
expr: z.string(),
|
|
309
|
+
metrics: z.array(z.string()),
|
|
310
|
+
description: z.string().optional(),
|
|
311
|
+
label: z.string().optional()
|
|
312
|
+
});
|
|
313
|
+
var RatioMetricSchema = z.object({
|
|
314
|
+
type: z.literal("ratio"),
|
|
315
|
+
numerator: z.union([z.string(), SimpleMetricSchema]),
|
|
316
|
+
denominator: z.union([z.string(), SimpleMetricSchema]),
|
|
317
|
+
filter: z.string().optional(),
|
|
318
|
+
description: z.string().optional(),
|
|
319
|
+
label: z.string().optional()
|
|
320
|
+
});
|
|
321
|
+
var CumulativeMetricSchema = z.object({
|
|
322
|
+
type: z.literal("cumulative"),
|
|
323
|
+
measure: z.string(),
|
|
324
|
+
window: z.string().optional(),
|
|
325
|
+
grain_to_date: AnalyticsTimeGranularitySchema.optional(),
|
|
326
|
+
description: z.string().optional(),
|
|
327
|
+
label: z.string().optional()
|
|
328
|
+
});
|
|
329
|
+
var MetricDefinitionSchema = z.discriminatedUnion("type", [
|
|
330
|
+
SimpleMetricSchema,
|
|
331
|
+
DerivedMetricSchema,
|
|
332
|
+
RatioMetricSchema,
|
|
333
|
+
CumulativeMetricSchema
|
|
334
|
+
]);
|
|
335
|
+
var AnalyticsBlockSchema = z.object({
|
|
336
|
+
measure_packs: z.array(z.string()).optional(),
|
|
337
|
+
cube_name: z.string().optional(),
|
|
338
|
+
metrics: z.record(z.string(), MetricDefinitionSchema).optional()
|
|
339
|
+
});
|
|
340
|
+
var EntityDefinitionSchema = z.object({
|
|
341
|
+
entity: EntityConfigSchema,
|
|
342
|
+
fields: z.record(z.string(), FieldDefinitionSchema),
|
|
343
|
+
relationships: z.record(z.string(), RelationshipSchema).optional(),
|
|
344
|
+
// Behaviors add cross-cutting concerns (timestamps, soft_delete, user_tracking, etc.)
|
|
345
|
+
behaviors: z.array(BehaviorConfigSchema).optional().default([]),
|
|
346
|
+
// v2: Declarative query generation (ADR-005)
|
|
347
|
+
// Generates repository + service + use case methods from declarations
|
|
348
|
+
queries: z.array(QueryDeclarationSchema).optional(),
|
|
349
|
+
// v2: Integration sync configuration (CODEGEN-EVOLUTION-PLAN Phase 2)
|
|
350
|
+
// Electric SQL + provider sync (Salesforce, HubSpot, etc.)
|
|
351
|
+
sync: SyncConfigSchema.optional(),
|
|
352
|
+
// v2: Domain event declarations (CODEGEN-EVOLUTION-PLAN Phase 2)
|
|
353
|
+
// Generates typed event classes, handlers, and queue registration
|
|
354
|
+
events: z.array(EventDeclarationSchema).optional(),
|
|
355
|
+
// v2: Analytics / semantic layer configuration
|
|
356
|
+
// Cube.js measure packs, custom cube name, and metric definitions
|
|
357
|
+
analytics: AnalyticsBlockSchema.optional()
|
|
358
|
+
}).strict();
|
|
359
|
+
|
|
360
|
+
// src/schema/relationship-definition.schema.ts
|
|
361
|
+
import { z as z2 } from "zod";
|
|
362
|
+
var TypeDirectionSchema = z2.object({
|
|
363
|
+
/** Name of the inverse type when viewed from the other direction */
|
|
364
|
+
inverse: z2.string().optional(),
|
|
365
|
+
/** Both directions are equivalent — queries should check both FK columns */
|
|
366
|
+
bidirectional: z2.boolean().optional(),
|
|
367
|
+
/** Explicitly directed, no named inverse (default behavior) */
|
|
368
|
+
directed: z2.boolean().optional()
|
|
369
|
+
}).refine(
|
|
370
|
+
(data) => {
|
|
371
|
+
const set = [data.inverse, data.bidirectional, data.directed].filter(
|
|
372
|
+
(v) => v !== void 0
|
|
373
|
+
);
|
|
374
|
+
return set.length === 1;
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
message: "Exactly one of inverse, bidirectional, or directed must be specified"
|
|
378
|
+
}
|
|
379
|
+
);
|
|
380
|
+
var RelationshipTypesSchema = z2.union([
|
|
381
|
+
// Simple list: all types are directed from→to
|
|
382
|
+
z2.array(z2.string().regex(/^[a-z][a-z0-9_]*$/, "Type must be snake_case")),
|
|
383
|
+
// Object map: each type has direction metadata
|
|
384
|
+
z2.record(
|
|
385
|
+
z2.string().regex(/^[a-z][a-z0-9_]*$/, "Type key must be snake_case"),
|
|
386
|
+
TypeDirectionSchema
|
|
387
|
+
)
|
|
388
|
+
]);
|
|
389
|
+
var OnDeleteActionSchema = z2.enum(["restrict", "cascade", "set_null", "no_action"]).default("restrict");
|
|
390
|
+
var RelationshipConfigSchema = z2.object({
|
|
391
|
+
/** Relationship name (snake_case). Used for class/file naming. */
|
|
392
|
+
name: z2.string().regex(
|
|
393
|
+
/^[a-z][a-z0-9_]*$/,
|
|
394
|
+
"Relationship name must be snake_case"
|
|
395
|
+
),
|
|
396
|
+
/** Database table name. Defaults to {name}s if not specified. */
|
|
397
|
+
table: z2.string().regex(/^[a-z][a-z0-9_]*$/, "Table must be snake_case").optional(),
|
|
398
|
+
/** The "from" entity — generates {entity}_id FK column (subject). */
|
|
399
|
+
from: z2.string().regex(/^[a-z][a-z0-9_]*$/, "Entity name must be snake_case"),
|
|
400
|
+
/** The "to" entity — generates {entity}_id FK column (object). */
|
|
401
|
+
to: z2.string().regex(/^[a-z][a-z0-9_]*$/, "Entity name must be snake_case"),
|
|
402
|
+
/**
|
|
403
|
+
* Relationship subtypes. Optional — omit for untyped junctions.
|
|
404
|
+
* When present, generates a `type` enum column on the junction table.
|
|
405
|
+
*
|
|
406
|
+
* Simple list: all types are directed (from→to). Use for cross-type
|
|
407
|
+
* relationships where entity asymmetry makes direction obvious.
|
|
408
|
+
*
|
|
409
|
+
* Object map: each type declares its own direction metadata.
|
|
410
|
+
* Required for self-referential relationships (from === to).
|
|
411
|
+
*/
|
|
412
|
+
types: RelationshipTypesSchema.optional(),
|
|
413
|
+
/**
|
|
414
|
+
* Generate temporal validity fields: valid_from (date), valid_to (date?),
|
|
415
|
+
* is_current (boolean, denormalized for query performance).
|
|
416
|
+
* Default: true
|
|
417
|
+
*/
|
|
418
|
+
temporal: z2.boolean().default(true),
|
|
419
|
+
/**
|
|
420
|
+
* Generate source tracking fields: source (enum), confidence (decimal 0-1).
|
|
421
|
+
* Default: true
|
|
422
|
+
*/
|
|
423
|
+
sourced: z2.boolean().default(true),
|
|
424
|
+
/** on_delete action for the "from" endpoint FK. Default: restrict */
|
|
425
|
+
on_delete_from: OnDeleteActionSchema.optional(),
|
|
426
|
+
/** on_delete action for the "to" endpoint FK. Default: restrict */
|
|
427
|
+
on_delete_to: OnDeleteActionSchema.optional(),
|
|
428
|
+
/**
|
|
429
|
+
* Override the default unique constraint columns.
|
|
430
|
+
*
|
|
431
|
+
* Defaults:
|
|
432
|
+
* - Typed: [from_id, to_id, type]
|
|
433
|
+
* - Typed + temporal: [from_id, to_id, type, valid_from]
|
|
434
|
+
* - Untyped: [from_id, to_id]
|
|
435
|
+
*
|
|
436
|
+
* Use this when the default doesn't fit — e.g., allowing multiple
|
|
437
|
+
* relationships of the same type between the same entities at different times.
|
|
438
|
+
*/
|
|
439
|
+
unique_on: z2.array(z2.string()).optional()
|
|
440
|
+
}).strict();
|
|
441
|
+
var RelationshipQuerySchema = z2.object({
|
|
442
|
+
by: z2.array(z2.string()).min(1),
|
|
443
|
+
unique: z2.boolean().optional(),
|
|
444
|
+
select: z2.array(z2.string()).optional(),
|
|
445
|
+
order: z2.string().optional(),
|
|
446
|
+
limit: z2.boolean().optional()
|
|
447
|
+
});
|
|
448
|
+
var RelationshipDefinitionSchema = z2.object({
|
|
449
|
+
/** Relationship configuration block */
|
|
450
|
+
relationship: RelationshipConfigSchema,
|
|
451
|
+
/**
|
|
452
|
+
* Additional fields beyond auto-generated ones.
|
|
453
|
+
* These describe the relationship, not either endpoint entity.
|
|
454
|
+
* Uses the same field definition schema as entity fields.
|
|
455
|
+
*/
|
|
456
|
+
fields: z2.record(z2.string(), z2.any()).optional(),
|
|
457
|
+
/** Declarative queries — same syntax as entity queries. */
|
|
458
|
+
queries: z2.array(RelationshipQuerySchema).optional()
|
|
459
|
+
}).strict().refine(
|
|
460
|
+
(data) => {
|
|
461
|
+
if (data.relationship.from === data.relationship.to && data.relationship.types) {
|
|
462
|
+
return !Array.isArray(data.relationship.types);
|
|
463
|
+
}
|
|
464
|
+
return true;
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
message: "Self-referential relationships must use the object map form for types (with inverse/bidirectional/directed metadata), not a simple list",
|
|
468
|
+
path: ["relationship", "types"]
|
|
469
|
+
}
|
|
470
|
+
).refine(
|
|
471
|
+
(data) => {
|
|
472
|
+
if (!data.fields) return true;
|
|
473
|
+
const reserved = getReservedColumnNames(data.relationship);
|
|
474
|
+
const collisions = Object.keys(data.fields).filter(
|
|
475
|
+
(key) => reserved.has(key)
|
|
476
|
+
);
|
|
477
|
+
return collisions.length === 0;
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
message: "fields: contains keys that collide with auto-generated columns. Reserved names depend on config (type, valid_from, valid_to, is_current, source, confidence, id, created_at, updated_at, and FK columns).",
|
|
481
|
+
path: ["fields"]
|
|
482
|
+
}
|
|
483
|
+
);
|
|
484
|
+
function getReservedColumnNames(config) {
|
|
485
|
+
const { fromColumn, toColumn } = deriveRelationshipFKColumns(config);
|
|
486
|
+
const reserved = /* @__PURE__ */ new Set([
|
|
487
|
+
"id",
|
|
488
|
+
"created_at",
|
|
489
|
+
"updated_at",
|
|
490
|
+
fromColumn,
|
|
491
|
+
toColumn
|
|
492
|
+
]);
|
|
493
|
+
if (config.types) {
|
|
494
|
+
reserved.add("type");
|
|
495
|
+
}
|
|
496
|
+
if (config.temporal) {
|
|
497
|
+
reserved.add("valid_from");
|
|
498
|
+
reserved.add("valid_to");
|
|
499
|
+
reserved.add("is_current");
|
|
500
|
+
}
|
|
501
|
+
if (config.sourced) {
|
|
502
|
+
reserved.add("source");
|
|
503
|
+
reserved.add("confidence");
|
|
504
|
+
}
|
|
505
|
+
return reserved;
|
|
506
|
+
}
|
|
507
|
+
function deriveRelationshipFKColumns(config) {
|
|
508
|
+
if (config.from === config.to) {
|
|
509
|
+
return {
|
|
510
|
+
fromColumn: `from_${config.from}_id`,
|
|
511
|
+
toColumn: `to_${config.to}_id`
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
return {
|
|
515
|
+
fromColumn: `${config.from}_id`,
|
|
516
|
+
toColumn: `${config.to}_id`
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
function deriveTableName(config) {
|
|
520
|
+
return config.table ?? `${config.name}s`;
|
|
521
|
+
}
|
|
522
|
+
function deriveUniqueConstraint(config) {
|
|
523
|
+
if (config.unique_on) return config.unique_on;
|
|
524
|
+
const { fromColumn, toColumn } = deriveRelationshipFKColumns(config);
|
|
525
|
+
const columns = [fromColumn, toColumn];
|
|
526
|
+
if (config.types) {
|
|
527
|
+
columns.push("type");
|
|
528
|
+
}
|
|
529
|
+
if (config.temporal && config.types) {
|
|
530
|
+
columns.push("valid_from");
|
|
531
|
+
}
|
|
532
|
+
return columns;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// src/utils/yaml-loader.ts
|
|
536
|
+
function loadEntityFromYaml(filePath) {
|
|
537
|
+
if (!existsSync(filePath)) {
|
|
538
|
+
return {
|
|
539
|
+
success: false,
|
|
540
|
+
error: `File not found: ${filePath}`,
|
|
541
|
+
filePath
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
let content;
|
|
545
|
+
try {
|
|
546
|
+
content = readFileSync(filePath, "utf-8");
|
|
547
|
+
} catch (err) {
|
|
548
|
+
return {
|
|
549
|
+
success: false,
|
|
550
|
+
error: `Failed to read file: ${filePath}`,
|
|
551
|
+
details: [err instanceof Error ? err.message : String(err)],
|
|
552
|
+
filePath
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
let parsed;
|
|
556
|
+
try {
|
|
557
|
+
parsed = parseYaml(content);
|
|
558
|
+
} catch (err) {
|
|
559
|
+
return {
|
|
560
|
+
success: false,
|
|
561
|
+
error: `Invalid YAML syntax in ${filePath}`,
|
|
562
|
+
details: [err instanceof Error ? err.message : String(err)],
|
|
563
|
+
filePath
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
const result = EntityDefinitionSchema.safeParse(parsed);
|
|
567
|
+
if (!result.success) {
|
|
568
|
+
return {
|
|
569
|
+
success: false,
|
|
570
|
+
error: `Validation failed for ${filePath}`,
|
|
571
|
+
details: formatZodErrors(result.error),
|
|
572
|
+
filePath
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
return {
|
|
576
|
+
success: true,
|
|
577
|
+
definition: result.data,
|
|
578
|
+
filePath
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
function formatZodErrors(error) {
|
|
582
|
+
return error.errors.map((err) => {
|
|
583
|
+
const path = err.path.join(".");
|
|
584
|
+
const location = path ? `at '${path}'` : "at root";
|
|
585
|
+
return `${err.message} ${location}`;
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
function loadRelationshipFromYaml(filePath) {
|
|
589
|
+
if (!existsSync(filePath)) {
|
|
590
|
+
return {
|
|
591
|
+
success: false,
|
|
592
|
+
error: `File not found: ${filePath}`,
|
|
593
|
+
filePath
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
let content;
|
|
597
|
+
try {
|
|
598
|
+
content = readFileSync(filePath, "utf-8");
|
|
599
|
+
} catch (err) {
|
|
600
|
+
return {
|
|
601
|
+
success: false,
|
|
602
|
+
error: `Failed to read file: ${filePath}`,
|
|
603
|
+
details: [err instanceof Error ? err.message : String(err)],
|
|
604
|
+
filePath
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
let parsed;
|
|
608
|
+
try {
|
|
609
|
+
parsed = parseYaml(content);
|
|
610
|
+
} catch (err) {
|
|
611
|
+
return {
|
|
612
|
+
success: false,
|
|
613
|
+
error: `Invalid YAML syntax in ${filePath}`,
|
|
614
|
+
details: [err instanceof Error ? err.message : String(err)],
|
|
615
|
+
filePath
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
const result = RelationshipDefinitionSchema.safeParse(parsed);
|
|
619
|
+
if (!result.success) {
|
|
620
|
+
return {
|
|
621
|
+
success: false,
|
|
622
|
+
error: `Validation failed for ${filePath}`,
|
|
623
|
+
details: formatZodErrors(result.error),
|
|
624
|
+
filePath
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
return {
|
|
628
|
+
success: true,
|
|
629
|
+
definition: result.data,
|
|
630
|
+
filePath
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// src/parser/load-entities.ts
|
|
635
|
+
function transformToEntity(result) {
|
|
636
|
+
const { definition, filePath } = result;
|
|
637
|
+
const queries = definition.queries?.map((q) => ({
|
|
638
|
+
by: q.by,
|
|
639
|
+
unique: q.unique,
|
|
640
|
+
select: q.select,
|
|
641
|
+
order: q.order,
|
|
642
|
+
limit: q.limit,
|
|
643
|
+
via: q.via
|
|
644
|
+
}));
|
|
645
|
+
const entity = {
|
|
646
|
+
name: definition.entity.name,
|
|
647
|
+
plural: definition.entity.plural,
|
|
648
|
+
table: definition.entity.table,
|
|
649
|
+
family: definition.entity.family,
|
|
650
|
+
folderStructure: definition.entity.folder_structure ?? "nested",
|
|
651
|
+
fields: /* @__PURE__ */ new Map(),
|
|
652
|
+
relationships: /* @__PURE__ */ new Map(),
|
|
653
|
+
behaviors: definition.behaviors.map((b) => typeof b === "string" ? b : b.name),
|
|
654
|
+
queries,
|
|
655
|
+
sourcePath: filePath
|
|
656
|
+
};
|
|
657
|
+
for (const [name, fieldDef] of Object.entries(definition.fields)) {
|
|
658
|
+
const field = {
|
|
659
|
+
name,
|
|
660
|
+
type: fieldDef.type,
|
|
661
|
+
required: fieldDef.required ?? false,
|
|
662
|
+
nullable: fieldDef.nullable ?? false,
|
|
663
|
+
unique: fieldDef.unique ?? false,
|
|
664
|
+
index: fieldDef.index ?? false,
|
|
665
|
+
foreignKey: fieldDef.foreign_key ? parseForeignKey(fieldDef.foreign_key) : void 0,
|
|
666
|
+
choices: fieldDef.choices,
|
|
667
|
+
constraints: {
|
|
668
|
+
minLength: fieldDef.min_length,
|
|
669
|
+
maxLength: fieldDef.max_length,
|
|
670
|
+
min: fieldDef.min,
|
|
671
|
+
max: fieldDef.max
|
|
672
|
+
},
|
|
673
|
+
ui: {
|
|
674
|
+
label: fieldDef.ui_label,
|
|
675
|
+
type: fieldDef.ui_type,
|
|
676
|
+
importance: fieldDef.ui_importance,
|
|
677
|
+
group: fieldDef.ui_group,
|
|
678
|
+
sortable: fieldDef.ui_sortable,
|
|
679
|
+
filterable: fieldDef.ui_filterable,
|
|
680
|
+
visible: fieldDef.ui_visible
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
entity.fields.set(name, field);
|
|
684
|
+
}
|
|
685
|
+
if (definition.relationships) {
|
|
686
|
+
for (const [name, relDef] of Object.entries(definition.relationships)) {
|
|
687
|
+
const relationship = {
|
|
688
|
+
name,
|
|
689
|
+
type: relDef.type,
|
|
690
|
+
target: relDef.target,
|
|
691
|
+
foreignKey: relDef.foreign_key,
|
|
692
|
+
inverse: relDef.inverse,
|
|
693
|
+
through: relDef.through,
|
|
694
|
+
resolved: false
|
|
695
|
+
};
|
|
696
|
+
entity.relationships.set(name, relationship);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
if (definition.sync) {
|
|
700
|
+
const syncDef = definition.sync;
|
|
701
|
+
const parsedSync = {
|
|
702
|
+
electric: syncDef.electric ?? false
|
|
703
|
+
};
|
|
704
|
+
if (syncDef.providers) {
|
|
705
|
+
parsedSync.providers = {};
|
|
706
|
+
for (const [providerName, providerDef] of Object.entries(syncDef.providers)) {
|
|
707
|
+
const parsedProvider = {
|
|
708
|
+
remoteEntity: providerDef.remote_entity,
|
|
709
|
+
direction: providerDef.direction,
|
|
710
|
+
cdc: providerDef.cdc ?? false
|
|
711
|
+
};
|
|
712
|
+
if (providerDef.field_mapping) {
|
|
713
|
+
parsedProvider.fieldMapping = providerDef.field_mapping;
|
|
714
|
+
}
|
|
715
|
+
if (providerDef.read_only_fields) {
|
|
716
|
+
parsedProvider.readOnlyFields = providerDef.read_only_fields;
|
|
717
|
+
}
|
|
718
|
+
parsedSync.providers[providerName] = parsedProvider;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
entity.sync = parsedSync;
|
|
722
|
+
}
|
|
723
|
+
if (definition.events) {
|
|
724
|
+
entity.events = definition.events.map((ev) => ({
|
|
725
|
+
name: ev.name,
|
|
726
|
+
queue: ev.queue,
|
|
727
|
+
body: ev.body,
|
|
728
|
+
generateHandler: ev.generate_handler
|
|
729
|
+
}));
|
|
730
|
+
}
|
|
731
|
+
return entity;
|
|
732
|
+
}
|
|
733
|
+
function parseForeignKey(fk) {
|
|
734
|
+
const [table, column] = fk.split(".");
|
|
735
|
+
return { table, column: column ?? "id" };
|
|
736
|
+
}
|
|
737
|
+
function loadErrorToIssue(error) {
|
|
738
|
+
const issues = [];
|
|
739
|
+
issues.push({
|
|
740
|
+
severity: "error",
|
|
741
|
+
type: "parse_error",
|
|
742
|
+
message: error.error,
|
|
743
|
+
path: error.filePath
|
|
744
|
+
});
|
|
745
|
+
if (error.details) {
|
|
746
|
+
for (const detail of error.details) {
|
|
747
|
+
issues.push({
|
|
748
|
+
severity: "error",
|
|
749
|
+
type: "schema_error",
|
|
750
|
+
message: detail,
|
|
751
|
+
path: error.filePath
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
return issues;
|
|
756
|
+
}
|
|
757
|
+
function loadEntities(entitiesDir) {
|
|
758
|
+
const entities = [];
|
|
759
|
+
const issues = [];
|
|
760
|
+
const resolvedDir = resolve(entitiesDir);
|
|
761
|
+
let files;
|
|
762
|
+
try {
|
|
763
|
+
files = readdirSync(resolvedDir).filter((f) => f.endsWith(".yaml") || f.endsWith(".yml")).map((f) => join(resolvedDir, f));
|
|
764
|
+
} catch (err) {
|
|
765
|
+
issues.push({
|
|
766
|
+
severity: "error",
|
|
767
|
+
type: "parse_error",
|
|
768
|
+
message: `Failed to read directory: ${resolvedDir}`,
|
|
769
|
+
path: resolvedDir
|
|
770
|
+
});
|
|
771
|
+
return { entities, issues };
|
|
772
|
+
}
|
|
773
|
+
if (files.length === 0) {
|
|
774
|
+
issues.push({
|
|
775
|
+
severity: "warning",
|
|
776
|
+
type: "no_files",
|
|
777
|
+
message: `No YAML files found in directory: ${resolvedDir}`,
|
|
778
|
+
path: resolvedDir
|
|
779
|
+
});
|
|
780
|
+
return { entities, issues };
|
|
781
|
+
}
|
|
782
|
+
for (const filePath of files) {
|
|
783
|
+
const result = loadEntityFromYaml(filePath);
|
|
784
|
+
if (result.success) {
|
|
785
|
+
entities.push(transformToEntity(result));
|
|
786
|
+
} else {
|
|
787
|
+
issues.push(...loadErrorToIssue(result));
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
return { entities, issues };
|
|
791
|
+
}
|
|
792
|
+
function resolveReferences(entities) {
|
|
793
|
+
const issues = [];
|
|
794
|
+
const entityMap = /* @__PURE__ */ new Map();
|
|
795
|
+
for (const entity of entities) {
|
|
796
|
+
if (entityMap.has(entity.name)) {
|
|
797
|
+
issues.push({
|
|
798
|
+
severity: "error",
|
|
799
|
+
type: "duplicate_entity",
|
|
800
|
+
entity: entity.name,
|
|
801
|
+
message: `Duplicate entity name: ${entity.name}`,
|
|
802
|
+
path: entity.sourcePath
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
entityMap.set(entity.name, entity);
|
|
806
|
+
}
|
|
807
|
+
for (const entity of entities) {
|
|
808
|
+
for (const [relName, rel] of entity.relationships) {
|
|
809
|
+
const targetEntity = entityMap.get(rel.target);
|
|
810
|
+
if (targetEntity) {
|
|
811
|
+
rel.resolved = true;
|
|
812
|
+
} else {
|
|
813
|
+
issues.push({
|
|
814
|
+
severity: "error",
|
|
815
|
+
type: "missing_target",
|
|
816
|
+
entity: entity.name,
|
|
817
|
+
field: relName,
|
|
818
|
+
message: `Relationship '${relName}' references unknown entity '${rel.target}'`,
|
|
819
|
+
path: entity.sourcePath,
|
|
820
|
+
suggestion: `Define entity '${rel.target}' or fix the target name`
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
for (const [fieldName, field] of entity.fields) {
|
|
825
|
+
if (field.foreignKey) {
|
|
826
|
+
const targetTable = field.foreignKey.table;
|
|
827
|
+
const targetEntity = Array.from(entityMap.values()).find(
|
|
828
|
+
(e) => e.table === targetTable
|
|
829
|
+
);
|
|
830
|
+
if (!targetEntity) {
|
|
831
|
+
issues.push({
|
|
832
|
+
severity: "warning",
|
|
833
|
+
type: "missing_fk_target",
|
|
834
|
+
entity: entity.name,
|
|
835
|
+
field: fieldName,
|
|
836
|
+
message: `Foreign key references unknown table '${targetTable}'`,
|
|
837
|
+
path: entity.sourcePath,
|
|
838
|
+
suggestion: `Define entity with table '${targetTable}' or fix the foreign_key reference`
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
return issues;
|
|
845
|
+
}
|
|
846
|
+
function transformToRelationshipDefinition(result) {
|
|
847
|
+
const { definition, filePath } = result;
|
|
848
|
+
const config = definition.relationship;
|
|
849
|
+
const { fromColumn, toColumn } = deriveRelationshipFKColumns(config);
|
|
850
|
+
const table = deriveTableName(config);
|
|
851
|
+
const uniqueOn = deriveUniqueConstraint(config);
|
|
852
|
+
const types = resolveTypeDirections(config.types);
|
|
853
|
+
const fields = /* @__PURE__ */ new Map();
|
|
854
|
+
if (definition.fields) {
|
|
855
|
+
for (const [name, fieldDef] of Object.entries(definition.fields)) {
|
|
856
|
+
const field = {
|
|
857
|
+
name,
|
|
858
|
+
type: fieldDef.type,
|
|
859
|
+
required: fieldDef.required ?? false,
|
|
860
|
+
nullable: fieldDef.nullable ?? false,
|
|
861
|
+
unique: fieldDef.unique ?? false,
|
|
862
|
+
index: fieldDef.index ?? false,
|
|
863
|
+
foreignKey: fieldDef.foreign_key ? parseForeignKey(fieldDef.foreign_key) : void 0,
|
|
864
|
+
choices: fieldDef.choices,
|
|
865
|
+
constraints: {
|
|
866
|
+
minLength: fieldDef.min_length,
|
|
867
|
+
maxLength: fieldDef.max_length,
|
|
868
|
+
min: fieldDef.min,
|
|
869
|
+
max: fieldDef.max
|
|
870
|
+
},
|
|
871
|
+
ui: {
|
|
872
|
+
label: fieldDef.ui_label,
|
|
873
|
+
type: fieldDef.ui_type,
|
|
874
|
+
importance: fieldDef.ui_importance,
|
|
875
|
+
group: fieldDef.ui_group,
|
|
876
|
+
sortable: fieldDef.ui_sortable,
|
|
877
|
+
filterable: fieldDef.ui_filterable,
|
|
878
|
+
visible: fieldDef.ui_visible
|
|
879
|
+
}
|
|
880
|
+
};
|
|
881
|
+
fields.set(name, field);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
const queries = definition.queries?.map((q) => ({
|
|
885
|
+
by: q.by,
|
|
886
|
+
unique: q.unique,
|
|
887
|
+
select: q.select,
|
|
888
|
+
order: q.order,
|
|
889
|
+
limit: q.limit
|
|
890
|
+
}));
|
|
891
|
+
return {
|
|
892
|
+
name: config.name,
|
|
893
|
+
table,
|
|
894
|
+
from: config.from,
|
|
895
|
+
to: config.to,
|
|
896
|
+
selfReferential: config.from === config.to,
|
|
897
|
+
fromColumn,
|
|
898
|
+
toColumn,
|
|
899
|
+
types,
|
|
900
|
+
hasTypes: types.length > 0,
|
|
901
|
+
temporal: config.temporal,
|
|
902
|
+
sourced: config.sourced,
|
|
903
|
+
onDeleteFrom: config.on_delete_from ?? "restrict",
|
|
904
|
+
onDeleteTo: config.on_delete_to ?? "restrict",
|
|
905
|
+
uniqueOn,
|
|
906
|
+
fields,
|
|
907
|
+
queries,
|
|
908
|
+
sourcePath: filePath
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
function resolveTypeDirections(types) {
|
|
912
|
+
if (!types) return [];
|
|
913
|
+
if (Array.isArray(types)) {
|
|
914
|
+
return types.map((name) => ({
|
|
915
|
+
name,
|
|
916
|
+
bidirectional: false,
|
|
917
|
+
directed: true
|
|
918
|
+
}));
|
|
919
|
+
}
|
|
920
|
+
return Object.entries(types).map(([name, dir]) => {
|
|
921
|
+
const direction = dir;
|
|
922
|
+
return {
|
|
923
|
+
name,
|
|
924
|
+
inverse: direction.inverse,
|
|
925
|
+
bidirectional: direction.bidirectional ?? false,
|
|
926
|
+
directed: direction.directed ?? (!direction.bidirectional && !direction.inverse)
|
|
927
|
+
};
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
function loadRelationships(relationshipsDir) {
|
|
931
|
+
const relationships = [];
|
|
932
|
+
const issues = [];
|
|
933
|
+
const resolvedDir = resolve(relationshipsDir);
|
|
934
|
+
let files;
|
|
935
|
+
try {
|
|
936
|
+
files = readdirSync(resolvedDir).filter((f) => f.endsWith(".yaml") || f.endsWith(".yml")).map((f) => join(resolvedDir, f));
|
|
937
|
+
} catch {
|
|
938
|
+
return { relationships, issues };
|
|
939
|
+
}
|
|
940
|
+
if (files.length === 0) {
|
|
941
|
+
return { relationships, issues };
|
|
942
|
+
}
|
|
943
|
+
for (const filePath of files) {
|
|
944
|
+
const result = loadRelationshipFromYaml(filePath);
|
|
945
|
+
if (result.success) {
|
|
946
|
+
relationships.push(transformToRelationshipDefinition(result));
|
|
947
|
+
} else {
|
|
948
|
+
issues.push(...loadErrorToIssue(result));
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
return { relationships, issues };
|
|
952
|
+
}
|
|
953
|
+
function resolveRelationshipReferences(relationshipDefs, entities) {
|
|
954
|
+
const issues = [];
|
|
955
|
+
const entityNames = new Set(entities.map((e) => e.name));
|
|
956
|
+
for (const relDef of relationshipDefs) {
|
|
957
|
+
if (!entityNames.has(relDef.from)) {
|
|
958
|
+
issues.push({
|
|
959
|
+
severity: "warning",
|
|
960
|
+
type: "missing_relationship_endpoint",
|
|
961
|
+
entity: relDef.name,
|
|
962
|
+
message: `Relationship '${relDef.name}' references unknown 'from' entity '${relDef.from}'`,
|
|
963
|
+
path: relDef.sourcePath,
|
|
964
|
+
suggestion: `Define entity '${relDef.from}' or fix the 'from' value`
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
if (!entityNames.has(relDef.to)) {
|
|
968
|
+
issues.push({
|
|
969
|
+
severity: "warning",
|
|
970
|
+
type: "missing_relationship_endpoint",
|
|
971
|
+
entity: relDef.name,
|
|
972
|
+
message: `Relationship '${relDef.name}' references unknown 'to' entity '${relDef.to}'`,
|
|
973
|
+
path: relDef.sourcePath,
|
|
974
|
+
suggestion: `Define entity '${relDef.to}' or fix the 'to' value`
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
const dupes = relationshipDefs.filter((r) => r.name === relDef.name);
|
|
978
|
+
if (dupes.length > 1) {
|
|
979
|
+
issues.push({
|
|
980
|
+
severity: "error",
|
|
981
|
+
type: "duplicate_relationship",
|
|
982
|
+
entity: relDef.name,
|
|
983
|
+
message: `Duplicate relationship name: ${relDef.name}`,
|
|
984
|
+
path: relDef.sourcePath
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
return issues;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// src/analyzer/graph-builder.ts
|
|
992
|
+
function inferCardinality(type) {
|
|
993
|
+
switch (type) {
|
|
994
|
+
case "belongs_to":
|
|
995
|
+
return "N:1";
|
|
996
|
+
case "has_many":
|
|
997
|
+
return "1:N";
|
|
998
|
+
case "has_one":
|
|
999
|
+
return "1:1";
|
|
1000
|
+
default:
|
|
1001
|
+
return "1:N";
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
function hasReverseEdge(edges, from, to) {
|
|
1005
|
+
return edges.find((e) => e.from === to && e.to === from);
|
|
1006
|
+
}
|
|
1007
|
+
function buildDomainGraph(entities, relationshipDefinitions = []) {
|
|
1008
|
+
const entityMap = /* @__PURE__ */ new Map();
|
|
1009
|
+
const relDefMap = /* @__PURE__ */ new Map();
|
|
1010
|
+
const edges = [];
|
|
1011
|
+
for (const entity of entities) {
|
|
1012
|
+
entityMap.set(entity.name, entity);
|
|
1013
|
+
}
|
|
1014
|
+
for (const relDef of relationshipDefinitions) {
|
|
1015
|
+
relDefMap.set(relDef.name, relDef);
|
|
1016
|
+
}
|
|
1017
|
+
for (const entity of entities) {
|
|
1018
|
+
for (const [relName, rel] of entity.relationships) {
|
|
1019
|
+
if (!rel.resolved) continue;
|
|
1020
|
+
const reverseEdge = hasReverseEdge(edges, entity.name, rel.target);
|
|
1021
|
+
const edge = {
|
|
1022
|
+
from: entity.name,
|
|
1023
|
+
to: rel.target,
|
|
1024
|
+
relationship: rel,
|
|
1025
|
+
cardinality: inferCardinality(rel.type),
|
|
1026
|
+
bidirectional: reverseEdge !== void 0
|
|
1027
|
+
};
|
|
1028
|
+
if (reverseEdge) {
|
|
1029
|
+
reverseEdge.bidirectional = true;
|
|
1030
|
+
}
|
|
1031
|
+
edges.push(edge);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
for (const relDef of relationshipDefinitions) {
|
|
1035
|
+
const fromExists = entityMap.has(relDef.from);
|
|
1036
|
+
const toExists = entityMap.has(relDef.to);
|
|
1037
|
+
if (fromExists && toExists) {
|
|
1038
|
+
const edge = {
|
|
1039
|
+
from: relDef.from,
|
|
1040
|
+
to: relDef.to,
|
|
1041
|
+
relationship: {
|
|
1042
|
+
name: relDef.name,
|
|
1043
|
+
type: "has_many",
|
|
1044
|
+
target: relDef.to,
|
|
1045
|
+
foreignKey: relDef.fromColumn,
|
|
1046
|
+
resolved: true
|
|
1047
|
+
},
|
|
1048
|
+
cardinality: "N:M",
|
|
1049
|
+
bidirectional: relDef.types.some((t) => t.bidirectional)
|
|
1050
|
+
};
|
|
1051
|
+
edges.push(edge);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
return { entities: entityMap, relationshipDefinitions: relDefMap, edges };
|
|
1055
|
+
}
|
|
1056
|
+
function getRelatedEntities(graph, entityName, depth = 1) {
|
|
1057
|
+
const related = /* @__PURE__ */ new Set();
|
|
1058
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1059
|
+
const queue = [
|
|
1060
|
+
{ name: entityName, currentDepth: 0 }
|
|
1061
|
+
];
|
|
1062
|
+
while (queue.length > 0) {
|
|
1063
|
+
const item = queue.shift();
|
|
1064
|
+
if (!item) continue;
|
|
1065
|
+
const { name, currentDepth } = item;
|
|
1066
|
+
if (visited.has(name) || currentDepth > depth) continue;
|
|
1067
|
+
visited.add(name);
|
|
1068
|
+
for (const edge of graph.edges) {
|
|
1069
|
+
if (edge.from === name && !visited.has(edge.to)) {
|
|
1070
|
+
related.add(edge.to);
|
|
1071
|
+
queue.push({ name: edge.to, currentDepth: currentDepth + 1 });
|
|
1072
|
+
}
|
|
1073
|
+
if (edge.to === name && !visited.has(edge.from)) {
|
|
1074
|
+
related.add(edge.from);
|
|
1075
|
+
queue.push({ name: edge.from, currentDepth: currentDepth + 1 });
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
return related;
|
|
1080
|
+
}
|
|
1081
|
+
function findOrphanEntities(graph) {
|
|
1082
|
+
const orphans = [];
|
|
1083
|
+
for (const [name] of graph.entities) {
|
|
1084
|
+
const hasRelationship = graph.edges.some((e) => e.from === name || e.to === name);
|
|
1085
|
+
if (!hasRelationship) {
|
|
1086
|
+
orphans.push(name);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
return orphans;
|
|
1090
|
+
}
|
|
1091
|
+
function findCircularDependencies(graph) {
|
|
1092
|
+
const cycles = [];
|
|
1093
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1094
|
+
const recursionStack = /* @__PURE__ */ new Set();
|
|
1095
|
+
function dfs(node, path) {
|
|
1096
|
+
visited.add(node);
|
|
1097
|
+
recursionStack.add(node);
|
|
1098
|
+
const outgoingEdges = graph.edges.filter((e) => e.from === node);
|
|
1099
|
+
for (const edge of outgoingEdges) {
|
|
1100
|
+
if (!visited.has(edge.to)) {
|
|
1101
|
+
dfs(edge.to, [...path, edge.to]);
|
|
1102
|
+
} else if (recursionStack.has(edge.to)) {
|
|
1103
|
+
const cycleStart = path.indexOf(edge.to);
|
|
1104
|
+
if (cycleStart !== -1) {
|
|
1105
|
+
cycles.push([...path.slice(cycleStart), edge.to]);
|
|
1106
|
+
} else {
|
|
1107
|
+
cycles.push([...path, edge.to]);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
recursionStack.delete(node);
|
|
1112
|
+
}
|
|
1113
|
+
for (const [name] of graph.entities) {
|
|
1114
|
+
if (!visited.has(name)) {
|
|
1115
|
+
dfs(name, [name]);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
const uniqueCycles = [];
|
|
1119
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1120
|
+
for (const cycle of cycles) {
|
|
1121
|
+
const minIndex = cycle.indexOf(
|
|
1122
|
+
cycle.reduce((min, val) => val < min ? val : min, cycle[0])
|
|
1123
|
+
);
|
|
1124
|
+
const normalized = [...cycle.slice(minIndex), ...cycle.slice(0, minIndex)];
|
|
1125
|
+
const key = normalized.join("->");
|
|
1126
|
+
if (!seen.has(key)) {
|
|
1127
|
+
seen.add(key);
|
|
1128
|
+
uniqueCycles.push(cycle);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
return uniqueCycles;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// src/behaviors/external-id-tracking.ts
|
|
1135
|
+
var externalIdTrackingBehavior = {
|
|
1136
|
+
name: "external_id_tracking",
|
|
1137
|
+
description: "Adds external_id, provider, and provider_metadata fields for external system sync tracking",
|
|
1138
|
+
fields: [
|
|
1139
|
+
{
|
|
1140
|
+
name: "external_id",
|
|
1141
|
+
camelName: "externalId",
|
|
1142
|
+
type: "string",
|
|
1143
|
+
tsType: "string | null",
|
|
1144
|
+
drizzleType: "varchar",
|
|
1145
|
+
drizzleImports: ["varchar", "index"],
|
|
1146
|
+
zodType: "z.string().nullable()",
|
|
1147
|
+
nullable: true,
|
|
1148
|
+
ui: {
|
|
1149
|
+
label: "External ID",
|
|
1150
|
+
type: "text",
|
|
1151
|
+
importance: "tertiary",
|
|
1152
|
+
group: "metadata",
|
|
1153
|
+
visible: false
|
|
1154
|
+
}
|
|
1155
|
+
},
|
|
1156
|
+
{
|
|
1157
|
+
name: "provider",
|
|
1158
|
+
camelName: "provider",
|
|
1159
|
+
type: "string",
|
|
1160
|
+
tsType: "string | null",
|
|
1161
|
+
drizzleType: "varchar",
|
|
1162
|
+
drizzleImports: ["varchar"],
|
|
1163
|
+
zodType: "z.string().nullable()",
|
|
1164
|
+
nullable: true,
|
|
1165
|
+
ui: {
|
|
1166
|
+
label: "Provider",
|
|
1167
|
+
type: "text",
|
|
1168
|
+
importance: "tertiary",
|
|
1169
|
+
group: "metadata",
|
|
1170
|
+
visible: false
|
|
1171
|
+
}
|
|
1172
|
+
},
|
|
1173
|
+
{
|
|
1174
|
+
name: "provider_metadata",
|
|
1175
|
+
camelName: "providerMetadata",
|
|
1176
|
+
type: "json",
|
|
1177
|
+
tsType: "unknown | null",
|
|
1178
|
+
drizzleType: "jsonb",
|
|
1179
|
+
drizzleImports: ["jsonb"],
|
|
1180
|
+
zodType: "z.unknown().nullable()",
|
|
1181
|
+
nullable: true,
|
|
1182
|
+
ui: {
|
|
1183
|
+
label: "Provider Metadata",
|
|
1184
|
+
type: "json",
|
|
1185
|
+
importance: "tertiary",
|
|
1186
|
+
group: "metadata",
|
|
1187
|
+
visible: false
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
],
|
|
1191
|
+
drizzleImports: ["varchar", "jsonb", "index"],
|
|
1192
|
+
configKey: "externalIdTracking"
|
|
1193
|
+
};
|
|
1194
|
+
|
|
1195
|
+
// src/behaviors/soft-delete.ts
|
|
1196
|
+
var softDeleteBehavior = {
|
|
1197
|
+
name: "soft_delete",
|
|
1198
|
+
description: "Adds deleted_at field for soft delete functionality",
|
|
1199
|
+
fields: [
|
|
1200
|
+
{
|
|
1201
|
+
name: "deleted_at",
|
|
1202
|
+
camelName: "deletedAt",
|
|
1203
|
+
type: "datetime",
|
|
1204
|
+
tsType: "Date | null",
|
|
1205
|
+
drizzleType: "timestamp",
|
|
1206
|
+
drizzleImports: ["timestamp"],
|
|
1207
|
+
zodType: "z.coerce.date().nullable()",
|
|
1208
|
+
nullable: true,
|
|
1209
|
+
ui: {
|
|
1210
|
+
label: "Deleted At",
|
|
1211
|
+
type: "datetime",
|
|
1212
|
+
importance: "tertiary",
|
|
1213
|
+
group: "metadata",
|
|
1214
|
+
visible: false
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
],
|
|
1218
|
+
drizzleImports: ["timestamp"],
|
|
1219
|
+
methods: [
|
|
1220
|
+
"softDelete",
|
|
1221
|
+
"restore",
|
|
1222
|
+
"findWithDeleted",
|
|
1223
|
+
"findOnlyDeleted",
|
|
1224
|
+
"baseQuery"
|
|
1225
|
+
// Modified to filter deleted records
|
|
1226
|
+
],
|
|
1227
|
+
configKey: "softDelete"
|
|
1228
|
+
};
|
|
1229
|
+
|
|
1230
|
+
// src/behaviors/timestamps.ts
|
|
1231
|
+
var timestampsBehavior = {
|
|
1232
|
+
name: "timestamps",
|
|
1233
|
+
description: "Adds created_at and updated_at timestamp fields",
|
|
1234
|
+
fields: [
|
|
1235
|
+
{
|
|
1236
|
+
name: "created_at",
|
|
1237
|
+
camelName: "createdAt",
|
|
1238
|
+
type: "datetime",
|
|
1239
|
+
tsType: "Date",
|
|
1240
|
+
drizzleType: "timestamp",
|
|
1241
|
+
drizzleImports: ["timestamp"],
|
|
1242
|
+
zodType: "z.coerce.date()",
|
|
1243
|
+
nullable: false,
|
|
1244
|
+
default: "now()",
|
|
1245
|
+
ui: {
|
|
1246
|
+
label: "Created At",
|
|
1247
|
+
type: "datetime",
|
|
1248
|
+
importance: "tertiary",
|
|
1249
|
+
group: "metadata",
|
|
1250
|
+
visible: false
|
|
1251
|
+
}
|
|
1252
|
+
},
|
|
1253
|
+
{
|
|
1254
|
+
name: "updated_at",
|
|
1255
|
+
camelName: "updatedAt",
|
|
1256
|
+
type: "datetime",
|
|
1257
|
+
tsType: "Date",
|
|
1258
|
+
drizzleType: "timestamp",
|
|
1259
|
+
drizzleImports: ["timestamp"],
|
|
1260
|
+
zodType: "z.coerce.date()",
|
|
1261
|
+
nullable: false,
|
|
1262
|
+
default: "now()",
|
|
1263
|
+
ui: {
|
|
1264
|
+
label: "Updated At",
|
|
1265
|
+
type: "datetime",
|
|
1266
|
+
importance: "tertiary",
|
|
1267
|
+
group: "metadata",
|
|
1268
|
+
visible: false
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
],
|
|
1272
|
+
drizzleImports: ["timestamp"],
|
|
1273
|
+
methods: ["applyTimestampsOnCreate", "applyTimestampsOnUpdate"],
|
|
1274
|
+
configKey: "timestamps"
|
|
1275
|
+
};
|
|
1276
|
+
|
|
1277
|
+
// src/behaviors/user-tracking.ts
|
|
1278
|
+
var userTrackingBehavior = {
|
|
1279
|
+
name: "user_tracking",
|
|
1280
|
+
description: "Adds created_by and updated_by user reference fields",
|
|
1281
|
+
fields: [
|
|
1282
|
+
{
|
|
1283
|
+
name: "created_by",
|
|
1284
|
+
camelName: "createdBy",
|
|
1285
|
+
type: "uuid",
|
|
1286
|
+
tsType: "string | null",
|
|
1287
|
+
drizzleType: "uuid",
|
|
1288
|
+
drizzleImports: ["uuid"],
|
|
1289
|
+
zodType: "z.string().uuid().nullable()",
|
|
1290
|
+
nullable: true,
|
|
1291
|
+
foreignKey: "users.id",
|
|
1292
|
+
ui: {
|
|
1293
|
+
label: "Created By",
|
|
1294
|
+
type: "reference",
|
|
1295
|
+
importance: "tertiary",
|
|
1296
|
+
group: "metadata",
|
|
1297
|
+
visible: false
|
|
1298
|
+
}
|
|
1299
|
+
},
|
|
1300
|
+
{
|
|
1301
|
+
name: "updated_by",
|
|
1302
|
+
camelName: "updatedBy",
|
|
1303
|
+
type: "uuid",
|
|
1304
|
+
tsType: "string | null",
|
|
1305
|
+
drizzleType: "uuid",
|
|
1306
|
+
drizzleImports: ["uuid"],
|
|
1307
|
+
zodType: "z.string().uuid().nullable()",
|
|
1308
|
+
nullable: true,
|
|
1309
|
+
foreignKey: "users.id",
|
|
1310
|
+
ui: {
|
|
1311
|
+
label: "Updated By",
|
|
1312
|
+
type: "reference",
|
|
1313
|
+
importance: "tertiary",
|
|
1314
|
+
group: "metadata",
|
|
1315
|
+
visible: false
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
],
|
|
1319
|
+
drizzleImports: ["uuid"],
|
|
1320
|
+
methods: ["applyUserTrackingOnCreate", "applyUserTrackingOnUpdate"],
|
|
1321
|
+
configKey: "userTracking"
|
|
1322
|
+
};
|
|
1323
|
+
|
|
1324
|
+
// src/behaviors/index.ts
|
|
1325
|
+
var behaviorRegistry = /* @__PURE__ */ new Map([
|
|
1326
|
+
["timestamps", timestampsBehavior],
|
|
1327
|
+
["soft_delete", softDeleteBehavior],
|
|
1328
|
+
["user_tracking", userTrackingBehavior],
|
|
1329
|
+
["external_id_tracking", externalIdTrackingBehavior]
|
|
1330
|
+
]);
|
|
1331
|
+
function getBehavior(name) {
|
|
1332
|
+
return behaviorRegistry.get(name);
|
|
1333
|
+
}
|
|
1334
|
+
function normalizeBehaviorConfig(config) {
|
|
1335
|
+
if (typeof config === "string") {
|
|
1336
|
+
return { name: config, options: {} };
|
|
1337
|
+
}
|
|
1338
|
+
return { name: config.name, options: config.options ?? {} };
|
|
1339
|
+
}
|
|
1340
|
+
function normalizeBehaviorConfigs(configs) {
|
|
1341
|
+
return configs.map(normalizeBehaviorConfig);
|
|
1342
|
+
}
|
|
1343
|
+
function resolveBehaviorFields(configs) {
|
|
1344
|
+
const normalized = normalizeBehaviorConfigs(configs);
|
|
1345
|
+
const fields = [];
|
|
1346
|
+
const addedFieldNames = /* @__PURE__ */ new Set();
|
|
1347
|
+
for (const config of normalized) {
|
|
1348
|
+
const behavior = getBehavior(config.name);
|
|
1349
|
+
if (!behavior) continue;
|
|
1350
|
+
for (const field of behavior.fields) {
|
|
1351
|
+
if (!addedFieldNames.has(field.name)) {
|
|
1352
|
+
fields.push(field);
|
|
1353
|
+
addedFieldNames.add(field.name);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
return fields;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
// src/analyzer/consistency-checker.ts
|
|
1361
|
+
function checkConsistency(graph) {
|
|
1362
|
+
const issues = [];
|
|
1363
|
+
for (const [name, entity] of graph.entities) {
|
|
1364
|
+
issues.push(...checkEntityConsistency(entity));
|
|
1365
|
+
issues.push(...checkRelationshipConsistency(entity, graph));
|
|
1366
|
+
issues.push(...checkNamingConventions(entity));
|
|
1367
|
+
issues.push(...checkMissingIndexes(entity));
|
|
1368
|
+
issues.push(...checkUiMetadata(entity));
|
|
1369
|
+
if (entity.queries !== void 0) {
|
|
1370
|
+
issues.push(...checkQueryFieldReferences(entity));
|
|
1371
|
+
}
|
|
1372
|
+
if (entity.sync !== void 0) {
|
|
1373
|
+
issues.push(...checkSyncFieldMappingReferences(entity));
|
|
1374
|
+
issues.push(...checkExternalIdTrackingCollision(entity));
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
issues.push(...checkOrphanEntities(graph));
|
|
1378
|
+
issues.push(...checkCircularReferences(graph));
|
|
1379
|
+
issues.push(...checkMissingInverses(graph));
|
|
1380
|
+
return issues;
|
|
1381
|
+
}
|
|
1382
|
+
function checkEntityConsistency(entity) {
|
|
1383
|
+
const issues = [];
|
|
1384
|
+
if (!entity.fields.has("id")) {
|
|
1385
|
+
issues.push({
|
|
1386
|
+
severity: "info",
|
|
1387
|
+
type: "missing_id",
|
|
1388
|
+
entity: entity.name,
|
|
1389
|
+
message: 'Entity missing standard "id" field',
|
|
1390
|
+
suggestion: 'Add an "id" field with type "uuid"'
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
const hasCreatedAt = entity.fields.has("created_at");
|
|
1394
|
+
const hasTimestampsBehavior = entity.behaviors.includes("timestamps");
|
|
1395
|
+
if (!hasCreatedAt && !hasTimestampsBehavior) {
|
|
1396
|
+
issues.push({
|
|
1397
|
+
severity: "info",
|
|
1398
|
+
type: "missing_timestamps",
|
|
1399
|
+
entity: entity.name,
|
|
1400
|
+
message: 'Entity missing "created_at" field and "timestamps" behavior',
|
|
1401
|
+
suggestion: 'Add "timestamps" to behaviors or add created_at/updated_at fields'
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
return issues;
|
|
1405
|
+
}
|
|
1406
|
+
function checkRelationshipConsistency(entity, graph) {
|
|
1407
|
+
const issues = [];
|
|
1408
|
+
for (const [relName, rel] of entity.relationships) {
|
|
1409
|
+
if (rel.type === "belongs_to") {
|
|
1410
|
+
const fkField = entity.fields.get(rel.foreignKey);
|
|
1411
|
+
if (!fkField) {
|
|
1412
|
+
issues.push({
|
|
1413
|
+
severity: "warning",
|
|
1414
|
+
type: "missing_fk_field",
|
|
1415
|
+
entity: entity.name,
|
|
1416
|
+
field: relName,
|
|
1417
|
+
message: `Relationship "${relName}" references foreign key "${rel.foreignKey}" but field doesn't exist`,
|
|
1418
|
+
suggestion: `Add field "${rel.foreignKey}" with foreign_key reference`
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
if (rel.type === "has_many" || rel.type === "has_one") {
|
|
1423
|
+
const targetEntity = graph.entities.get(rel.target);
|
|
1424
|
+
if (targetEntity) {
|
|
1425
|
+
const targetFkField = targetEntity.fields.get(rel.foreignKey);
|
|
1426
|
+
if (!targetFkField) {
|
|
1427
|
+
issues.push({
|
|
1428
|
+
severity: "warning",
|
|
1429
|
+
type: "missing_target_fk",
|
|
1430
|
+
entity: entity.name,
|
|
1431
|
+
field: relName,
|
|
1432
|
+
message: `Relationship "${relName}" expects foreign key "${rel.foreignKey}" on "${rel.target}" but field doesn't exist`,
|
|
1433
|
+
suggestion: `Add field "${rel.foreignKey}" to "${rel.target}" entity`
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
return issues;
|
|
1440
|
+
}
|
|
1441
|
+
function checkNamingConventions(entity) {
|
|
1442
|
+
const issues = [];
|
|
1443
|
+
if (entity.name !== entity.name.toLowerCase()) {
|
|
1444
|
+
issues.push({
|
|
1445
|
+
severity: "warning",
|
|
1446
|
+
type: "naming_convention",
|
|
1447
|
+
entity: entity.name,
|
|
1448
|
+
message: "Entity name should be lowercase",
|
|
1449
|
+
suggestion: `Use "${entity.name.toLowerCase()}"`
|
|
1450
|
+
});
|
|
1451
|
+
}
|
|
1452
|
+
for (const [fieldName] of entity.fields) {
|
|
1453
|
+
if (fieldName !== fieldName.toLowerCase()) {
|
|
1454
|
+
issues.push({
|
|
1455
|
+
severity: "warning",
|
|
1456
|
+
type: "naming_convention",
|
|
1457
|
+
entity: entity.name,
|
|
1458
|
+
field: fieldName,
|
|
1459
|
+
message: "Field name should be snake_case",
|
|
1460
|
+
suggestion: `Use "${toSnakeCase(fieldName)}"`
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
for (const [relName] of entity.relationships) {
|
|
1465
|
+
if (relName !== relName.toLowerCase()) {
|
|
1466
|
+
issues.push({
|
|
1467
|
+
severity: "warning",
|
|
1468
|
+
type: "naming_convention",
|
|
1469
|
+
entity: entity.name,
|
|
1470
|
+
field: relName,
|
|
1471
|
+
message: "Relationship name should be snake_case",
|
|
1472
|
+
suggestion: `Use "${toSnakeCase(relName)}"`
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
return issues;
|
|
1477
|
+
}
|
|
1478
|
+
function checkMissingIndexes(entity) {
|
|
1479
|
+
const issues = [];
|
|
1480
|
+
for (const [fieldName, field] of entity.fields) {
|
|
1481
|
+
if (field.ui.filterable && !field.index && !field.unique) {
|
|
1482
|
+
issues.push({
|
|
1483
|
+
severity: "warning",
|
|
1484
|
+
type: "missing_index",
|
|
1485
|
+
entity: entity.name,
|
|
1486
|
+
field: fieldName,
|
|
1487
|
+
message: `Field "${fieldName}" is filterable but has no index`,
|
|
1488
|
+
suggestion: 'Add "index: true" to improve query performance'
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
if (field.foreignKey && !field.index && !field.unique) {
|
|
1492
|
+
issues.push({
|
|
1493
|
+
severity: "info",
|
|
1494
|
+
type: "missing_fk_index",
|
|
1495
|
+
entity: entity.name,
|
|
1496
|
+
field: fieldName,
|
|
1497
|
+
message: `Foreign key field "${fieldName}" has no index`,
|
|
1498
|
+
suggestion: 'Add "index: true" for better join performance'
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
return issues;
|
|
1503
|
+
}
|
|
1504
|
+
function checkUiMetadata(entity) {
|
|
1505
|
+
const issues = [];
|
|
1506
|
+
const systemFields = /* @__PURE__ */ new Set(["id", "created_at", "updated_at", "deleted_at", "tenant_id"]);
|
|
1507
|
+
for (const [fieldName, field] of entity.fields) {
|
|
1508
|
+
if (systemFields.has(fieldName)) continue;
|
|
1509
|
+
const hasAnyUiMeta = field.ui.label !== void 0 || field.ui.type !== void 0 || field.ui.group !== void 0;
|
|
1510
|
+
if (!hasAnyUiMeta) {
|
|
1511
|
+
issues.push({
|
|
1512
|
+
severity: "info",
|
|
1513
|
+
type: "missing_ui_metadata",
|
|
1514
|
+
entity: entity.name,
|
|
1515
|
+
field: fieldName,
|
|
1516
|
+
message: `Field "${fieldName}" has no UI metadata`,
|
|
1517
|
+
suggestion: "Add ui_label, ui_type, ui_group for better admin panel display"
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
return issues;
|
|
1522
|
+
}
|
|
1523
|
+
function checkOrphanEntities(graph) {
|
|
1524
|
+
const orphans = findOrphanEntities(graph);
|
|
1525
|
+
return orphans.map((name) => ({
|
|
1526
|
+
severity: "info",
|
|
1527
|
+
type: "orphan_entity",
|
|
1528
|
+
entity: name,
|
|
1529
|
+
message: `Entity "${name}" has no relationships to other entities`,
|
|
1530
|
+
suggestion: "Consider if this entity should be related to others"
|
|
1531
|
+
}));
|
|
1532
|
+
}
|
|
1533
|
+
function checkCircularReferences(graph) {
|
|
1534
|
+
const cycles = findCircularDependencies(graph);
|
|
1535
|
+
return cycles.map((cycle) => ({
|
|
1536
|
+
severity: "info",
|
|
1537
|
+
type: "circular_dependency",
|
|
1538
|
+
entity: cycle[0],
|
|
1539
|
+
message: `Circular reference detected: ${cycle.join(" -> ")}`,
|
|
1540
|
+
suggestion: "Verify this is intentional (e.g., self-referential hierarchy)"
|
|
1541
|
+
}));
|
|
1542
|
+
}
|
|
1543
|
+
function checkMissingInverses(graph) {
|
|
1544
|
+
const issues = [];
|
|
1545
|
+
for (const edge of graph.edges) {
|
|
1546
|
+
const { from, to, relationship } = edge;
|
|
1547
|
+
const targetEntity = graph.entities.get(to);
|
|
1548
|
+
if (!targetEntity) continue;
|
|
1549
|
+
const hasInverse = Array.from(targetEntity.relationships.values()).some(
|
|
1550
|
+
(rel) => rel.target === from
|
|
1551
|
+
);
|
|
1552
|
+
if (!hasInverse && relationship.type !== "belongs_to") {
|
|
1553
|
+
issues.push({
|
|
1554
|
+
severity: "info",
|
|
1555
|
+
type: "missing_inverse",
|
|
1556
|
+
entity: from,
|
|
1557
|
+
field: relationship.name,
|
|
1558
|
+
message: `Relationship "${relationship.name}" to "${to}" has no inverse defined on target`,
|
|
1559
|
+
suggestion: `Add inverse relationship on "${to}" pointing back to "${from}"`
|
|
1560
|
+
});
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
return issues;
|
|
1564
|
+
}
|
|
1565
|
+
function getAvailableFieldNames(entity) {
|
|
1566
|
+
const entityFieldNames = Array.from(entity.fields.keys());
|
|
1567
|
+
const behaviorFields = resolveBehaviorFields(entity.behaviors);
|
|
1568
|
+
const behaviorFieldNames = behaviorFields.map((f) => f.name);
|
|
1569
|
+
const belongsToFkNames = [];
|
|
1570
|
+
for (const rel of entity.relationships.values()) {
|
|
1571
|
+
if (rel.type === "belongs_to" && rel.foreignKey) {
|
|
1572
|
+
belongsToFkNames.push(rel.foreignKey);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
return [.../* @__PURE__ */ new Set([...entityFieldNames, ...behaviorFieldNames, ...belongsToFkNames])];
|
|
1576
|
+
}
|
|
1577
|
+
function checkQueryFieldReferences(entity) {
|
|
1578
|
+
const issues = [];
|
|
1579
|
+
const availableFields = getAvailableFieldNames(entity);
|
|
1580
|
+
const availableSet = new Set(availableFields);
|
|
1581
|
+
for (const query of entity.queries ?? []) {
|
|
1582
|
+
if (!query.via) {
|
|
1583
|
+
for (const fieldName of query.by) {
|
|
1584
|
+
if (!availableSet.has(fieldName)) {
|
|
1585
|
+
issues.push({
|
|
1586
|
+
severity: "error",
|
|
1587
|
+
type: "unknown_query_field",
|
|
1588
|
+
entity: entity.name,
|
|
1589
|
+
field: fieldName,
|
|
1590
|
+
message: `Query references unknown field "${fieldName}" in entity "${entity.name}". Available fields: ${availableFields.join(", ")}`
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
for (const fieldName of query.select ?? []) {
|
|
1596
|
+
if (!availableSet.has(fieldName)) {
|
|
1597
|
+
issues.push({
|
|
1598
|
+
severity: "error",
|
|
1599
|
+
type: "unknown_query_field",
|
|
1600
|
+
entity: entity.name,
|
|
1601
|
+
field: fieldName,
|
|
1602
|
+
message: `Query references unknown field "${fieldName}" in entity "${entity.name}". Available fields: ${availableFields.join(", ")}`
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
return issues;
|
|
1608
|
+
}
|
|
1609
|
+
function checkSyncFieldMappingReferences(entity) {
|
|
1610
|
+
const issues = [];
|
|
1611
|
+
const availableFields = getAvailableFieldNames(entity);
|
|
1612
|
+
const availableSet = new Set(availableFields);
|
|
1613
|
+
for (const [providerName, provider] of Object.entries(entity.sync?.providers ?? {})) {
|
|
1614
|
+
for (const fieldName of Object.keys(provider.fieldMapping ?? {})) {
|
|
1615
|
+
if (!availableSet.has(fieldName)) {
|
|
1616
|
+
issues.push({
|
|
1617
|
+
severity: "warning",
|
|
1618
|
+
type: "unknown_sync_field_mapping",
|
|
1619
|
+
entity: entity.name,
|
|
1620
|
+
field: fieldName,
|
|
1621
|
+
message: `Sync field mapping references unknown field "${fieldName}" for provider "${providerName}" in entity "${entity.name}"`
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
for (const fieldName of provider.readOnlyFields ?? []) {
|
|
1626
|
+
if (!availableSet.has(fieldName)) {
|
|
1627
|
+
issues.push({
|
|
1628
|
+
severity: "warning",
|
|
1629
|
+
type: "unknown_sync_field_mapping",
|
|
1630
|
+
entity: entity.name,
|
|
1631
|
+
field: fieldName,
|
|
1632
|
+
message: `Sync field mapping references unknown field "${fieldName}" for provider "${providerName}" in entity "${entity.name}"`
|
|
1633
|
+
});
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
return issues;
|
|
1638
|
+
}
|
|
1639
|
+
function checkExternalIdTrackingCollision(entity) {
|
|
1640
|
+
const issues = [];
|
|
1641
|
+
const hasExternalIdTracking = entity.behaviors.includes("external_id_tracking");
|
|
1642
|
+
if (!hasExternalIdTracking) return issues;
|
|
1643
|
+
for (const [providerName, provider] of Object.entries(entity.sync?.providers ?? {})) {
|
|
1644
|
+
if (provider.fieldMapping && "external_id" in provider.fieldMapping) {
|
|
1645
|
+
issues.push({
|
|
1646
|
+
severity: "warning",
|
|
1647
|
+
type: "external_id_tracking_collision",
|
|
1648
|
+
entity: entity.name,
|
|
1649
|
+
field: "external_id",
|
|
1650
|
+
message: `Entity "${entity.name}" has external_id_tracking behavior and also maps "external_id" in sync field_mapping for provider "${providerName}". The behavior-added field may collide with the mapped field.`
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
return issues;
|
|
1655
|
+
}
|
|
1656
|
+
function toSnakeCase(str) {
|
|
1657
|
+
return str.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
// src/analyzer/statistics.ts
|
|
1661
|
+
function computeStatistics(graph) {
|
|
1662
|
+
const entities = Array.from(graph.entities.values());
|
|
1663
|
+
const fieldsByType = {};
|
|
1664
|
+
const relationshipsByType = {};
|
|
1665
|
+
let totalFields = 0;
|
|
1666
|
+
let totalRelationships = 0;
|
|
1667
|
+
let entitiesWithBehaviors = 0;
|
|
1668
|
+
for (const entity of entities) {
|
|
1669
|
+
totalFields += entity.fields.size;
|
|
1670
|
+
totalRelationships += entity.relationships.size;
|
|
1671
|
+
if (entity.behaviors.length > 0) {
|
|
1672
|
+
entitiesWithBehaviors++;
|
|
1673
|
+
}
|
|
1674
|
+
for (const field of entity.fields.values()) {
|
|
1675
|
+
fieldsByType[field.type] = (fieldsByType[field.type] ?? 0) + 1;
|
|
1676
|
+
}
|
|
1677
|
+
for (const rel of entity.relationships.values()) {
|
|
1678
|
+
relationshipsByType[rel.type] = (relationshipsByType[rel.type] ?? 0) + 1;
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
return {
|
|
1682
|
+
totalEntities: entities.length,
|
|
1683
|
+
totalFields,
|
|
1684
|
+
totalRelationships,
|
|
1685
|
+
fieldsByType,
|
|
1686
|
+
relationshipsByType,
|
|
1687
|
+
entitiesWithBehaviors,
|
|
1688
|
+
averageFieldsPerEntity: entities.length > 0 ? totalFields / entities.length : 0
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
// src/formatters/console-formatter.ts
|
|
1693
|
+
var colors = {
|
|
1694
|
+
reset: "\x1B[0m",
|
|
1695
|
+
bold: "\x1B[1m",
|
|
1696
|
+
dim: "\x1B[2m",
|
|
1697
|
+
red: "\x1B[31m",
|
|
1698
|
+
green: "\x1B[32m",
|
|
1699
|
+
yellow: "\x1B[33m",
|
|
1700
|
+
blue: "\x1B[34m",
|
|
1701
|
+
magenta: "\x1B[35m",
|
|
1702
|
+
cyan: "\x1B[36m",
|
|
1703
|
+
white: "\x1B[37m",
|
|
1704
|
+
bgRed: "\x1B[41m",
|
|
1705
|
+
bgGreen: "\x1B[42m",
|
|
1706
|
+
bgYellow: "\x1B[43m"
|
|
1707
|
+
};
|
|
1708
|
+
function color(text, ...styles) {
|
|
1709
|
+
return `${styles.join("")}${text}${colors.reset}`;
|
|
1710
|
+
}
|
|
1711
|
+
function severityColor(severity) {
|
|
1712
|
+
switch (severity) {
|
|
1713
|
+
case "error":
|
|
1714
|
+
return colors.red;
|
|
1715
|
+
case "warning":
|
|
1716
|
+
return colors.yellow;
|
|
1717
|
+
case "info":
|
|
1718
|
+
return colors.cyan;
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
function severityIcon(severity) {
|
|
1722
|
+
switch (severity) {
|
|
1723
|
+
case "error":
|
|
1724
|
+
return "X";
|
|
1725
|
+
case "warning":
|
|
1726
|
+
return "!";
|
|
1727
|
+
case "info":
|
|
1728
|
+
return "i";
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
function formatConsole(result) {
|
|
1732
|
+
const lines = [];
|
|
1733
|
+
lines.push("");
|
|
1734
|
+
lines.push(color("=".repeat(60), colors.dim));
|
|
1735
|
+
lines.push(color(" Domain Analysis Report", colors.bold, colors.cyan));
|
|
1736
|
+
lines.push(color("=".repeat(60), colors.dim));
|
|
1737
|
+
lines.push("");
|
|
1738
|
+
lines.push(...formatStatistics(result));
|
|
1739
|
+
lines.push(...formatEntities(result));
|
|
1740
|
+
lines.push(...formatRelationships(result));
|
|
1741
|
+
if (result.issues.length > 0) {
|
|
1742
|
+
lines.push(...formatIssues(result.issues));
|
|
1743
|
+
}
|
|
1744
|
+
lines.push("");
|
|
1745
|
+
lines.push(color("-".repeat(60), colors.dim));
|
|
1746
|
+
const errors = result.issues.filter((i) => i.severity === "error");
|
|
1747
|
+
const warnings = result.issues.filter((i) => i.severity === "warning");
|
|
1748
|
+
const infos = result.issues.filter((i) => i.severity === "info");
|
|
1749
|
+
if (result.isValid) {
|
|
1750
|
+
lines.push(
|
|
1751
|
+
color(
|
|
1752
|
+
`[OK] Domain is valid (${warnings.length} warnings, ${infos.length} info)`,
|
|
1753
|
+
colors.green
|
|
1754
|
+
)
|
|
1755
|
+
);
|
|
1756
|
+
} else {
|
|
1757
|
+
lines.push(color(`[FAIL] Domain has ${errors.length} errors`, colors.red));
|
|
1758
|
+
}
|
|
1759
|
+
lines.push(color("-".repeat(60), colors.dim));
|
|
1760
|
+
lines.push("");
|
|
1761
|
+
return lines.join("\n");
|
|
1762
|
+
}
|
|
1763
|
+
function formatStatistics(result) {
|
|
1764
|
+
const lines = [];
|
|
1765
|
+
const stats = result.statistics;
|
|
1766
|
+
lines.push(color("Statistics:", colors.bold));
|
|
1767
|
+
lines.push("");
|
|
1768
|
+
lines.push(` Entities: ${stats.totalEntities}`);
|
|
1769
|
+
lines.push(
|
|
1770
|
+
` Fields: ${stats.totalFields} (avg ${stats.averageFieldsPerEntity.toFixed(1)}/entity)`
|
|
1771
|
+
);
|
|
1772
|
+
lines.push(` Relationships: ${stats.totalRelationships}`);
|
|
1773
|
+
lines.push(` With behaviors: ${stats.entitiesWithBehaviors}`);
|
|
1774
|
+
lines.push("");
|
|
1775
|
+
lines.push(" Field types:");
|
|
1776
|
+
const sortedTypes = Object.entries(stats.fieldsByType).sort((a, b) => b[1] - a[1]);
|
|
1777
|
+
for (const [type, count] of sortedTypes) {
|
|
1778
|
+
const bar = color("|".repeat(Math.min(count, 20)), colors.blue);
|
|
1779
|
+
lines.push(` ${type.padEnd(12)} ${bar} ${count}`);
|
|
1780
|
+
}
|
|
1781
|
+
lines.push("");
|
|
1782
|
+
if (stats.totalRelationships > 0) {
|
|
1783
|
+
lines.push(" Relationship types:");
|
|
1784
|
+
const sortedRels = Object.entries(stats.relationshipsByType).sort((a, b) => b[1] - a[1]);
|
|
1785
|
+
for (const [type, count] of sortedRels) {
|
|
1786
|
+
const bar = color("|".repeat(Math.min(count, 20)), colors.magenta);
|
|
1787
|
+
lines.push(` ${type.padEnd(12)} ${bar} ${count}`);
|
|
1788
|
+
}
|
|
1789
|
+
lines.push("");
|
|
1790
|
+
}
|
|
1791
|
+
return lines;
|
|
1792
|
+
}
|
|
1793
|
+
function formatEntities(result) {
|
|
1794
|
+
const lines = [];
|
|
1795
|
+
lines.push(color("Entities:", colors.bold));
|
|
1796
|
+
lines.push("");
|
|
1797
|
+
for (const entity of result.entities) {
|
|
1798
|
+
const fieldCount = entity.fields.size;
|
|
1799
|
+
const relCount = entity.relationships.size;
|
|
1800
|
+
lines.push(
|
|
1801
|
+
` ${color(entity.name, colors.cyan)} (${fieldCount} fields, ${relCount} relationships)`
|
|
1802
|
+
);
|
|
1803
|
+
if (entity.behaviors.length > 0) {
|
|
1804
|
+
lines.push(color(` behaviors: ${entity.behaviors.join(", ")}`, colors.dim));
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
lines.push("");
|
|
1808
|
+
return lines;
|
|
1809
|
+
}
|
|
1810
|
+
function formatRelationships(result) {
|
|
1811
|
+
const lines = [];
|
|
1812
|
+
if (result.graph.edges.length === 0) {
|
|
1813
|
+
return lines;
|
|
1814
|
+
}
|
|
1815
|
+
lines.push(color("Relationships:", colors.bold));
|
|
1816
|
+
lines.push("");
|
|
1817
|
+
for (const edge of result.graph.edges) {
|
|
1818
|
+
const arrow = getCardinalityArrow(edge.cardinality);
|
|
1819
|
+
const bidir = edge.bidirectional ? color(" (bidirectional)", colors.dim) : "";
|
|
1820
|
+
lines.push(
|
|
1821
|
+
` ${edge.from.padEnd(20)} ${arrow} ${edge.to} ${color(`(${edge.relationship.type})`, colors.dim)}${bidir}`
|
|
1822
|
+
);
|
|
1823
|
+
}
|
|
1824
|
+
lines.push("");
|
|
1825
|
+
return lines;
|
|
1826
|
+
}
|
|
1827
|
+
function getCardinalityArrow(cardinality) {
|
|
1828
|
+
switch (cardinality) {
|
|
1829
|
+
case "1:N":
|
|
1830
|
+
return color("--<", colors.magenta);
|
|
1831
|
+
case "N:1":
|
|
1832
|
+
return color(">--", colors.magenta);
|
|
1833
|
+
case "1:1":
|
|
1834
|
+
return color("---", colors.magenta);
|
|
1835
|
+
case "N:M":
|
|
1836
|
+
return color(">-<", colors.magenta);
|
|
1837
|
+
default:
|
|
1838
|
+
return color("-->", colors.magenta);
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
function formatIssues(issues) {
|
|
1842
|
+
const lines = [];
|
|
1843
|
+
const errors = issues.filter((i) => i.severity === "error");
|
|
1844
|
+
const warnings = issues.filter((i) => i.severity === "warning");
|
|
1845
|
+
const infos = issues.filter((i) => i.severity === "info");
|
|
1846
|
+
if (errors.length > 0) {
|
|
1847
|
+
lines.push(color(`Errors (${errors.length}):`, colors.bold, colors.red));
|
|
1848
|
+
lines.push("");
|
|
1849
|
+
lines.push(...formatIssueList(errors, "error"));
|
|
1850
|
+
lines.push("");
|
|
1851
|
+
}
|
|
1852
|
+
if (warnings.length > 0) {
|
|
1853
|
+
lines.push(color(`Warnings (${warnings.length}):`, colors.bold, colors.yellow));
|
|
1854
|
+
lines.push("");
|
|
1855
|
+
lines.push(...formatIssueList(warnings, "warning"));
|
|
1856
|
+
lines.push("");
|
|
1857
|
+
}
|
|
1858
|
+
if (infos.length > 0) {
|
|
1859
|
+
lines.push(color(`Info (${infos.length}):`, colors.bold, colors.cyan));
|
|
1860
|
+
lines.push("");
|
|
1861
|
+
lines.push(...formatIssueList(infos, "info", 5));
|
|
1862
|
+
lines.push("");
|
|
1863
|
+
}
|
|
1864
|
+
return lines;
|
|
1865
|
+
}
|
|
1866
|
+
function formatIssueList(issues, severity, limit) {
|
|
1867
|
+
const lines = [];
|
|
1868
|
+
const displayIssues = limit ? issues.slice(0, limit) : issues;
|
|
1869
|
+
const byType = /* @__PURE__ */ new Map();
|
|
1870
|
+
for (const issue of displayIssues) {
|
|
1871
|
+
const list = byType.get(issue.type) ?? [];
|
|
1872
|
+
list.push(issue);
|
|
1873
|
+
byType.set(issue.type, list);
|
|
1874
|
+
}
|
|
1875
|
+
for (const [type, typeIssues] of byType) {
|
|
1876
|
+
lines.push(color(` ${type} (${typeIssues.length}):`, colors.dim));
|
|
1877
|
+
for (const issue of typeIssues.slice(0, 5)) {
|
|
1878
|
+
const location = issue.entity ? `${issue.entity}${issue.field ? "." + issue.field : ""}` : issue.path ?? "unknown";
|
|
1879
|
+
const icon = color(`[${severityIcon(severity)}]`, severityColor(severity));
|
|
1880
|
+
lines.push(` ${icon} ${color(location, colors.bold)}: ${issue.message}`);
|
|
1881
|
+
if (issue.suggestion) {
|
|
1882
|
+
lines.push(color(` -> ${issue.suggestion}`, colors.dim));
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
if (typeIssues.length > 5) {
|
|
1886
|
+
lines.push(color(` ... and ${typeIssues.length - 5} more`, colors.dim));
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
if (limit && issues.length > limit) {
|
|
1890
|
+
lines.push(color(` ... and ${issues.length - limit} more info messages`, colors.dim));
|
|
1891
|
+
}
|
|
1892
|
+
return lines;
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
// src/formatters/json-formatter.ts
|
|
1896
|
+
function mapToObject(map) {
|
|
1897
|
+
const obj = {};
|
|
1898
|
+
for (const [key, value] of map) {
|
|
1899
|
+
obj[key] = value;
|
|
1900
|
+
}
|
|
1901
|
+
return obj;
|
|
1902
|
+
}
|
|
1903
|
+
function serializeEntity(entity) {
|
|
1904
|
+
return {
|
|
1905
|
+
name: entity.name,
|
|
1906
|
+
plural: entity.plural,
|
|
1907
|
+
table: entity.table,
|
|
1908
|
+
folderStructure: entity.folderStructure,
|
|
1909
|
+
fields: mapToObject(entity.fields),
|
|
1910
|
+
relationships: mapToObject(entity.relationships),
|
|
1911
|
+
behaviors: entity.behaviors,
|
|
1912
|
+
sourcePath: entity.sourcePath
|
|
1913
|
+
};
|
|
1914
|
+
}
|
|
1915
|
+
function serializeGraph(graph) {
|
|
1916
|
+
const entities = {};
|
|
1917
|
+
for (const [name, entity] of graph.entities) {
|
|
1918
|
+
entities[name] = serializeEntity(entity);
|
|
1919
|
+
}
|
|
1920
|
+
return {
|
|
1921
|
+
entities,
|
|
1922
|
+
edges: graph.edges
|
|
1923
|
+
};
|
|
1924
|
+
}
|
|
1925
|
+
function formatJson(result, pretty = true) {
|
|
1926
|
+
const output = {
|
|
1927
|
+
isValid: result.isValid,
|
|
1928
|
+
summary: {
|
|
1929
|
+
entities: result.statistics.totalEntities,
|
|
1930
|
+
fields: result.statistics.totalFields,
|
|
1931
|
+
relationships: result.statistics.totalRelationships,
|
|
1932
|
+
errors: result.issues.filter((i) => i.severity === "error").length,
|
|
1933
|
+
warnings: result.issues.filter((i) => i.severity === "warning").length,
|
|
1934
|
+
info: result.issues.filter((i) => i.severity === "info").length
|
|
1935
|
+
},
|
|
1936
|
+
entities: result.entities.map(serializeEntity),
|
|
1937
|
+
graph: serializeGraph(result.graph),
|
|
1938
|
+
issues: result.issues,
|
|
1939
|
+
statistics: result.statistics,
|
|
1940
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1941
|
+
};
|
|
1942
|
+
return pretty ? JSON.stringify(output, null, 2) : JSON.stringify(output);
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
// src/formatters/markdown-formatter.ts
|
|
1946
|
+
function formatMarkdown(result) {
|
|
1947
|
+
const lines = [];
|
|
1948
|
+
lines.push("# Domain Model Documentation");
|
|
1949
|
+
lines.push("");
|
|
1950
|
+
lines.push(`Generated: ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
1951
|
+
lines.push("");
|
|
1952
|
+
lines.push("## Overview");
|
|
1953
|
+
lines.push("");
|
|
1954
|
+
lines.push("| Metric | Value |");
|
|
1955
|
+
lines.push("|--------|-------|");
|
|
1956
|
+
lines.push(`| Entities | ${result.statistics.totalEntities} |`);
|
|
1957
|
+
lines.push(`| Total Fields | ${result.statistics.totalFields} |`);
|
|
1958
|
+
lines.push(`| Total Relationships | ${result.statistics.totalRelationships} |`);
|
|
1959
|
+
lines.push(
|
|
1960
|
+
`| Avg Fields/Entity | ${result.statistics.averageFieldsPerEntity.toFixed(1)} |`
|
|
1961
|
+
);
|
|
1962
|
+
lines.push("");
|
|
1963
|
+
lines.push("### Field Type Distribution");
|
|
1964
|
+
lines.push("");
|
|
1965
|
+
lines.push("| Type | Count |");
|
|
1966
|
+
lines.push("|------|-------|");
|
|
1967
|
+
const sortedTypes = Object.entries(result.statistics.fieldsByType).sort(
|
|
1968
|
+
(a, b) => b[1] - a[1]
|
|
1969
|
+
);
|
|
1970
|
+
for (const [type, count] of sortedTypes) {
|
|
1971
|
+
lines.push(`| ${type} | ${count} |`);
|
|
1972
|
+
}
|
|
1973
|
+
lines.push("");
|
|
1974
|
+
if (result.statistics.totalRelationships > 0) {
|
|
1975
|
+
lines.push("### Relationship Type Distribution");
|
|
1976
|
+
lines.push("");
|
|
1977
|
+
lines.push("| Type | Count |");
|
|
1978
|
+
lines.push("|------|-------|");
|
|
1979
|
+
const sortedRels = Object.entries(result.statistics.relationshipsByType).sort(
|
|
1980
|
+
(a, b) => b[1] - a[1]
|
|
1981
|
+
);
|
|
1982
|
+
for (const [type, count] of sortedRels) {
|
|
1983
|
+
lines.push(`| ${type} | ${count} |`);
|
|
1984
|
+
}
|
|
1985
|
+
lines.push("");
|
|
1986
|
+
}
|
|
1987
|
+
lines.push("## Entity Relationship Diagram");
|
|
1988
|
+
lines.push("");
|
|
1989
|
+
lines.push("```mermaid");
|
|
1990
|
+
lines.push(...generateMermaidErDiagram(result));
|
|
1991
|
+
lines.push("```");
|
|
1992
|
+
lines.push("");
|
|
1993
|
+
lines.push("## Entities");
|
|
1994
|
+
lines.push("");
|
|
1995
|
+
for (const entity of result.entities) {
|
|
1996
|
+
lines.push(...formatEntitySection(entity));
|
|
1997
|
+
}
|
|
1998
|
+
const errors = result.issues.filter((i) => i.severity === "error");
|
|
1999
|
+
const warnings = result.issues.filter((i) => i.severity === "warning");
|
|
2000
|
+
const infos = result.issues.filter((i) => i.severity === "info");
|
|
2001
|
+
if (result.issues.length > 0) {
|
|
2002
|
+
lines.push("## Analysis Issues");
|
|
2003
|
+
lines.push("");
|
|
2004
|
+
if (errors.length > 0) {
|
|
2005
|
+
lines.push("### Errors");
|
|
2006
|
+
lines.push("");
|
|
2007
|
+
for (const issue of errors) {
|
|
2008
|
+
const location = issue.entity ? `**${issue.entity}${issue.field ? "." + issue.field : ""}**` : issue.path ?? "unknown";
|
|
2009
|
+
lines.push(`- [${issue.type}] ${location}: ${issue.message}`);
|
|
2010
|
+
if (issue.suggestion) {
|
|
2011
|
+
lines.push(` - Suggestion: ${issue.suggestion}`);
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
lines.push("");
|
|
2015
|
+
}
|
|
2016
|
+
if (warnings.length > 0) {
|
|
2017
|
+
lines.push("### Warnings");
|
|
2018
|
+
lines.push("");
|
|
2019
|
+
for (const issue of warnings) {
|
|
2020
|
+
const location = issue.entity ? `**${issue.entity}${issue.field ? "." + issue.field : ""}**` : issue.path ?? "unknown";
|
|
2021
|
+
lines.push(`- [${issue.type}] ${location}: ${issue.message}`);
|
|
2022
|
+
if (issue.suggestion) {
|
|
2023
|
+
lines.push(` - Suggestion: ${issue.suggestion}`);
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
lines.push("");
|
|
2027
|
+
}
|
|
2028
|
+
if (infos.length > 0) {
|
|
2029
|
+
lines.push("### Info");
|
|
2030
|
+
lines.push("");
|
|
2031
|
+
lines.push("<details>");
|
|
2032
|
+
lines.push("<summary>Show info messages</summary>");
|
|
2033
|
+
lines.push("");
|
|
2034
|
+
for (const issue of infos) {
|
|
2035
|
+
const location = issue.entity ? `**${issue.entity}${issue.field ? "." + issue.field : ""}**` : issue.path ?? "unknown";
|
|
2036
|
+
lines.push(`- [${issue.type}] ${location}: ${issue.message}`);
|
|
2037
|
+
}
|
|
2038
|
+
lines.push("");
|
|
2039
|
+
lines.push("</details>");
|
|
2040
|
+
lines.push("");
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
return lines.join("\n");
|
|
2044
|
+
}
|
|
2045
|
+
function formatEntitySection(entity) {
|
|
2046
|
+
const lines = [];
|
|
2047
|
+
lines.push(`### ${entity.name}`);
|
|
2048
|
+
lines.push("");
|
|
2049
|
+
lines.push(`**Table:** \`${entity.table}\``);
|
|
2050
|
+
lines.push(`**Plural:** ${entity.plural}`);
|
|
2051
|
+
if (entity.behaviors.length > 0) {
|
|
2052
|
+
lines.push(`**Behaviors:** ${entity.behaviors.join(", ")}`);
|
|
2053
|
+
}
|
|
2054
|
+
lines.push("");
|
|
2055
|
+
lines.push("#### Fields");
|
|
2056
|
+
lines.push("");
|
|
2057
|
+
lines.push("| Name | Type | Required | Nullable | Index | Foreign Key |");
|
|
2058
|
+
lines.push("|------|------|----------|----------|-------|-------------|");
|
|
2059
|
+
for (const [name, field] of entity.fields) {
|
|
2060
|
+
const required = field.required ? "Yes" : "";
|
|
2061
|
+
const nullable = field.nullable ? "Yes" : "";
|
|
2062
|
+
const index = field.index ? "Yes" : field.unique ? "Unique" : "";
|
|
2063
|
+
const fk = field.foreignKey ? `${field.foreignKey.table}.${field.foreignKey.column}` : "";
|
|
2064
|
+
lines.push(`| ${name} | ${field.type} | ${required} | ${nullable} | ${index} | ${fk} |`);
|
|
2065
|
+
}
|
|
2066
|
+
lines.push("");
|
|
2067
|
+
if (entity.relationships.size > 0) {
|
|
2068
|
+
lines.push("#### Relationships");
|
|
2069
|
+
lines.push("");
|
|
2070
|
+
lines.push("| Name | Type | Target | Foreign Key |");
|
|
2071
|
+
lines.push("|------|------|--------|-------------|");
|
|
2072
|
+
for (const [name, rel] of entity.relationships) {
|
|
2073
|
+
lines.push(`| ${name} | ${rel.type} | ${rel.target} | ${rel.foreignKey} |`);
|
|
2074
|
+
}
|
|
2075
|
+
lines.push("");
|
|
2076
|
+
}
|
|
2077
|
+
return lines;
|
|
2078
|
+
}
|
|
2079
|
+
function generateMermaidErDiagram(result) {
|
|
2080
|
+
const lines = [];
|
|
2081
|
+
lines.push("erDiagram");
|
|
2082
|
+
for (const entity of result.entities) {
|
|
2083
|
+
const entityName = entity.name.toUpperCase();
|
|
2084
|
+
lines.push(` ${entityName} {`);
|
|
2085
|
+
const keyFields = Array.from(entity.fields.entries()).filter(
|
|
2086
|
+
([name, field]) => field.foreignKey || field.unique || field.index || name === "id" || name === "name"
|
|
2087
|
+
).slice(0, 6);
|
|
2088
|
+
for (const [name, field] of keyFields) {
|
|
2089
|
+
const typeStr = field.type;
|
|
2090
|
+
const pk = name === "id" ? "PK" : "";
|
|
2091
|
+
const fk = field.foreignKey ? "FK" : "";
|
|
2092
|
+
const marker = pk || fk ? ` "${pk}${fk}"` : "";
|
|
2093
|
+
lines.push(` ${typeStr} ${name}${marker}`);
|
|
2094
|
+
}
|
|
2095
|
+
if (entity.fields.size > keyFields.length) {
|
|
2096
|
+
lines.push(` string _more_fields`);
|
|
2097
|
+
}
|
|
2098
|
+
lines.push(" }");
|
|
2099
|
+
}
|
|
2100
|
+
for (const edge of result.graph.edges) {
|
|
2101
|
+
const from = edge.from.toUpperCase();
|
|
2102
|
+
const to = edge.to.toUpperCase();
|
|
2103
|
+
const cardinalitySymbol = getCardinalitySymbol(edge.cardinality);
|
|
2104
|
+
const label = edge.relationship.name;
|
|
2105
|
+
lines.push(` ${from} ${cardinalitySymbol} ${to} : "${label}"`);
|
|
2106
|
+
}
|
|
2107
|
+
return lines;
|
|
2108
|
+
}
|
|
2109
|
+
function getCardinalitySymbol(cardinality) {
|
|
2110
|
+
switch (cardinality) {
|
|
2111
|
+
case "1:N":
|
|
2112
|
+
return "||--o{";
|
|
2113
|
+
case "N:1":
|
|
2114
|
+
return "}o--||";
|
|
2115
|
+
case "1:1":
|
|
2116
|
+
return "||--||";
|
|
2117
|
+
case "N:M":
|
|
2118
|
+
return "}o--o{";
|
|
2119
|
+
default:
|
|
2120
|
+
return "||--o{";
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
function formatMermaidGraph(result) {
|
|
2124
|
+
const lines = [];
|
|
2125
|
+
lines.push("```mermaid");
|
|
2126
|
+
lines.push("graph LR");
|
|
2127
|
+
lines.push(" classDef entity fill:#e1f5fe,stroke:#01579b");
|
|
2128
|
+
for (const entity of result.entities) {
|
|
2129
|
+
lines.push(` ${entity.name}["${entity.name}\\n(${entity.fields.size} fields)"]`);
|
|
2130
|
+
}
|
|
2131
|
+
for (const edge of result.graph.edges) {
|
|
2132
|
+
const style = edge.bidirectional ? "<-->" : "-->";
|
|
2133
|
+
lines.push(` ${edge.from} ${style}|${edge.relationship.type}| ${edge.to}`);
|
|
2134
|
+
}
|
|
2135
|
+
const entityList = result.entities.map((e) => e.name).join(",");
|
|
2136
|
+
if (entityList) {
|
|
2137
|
+
lines.push(` class ${entityList} entity`);
|
|
2138
|
+
}
|
|
2139
|
+
lines.push("```");
|
|
2140
|
+
return lines.join("\n");
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
// src/index.ts
|
|
2144
|
+
async function analyzeDomain(entitiesDir, relationshipsDir) {
|
|
2145
|
+
const { entities, issues: loadIssues } = loadEntities(entitiesDir);
|
|
2146
|
+
const { relationships: relationshipDefinitions, issues: relLoadIssues } = relationshipsDir ? loadRelationships(relationshipsDir) : { relationships: [], issues: [] };
|
|
2147
|
+
const resolveIssues = resolveReferences(entities);
|
|
2148
|
+
const relResolveIssues = resolveRelationshipReferences(
|
|
2149
|
+
relationshipDefinitions,
|
|
2150
|
+
entities
|
|
2151
|
+
);
|
|
2152
|
+
const graph = buildDomainGraph(entities, relationshipDefinitions);
|
|
2153
|
+
const consistencyIssues = checkConsistency(graph);
|
|
2154
|
+
const statistics = computeStatistics(graph);
|
|
2155
|
+
const allIssues = [
|
|
2156
|
+
...loadIssues,
|
|
2157
|
+
...relLoadIssues,
|
|
2158
|
+
...resolveIssues,
|
|
2159
|
+
...relResolveIssues,
|
|
2160
|
+
...consistencyIssues
|
|
2161
|
+
];
|
|
2162
|
+
const hasErrors = allIssues.some((i) => i.severity === "error");
|
|
2163
|
+
return {
|
|
2164
|
+
isValid: !hasErrors,
|
|
2165
|
+
entities,
|
|
2166
|
+
relationshipDefinitions,
|
|
2167
|
+
graph,
|
|
2168
|
+
issues: allIssues,
|
|
2169
|
+
statistics
|
|
2170
|
+
};
|
|
2171
|
+
}
|
|
2172
|
+
function validateEntities(entitiesDir) {
|
|
2173
|
+
const { entities, issues } = loadEntities(entitiesDir);
|
|
2174
|
+
const errors = issues.filter((i) => i.severity === "error").map((i) => i.message);
|
|
2175
|
+
return {
|
|
2176
|
+
valid: errors.length === 0,
|
|
2177
|
+
errors
|
|
2178
|
+
};
|
|
2179
|
+
}
|
|
2180
|
+
export {
|
|
2181
|
+
analyzeDomain,
|
|
2182
|
+
buildDomainGraph,
|
|
2183
|
+
checkConsistency,
|
|
2184
|
+
computeStatistics,
|
|
2185
|
+
findCircularDependencies,
|
|
2186
|
+
findOrphanEntities,
|
|
2187
|
+
formatConsole,
|
|
2188
|
+
formatJson,
|
|
2189
|
+
formatMarkdown,
|
|
2190
|
+
formatMermaidGraph,
|
|
2191
|
+
getRelatedEntities,
|
|
2192
|
+
loadEntities,
|
|
2193
|
+
loadEntityFromYaml,
|
|
2194
|
+
loadRelationshipFromYaml,
|
|
2195
|
+
loadRelationships,
|
|
2196
|
+
validateEntities
|
|
2197
|
+
};
|
|
2198
|
+
//# sourceMappingURL=index.js.map
|