@pattern-stack/codegen 0.15.1 → 0.15.2
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 +53 -0
- package/dist/chunk-24CWKBK5.js +94 -0
- package/dist/chunk-24CWKBK5.js.map +1 -0
- package/dist/chunk-2E224ZSN.js +20 -0
- package/dist/chunk-2E224ZSN.js.map +1 -0
- package/dist/chunk-2FTZLDBP.js +179 -0
- package/dist/chunk-2FTZLDBP.js.map +1 -0
- package/dist/chunk-2N4UG4VD.js +20 -0
- package/dist/chunk-2N4UG4VD.js.map +1 -0
- package/dist/chunk-2TVVBC53.js +92 -0
- package/dist/chunk-2TVVBC53.js.map +1 -0
- package/dist/chunk-2VHZ7EKC.js +37 -0
- package/dist/chunk-2VHZ7EKC.js.map +1 -0
- package/dist/chunk-32BMMV4H.js +109 -0
- package/dist/chunk-32BMMV4H.js.map +1 -0
- package/dist/chunk-32DOFN3T.js +4042 -0
- package/dist/chunk-32DOFN3T.js.map +1 -0
- package/dist/chunk-36U5UGIO.js +107 -0
- package/dist/chunk-36U5UGIO.js.map +1 -0
- package/dist/chunk-3CJFPU6Q.js +14 -0
- package/dist/chunk-3CJFPU6Q.js.map +1 -0
- package/dist/chunk-3NMCDN7L.js +90 -0
- package/dist/chunk-3NMCDN7L.js.map +1 -0
- package/dist/chunk-3SZFUTXE.js +62 -0
- package/dist/chunk-3SZFUTXE.js.map +1 -0
- package/dist/chunk-4DOJBQTP.js +117 -0
- package/dist/chunk-4DOJBQTP.js.map +1 -0
- package/dist/chunk-4JLJYWJC.js +308 -0
- package/dist/chunk-4JLJYWJC.js.map +1 -0
- package/dist/chunk-4KNXX6TI.js +29 -0
- package/dist/chunk-4KNXX6TI.js.map +1 -0
- package/dist/chunk-4LH67P4U.js +17 -0
- package/dist/chunk-4LH67P4U.js.map +1 -0
- package/dist/chunk-4MVGAMUA.js +40 -0
- package/dist/chunk-4MVGAMUA.js.map +1 -0
- package/dist/chunk-4OMHBMZJ.js +75 -0
- package/dist/chunk-4OMHBMZJ.js.map +1 -0
- package/dist/chunk-4RFHUZXU.js +635 -0
- package/dist/chunk-4RFHUZXU.js.map +1 -0
- package/dist/chunk-5A432NZJ.js +7 -0
- package/dist/chunk-5A432NZJ.js.map +1 -0
- package/dist/chunk-5Y7W3XR6.js +356 -0
- package/dist/chunk-5Y7W3XR6.js.map +1 -0
- package/dist/chunk-6DWFJNIK.js +15 -0
- package/dist/chunk-6DWFJNIK.js.map +1 -0
- package/dist/chunk-6I7ULIN6.js +15 -0
- package/dist/chunk-6I7ULIN6.js.map +1 -0
- package/dist/chunk-6XY6ZMMD.js +25 -0
- package/dist/chunk-6XY6ZMMD.js.map +1 -0
- package/dist/chunk-7B3RYX45.js +63 -0
- package/dist/chunk-7B3RYX45.js.map +1 -0
- package/dist/chunk-7C3FOSDI.js +1 -0
- package/dist/chunk-7C3FOSDI.js.map +1 -0
- package/dist/chunk-7KOW6PU6.js +59 -0
- package/dist/chunk-7KOW6PU6.js.map +1 -0
- package/dist/chunk-7LKAMLV4.js +92 -0
- package/dist/chunk-7LKAMLV4.js.map +1 -0
- package/dist/chunk-7RELQJIN.js +22 -0
- package/dist/chunk-7RELQJIN.js.map +1 -0
- package/dist/chunk-AHV4GDYM.js +63 -0
- package/dist/chunk-AHV4GDYM.js.map +1 -0
- package/dist/chunk-AQFQ4BYM.js +81 -0
- package/dist/chunk-AQFQ4BYM.js.map +1 -0
- package/dist/chunk-AS3NAZB6.js +14 -0
- package/dist/chunk-AS3NAZB6.js.map +1 -0
- package/dist/chunk-BGULBWKJ.js +88 -0
- package/dist/chunk-BGULBWKJ.js.map +1 -0
- package/dist/chunk-BIO6F7YI.js +17 -0
- package/dist/chunk-BIO6F7YI.js.map +1 -0
- package/dist/chunk-BOPZWRJK.js +36 -0
- package/dist/chunk-BOPZWRJK.js.map +1 -0
- package/dist/chunk-BPARRK6F.js +14 -0
- package/dist/chunk-BPARRK6F.js.map +1 -0
- package/dist/chunk-CO6LUM72.js +59 -0
- package/dist/chunk-CO6LUM72.js.map +1 -0
- package/dist/chunk-COGHTKXY.js +84 -0
- package/dist/chunk-COGHTKXY.js.map +1 -0
- package/dist/chunk-DCCZB4UC.js +100 -0
- package/dist/chunk-DCCZB4UC.js.map +1 -0
- package/dist/chunk-DKKFTHHI.js +53 -0
- package/dist/chunk-DKKFTHHI.js.map +1 -0
- package/dist/chunk-DV4RV2DC.js +59 -0
- package/dist/chunk-DV4RV2DC.js.map +1 -0
- package/dist/chunk-EDKJU5BO.js +11 -0
- package/dist/chunk-EDKJU5BO.js.map +1 -0
- package/dist/chunk-EO2QPOKH.js +116 -0
- package/dist/chunk-EO2QPOKH.js.map +1 -0
- package/dist/chunk-EOLLMEAH.js +155 -0
- package/dist/chunk-EOLLMEAH.js.map +1 -0
- package/dist/chunk-EWYCWP4H.js +14 -0
- package/dist/chunk-EWYCWP4H.js.map +1 -0
- package/dist/chunk-EXVDJMIY.js +33 -0
- package/dist/chunk-EXVDJMIY.js.map +1 -0
- package/dist/chunk-FASRXRX5.js +19 -0
- package/dist/chunk-FASRXRX5.js.map +1 -0
- package/dist/chunk-FI34KYZ5.js +1 -0
- package/dist/chunk-FI34KYZ5.js.map +1 -0
- package/dist/chunk-FN2PYDPP.js +1 -0
- package/dist/chunk-FN2PYDPP.js.map +1 -0
- package/dist/chunk-GM3RMJIJ.js +92 -0
- package/dist/chunk-GM3RMJIJ.js.map +1 -0
- package/dist/chunk-GYGNEQSC.js +9 -0
- package/dist/chunk-GYGNEQSC.js.map +1 -0
- package/dist/chunk-H5NH7KPE.js +21 -0
- package/dist/chunk-H5NH7KPE.js.map +1 -0
- package/dist/chunk-HNWZFNKP.js +168 -0
- package/dist/chunk-HNWZFNKP.js.map +1 -0
- package/dist/chunk-HUH73XGI.js +1 -0
- package/dist/chunk-HUH73XGI.js.map +1 -0
- package/dist/chunk-I6MG4M3F.js +201 -0
- package/dist/chunk-I6MG4M3F.js.map +1 -0
- package/dist/chunk-I6MVCB5A.js +39 -0
- package/dist/chunk-I6MVCB5A.js.map +1 -0
- package/dist/chunk-IBGER4YK.js +12 -0
- package/dist/chunk-IBGER4YK.js.map +1 -0
- package/dist/chunk-IF5I3DAA.js +92 -0
- package/dist/chunk-IF5I3DAA.js.map +1 -0
- package/dist/chunk-IP4OO26U.js +54 -0
- package/dist/chunk-IP4OO26U.js.map +1 -0
- package/dist/chunk-IWAOY6KC.js +1 -0
- package/dist/chunk-IWAOY6KC.js.map +1 -0
- package/dist/chunk-J37YWU7Y.js +19 -0
- package/dist/chunk-J37YWU7Y.js.map +1 -0
- package/dist/chunk-J6KZS54B.js +269 -0
- package/dist/chunk-J6KZS54B.js.map +1 -0
- package/dist/chunk-J6MN42LG.js +19 -0
- package/dist/chunk-J6MN42LG.js.map +1 -0
- package/dist/chunk-JRQO2IOF.js +65 -0
- package/dist/chunk-JRQO2IOF.js.map +1 -0
- package/dist/chunk-JRVNVKN6.js +212 -0
- package/dist/chunk-JRVNVKN6.js.map +1 -0
- package/dist/chunk-JWNHNUYL.js +96 -0
- package/dist/chunk-JWNHNUYL.js.map +1 -0
- package/dist/chunk-K2I6XIK5.js +122 -0
- package/dist/chunk-K2I6XIK5.js.map +1 -0
- package/dist/chunk-KMZCQASO.js +111 -0
- package/dist/chunk-KMZCQASO.js.map +1 -0
- package/dist/chunk-KVOWSC5S.js +1 -0
- package/dist/chunk-KVOWSC5S.js.map +1 -0
- package/dist/chunk-KYR3B3OW.js +79 -0
- package/dist/chunk-KYR3B3OW.js.map +1 -0
- package/dist/chunk-L3LZWWSX.js +61 -0
- package/dist/chunk-L3LZWWSX.js.map +1 -0
- package/dist/chunk-L4SDDEEU.js +1 -0
- package/dist/chunk-L4SDDEEU.js.map +1 -0
- package/dist/chunk-L6FTY45T.js +13 -0
- package/dist/chunk-L6FTY45T.js.map +1 -0
- package/dist/chunk-L7BNNRGI.js +134 -0
- package/dist/chunk-L7BNNRGI.js.map +1 -0
- package/dist/chunk-LG57S2SC.js +150 -0
- package/dist/chunk-LG57S2SC.js.map +1 -0
- package/dist/chunk-M6QLSLPO.js +97 -0
- package/dist/chunk-M6QLSLPO.js.map +1 -0
- package/dist/chunk-MZ6GV4YF.js +21 -0
- package/dist/chunk-MZ6GV4YF.js.map +1 -0
- package/dist/chunk-N5OTOWTP.js +55 -0
- package/dist/chunk-N5OTOWTP.js.map +1 -0
- package/dist/chunk-NN7XZEGF.js +14 -0
- package/dist/chunk-NN7XZEGF.js.map +1 -0
- package/dist/chunk-NPFPZ2HO.js +13 -0
- package/dist/chunk-NPFPZ2HO.js.map +1 -0
- package/dist/chunk-NXXDZ6ZF.js +42 -0
- package/dist/chunk-NXXDZ6ZF.js.map +1 -0
- package/dist/chunk-NYBCQZC7.js +11 -0
- package/dist/chunk-NYBCQZC7.js.map +1 -0
- package/dist/chunk-OFRRBC7M.js +78 -0
- package/dist/chunk-OFRRBC7M.js.map +1 -0
- package/dist/chunk-OGIZXGPY.js +222 -0
- package/dist/chunk-OGIZXGPY.js.map +1 -0
- package/dist/chunk-OKXZ63IA.js +168 -0
- package/dist/chunk-OKXZ63IA.js.map +1 -0
- package/dist/chunk-OSQRXVG2.js +58 -0
- package/dist/chunk-OSQRXVG2.js.map +1 -0
- package/dist/chunk-OTDN3OUQ.js +215 -0
- package/dist/chunk-OTDN3OUQ.js.map +1 -0
- package/dist/chunk-OZZJDRGW.js +122 -0
- package/dist/chunk-OZZJDRGW.js.map +1 -0
- package/dist/chunk-PNZSGAB2.js +114 -0
- package/dist/chunk-PNZSGAB2.js.map +1 -0
- package/dist/chunk-PRWIX6UW.js +21 -0
- package/dist/chunk-PRWIX6UW.js.map +1 -0
- package/dist/chunk-PSXUNOVU.js +7 -0
- package/dist/chunk-PSXUNOVU.js.map +1 -0
- package/dist/chunk-QLTJSCE6.js +44 -0
- package/dist/chunk-QLTJSCE6.js.map +1 -0
- package/dist/chunk-RC23QROE.js +447 -0
- package/dist/chunk-RC23QROE.js.map +1 -0
- package/dist/chunk-RFH7N6EP.js +36 -0
- package/dist/chunk-RFH7N6EP.js.map +1 -0
- package/dist/chunk-RHVN6NA7.js +134 -0
- package/dist/chunk-RHVN6NA7.js.map +1 -0
- package/dist/chunk-S7C6TIIF.js +21 -0
- package/dist/chunk-S7C6TIIF.js.map +1 -0
- package/dist/chunk-SNQ3TOWP.js +20 -0
- package/dist/chunk-SNQ3TOWP.js.map +1 -0
- package/dist/chunk-SOVM2VEK.js +14 -0
- package/dist/chunk-SOVM2VEK.js.map +1 -0
- package/dist/chunk-SQDOBLBP.js +13 -0
- package/dist/chunk-SQDOBLBP.js.map +1 -0
- package/dist/chunk-SR7F3TJY.js +130 -0
- package/dist/chunk-SR7F3TJY.js.map +1 -0
- package/dist/chunk-SZVPIHWE.js +129 -0
- package/dist/chunk-SZVPIHWE.js.map +1 -0
- package/dist/chunk-T4BIIU5E.js +89 -0
- package/dist/chunk-T4BIIU5E.js.map +1 -0
- package/dist/chunk-T6C4LFLC.js +112 -0
- package/dist/chunk-T6C4LFLC.js.map +1 -0
- package/dist/chunk-TNXH7BJS.js +48 -0
- package/dist/chunk-TNXH7BJS.js.map +1 -0
- package/dist/chunk-U64T4YZE.js +9 -0
- package/dist/chunk-U64T4YZE.js.map +1 -0
- package/dist/chunk-UQ5EHOH2.js +39 -0
- package/dist/chunk-UQ5EHOH2.js.map +1 -0
- package/dist/chunk-UTN4GBPQ.js +1 -0
- package/dist/chunk-UTN4GBPQ.js.map +1 -0
- package/dist/chunk-V4AF6DI4.js +16 -0
- package/dist/chunk-V4AF6DI4.js.map +1 -0
- package/dist/chunk-W72PRNJY.js +126 -0
- package/dist/chunk-W72PRNJY.js.map +1 -0
- package/dist/chunk-WEVWJKOW.js +81 -0
- package/dist/chunk-WEVWJKOW.js.map +1 -0
- package/dist/chunk-WL67FZGF.js +21 -0
- package/dist/chunk-WL67FZGF.js.map +1 -0
- package/dist/chunk-WPXNN6QS.js +290 -0
- package/dist/chunk-WPXNN6QS.js.map +1 -0
- package/dist/chunk-WRUUSZDJ.js +29 -0
- package/dist/chunk-WRUUSZDJ.js.map +1 -0
- package/dist/chunk-X2GMTYPA.js +50 -0
- package/dist/chunk-X2GMTYPA.js.map +1 -0
- package/dist/chunk-XCEI7NUH.js +41 -0
- package/dist/chunk-XCEI7NUH.js.map +1 -0
- package/dist/chunk-Y7GDG744.js +88 -0
- package/dist/chunk-Y7GDG744.js.map +1 -0
- package/dist/chunk-Y7RRSEOC.js +9 -0
- package/dist/chunk-Y7RRSEOC.js.map +1 -0
- package/dist/chunk-YPWODKD5.js +184 -0
- package/dist/chunk-YPWODKD5.js.map +1 -0
- package/dist/chunk-YSLTTQLC.js +25 -0
- package/dist/chunk-YSLTTQLC.js.map +1 -0
- package/dist/chunk-YTN6BKWA.js +121 -0
- package/dist/chunk-YTN6BKWA.js.map +1 -0
- package/dist/chunk-Z7PQCAVK.js +200 -0
- package/dist/chunk-Z7PQCAVK.js.map +1 -0
- package/dist/chunk-ZUKFQL6E.js +47 -0
- package/dist/chunk-ZUKFQL6E.js.map +1 -0
- package/dist/chunk-ZUMULSEQ.js +1 -0
- package/dist/chunk-ZUMULSEQ.js.map +1 -0
- package/dist/runtime/analytics/index.js +8 -41
- package/dist/runtime/analytics/index.js.map +1 -1
- package/dist/runtime/analytics/types.js +8 -41
- package/dist/runtime/analytics/types.js.map +1 -1
- package/dist/runtime/base-classes/activity-entity-repository.js +6 -312
- package/dist/runtime/base-classes/activity-entity-repository.js.map +1 -1
- package/dist/runtime/base-classes/activity-entity-service.js +6 -212
- package/dist/runtime/base-classes/activity-entity-service.js.map +1 -1
- package/dist/runtime/base-classes/base-read-use-cases.js +5 -27
- package/dist/runtime/base-classes/base-read-use-cases.js.map +1 -1
- package/dist/runtime/base-classes/base-repository.js +5 -277
- package/dist/runtime/base-classes/base-repository.js.map +1 -1
- package/dist/runtime/base-classes/base-service.js +5 -184
- package/dist/runtime/base-classes/base-service.js.map +1 -1
- package/dist/runtime/base-classes/index.js +59 -1076
- package/dist/runtime/base-classes/index.js.map +1 -1
- package/dist/runtime/base-classes/integrated-entity-repository.js +6 -486
- package/dist/runtime/base-classes/integrated-entity-repository.js.map +1 -1
- package/dist/runtime/base-classes/integrated-entity-service.js +6 -213
- package/dist/runtime/base-classes/integrated-entity-service.js.map +1 -1
- package/dist/runtime/base-classes/junction-integration-repository.js +8 -448
- package/dist/runtime/base-classes/junction-integration-repository.js.map +1 -1
- package/dist/runtime/base-classes/knowledge-entity-repository.js +6 -283
- package/dist/runtime/base-classes/knowledge-entity-repository.js.map +1 -1
- package/dist/runtime/base-classes/knowledge-entity-service.js +6 -190
- package/dist/runtime/base-classes/knowledge-entity-service.js.map +1 -1
- package/dist/runtime/base-classes/lifecycle-events.js +8 -70
- package/dist/runtime/base-classes/lifecycle-events.js.map +1 -1
- package/dist/runtime/base-classes/metadata-entity-repository.js +6 -330
- package/dist/runtime/base-classes/metadata-entity-repository.js.map +1 -1
- package/dist/runtime/base-classes/metadata-entity-service.js +6 -212
- package/dist/runtime/base-classes/metadata-entity-service.js.map +1 -1
- package/dist/runtime/base-classes/tenant-context.js +10 -36
- package/dist/runtime/base-classes/tenant-context.js.map +1 -1
- package/dist/runtime/base-classes/with-analytics.js +4 -7
- package/dist/runtime/base-classes/with-analytics.js.map +1 -1
- package/dist/runtime/constants/tokens.js +5 -3
- package/dist/runtime/constants/tokens.js.map +1 -1
- package/dist/runtime/eav-helpers.js +2 -0
- package/dist/runtime/eav-helpers.js.map +1 -1
- package/dist/runtime/pipes/zod-validation.pipe.js +3 -10
- package/dist/runtime/pipes/zod-validation.pipe.js.map +1 -1
- package/dist/runtime/shared/openapi/error-response.dto.js +5 -8
- package/dist/runtime/shared/openapi/error-response.dto.js.map +1 -1
- package/dist/runtime/shared/openapi/errors.js +5 -19
- package/dist/runtime/shared/openapi/errors.js.map +1 -1
- package/dist/runtime/shared/openapi/index.js +15 -106
- package/dist/runtime/shared/openapi/index.js.map +1 -1
- package/dist/runtime/shared/openapi/registry.js +6 -103
- package/dist/runtime/shared/openapi/registry.js.map +1 -1
- package/dist/runtime/shared/openapi/registry.tokens.js +4 -2
- package/dist/runtime/shared/openapi/registry.tokens.js.map +1 -1
- package/dist/runtime/subsystems/analytics/analytics.module.js +8 -117
- package/dist/runtime/subsystems/analytics/analytics.module.js.map +1 -1
- package/dist/runtime/subsystems/analytics/analytics.tokens.js +7 -8
- package/dist/runtime/subsystems/analytics/analytics.tokens.js.map +1 -1
- package/dist/runtime/subsystems/analytics/cube-backend.js +6 -71
- package/dist/runtime/subsystems/analytics/cube-backend.js.map +1 -1
- package/dist/runtime/subsystems/analytics/index.js +16 -117
- package/dist/runtime/subsystems/analytics/index.js.map +1 -1
- package/dist/runtime/subsystems/analytics/noop-backend.js +4 -21
- package/dist/runtime/subsystems/analytics/noop-backend.js.map +1 -1
- package/dist/runtime/subsystems/auth/auth-oauth-state.schema.js +4 -8
- package/dist/runtime/subsystems/auth/auth-oauth-state.schema.js.map +1 -1
- package/dist/runtime/subsystems/auth/auth.module.js +12 -359
- package/dist/runtime/subsystems/auth/auth.module.js.map +1 -1
- package/dist/runtime/subsystems/auth/auth.tokens.js +12 -13
- package/dist/runtime/subsystems/auth/auth.tokens.js.map +1 -1
- package/dist/runtime/subsystems/auth/backends/encryption-key/env.js +4 -49
- package/dist/runtime/subsystems/auth/backends/encryption-key/env.js.map +1 -1
- package/dist/runtime/subsystems/auth/backends/state-store.drizzle-backend.js +6 -64
- package/dist/runtime/subsystems/auth/backends/state-store.drizzle-backend.js.map +1 -1
- package/dist/runtime/subsystems/auth/backends/state-store.memory-backend.js +5 -47
- package/dist/runtime/subsystems/auth/backends/state-store.memory-backend.js.map +1 -1
- package/dist/runtime/subsystems/auth/controllers/auth.controller.js +5 -139
- package/dist/runtime/subsystems/auth/controllers/auth.controller.js.map +1 -1
- package/dist/runtime/subsystems/auth/index.js +53 -542
- package/dist/runtime/subsystems/auth/index.js.map +1 -1
- package/dist/runtime/subsystems/auth/middleware/requester-context.js +9 -65
- package/dist/runtime/subsystems/auth/middleware/requester-context.js.map +1 -1
- package/dist/runtime/subsystems/auth/protocols/oauth-state-store.js +4 -9
- package/dist/runtime/subsystems/auth/protocols/oauth-state-store.js.map +1 -1
- package/dist/runtime/subsystems/auth/runtime/connection-broken.error.js +4 -15
- package/dist/runtime/subsystems/auth/runtime/connection-broken.error.js.map +1 -1
- package/dist/runtime/subsystems/auth/runtime/oauth2-refresh.strategy.js +5 -104
- package/dist/runtime/subsystems/auth/runtime/oauth2-refresh.strategy.js.map +1 -1
- package/dist/runtime/subsystems/auth/runtime/session-expired.error.js +5 -16
- package/dist/runtime/subsystems/auth/runtime/session-expired.error.js.map +1 -1
- package/dist/runtime/subsystems/auth/runtime/with-auth-retry.js +5 -29
- package/dist/runtime/subsystems/auth/runtime/with-auth-retry.js.map +1 -1
- package/dist/runtime/subsystems/bridge/assert-tenant-id.js +5 -18
- package/dist/runtime/subsystems/bridge/assert-tenant-id.js.map +1 -1
- package/dist/runtime/subsystems/bridge/bridge-delivery-handler.js +12 -184
- package/dist/runtime/subsystems/bridge/bridge-delivery-handler.js.map +1 -1
- package/dist/runtime/subsystems/bridge/bridge-delivery.drizzle-backend.js +10 -448
- package/dist/runtime/subsystems/bridge/bridge-delivery.drizzle-backend.js.map +1 -1
- package/dist/runtime/subsystems/bridge/bridge-delivery.memory-backend.js +5 -126
- package/dist/runtime/subsystems/bridge/bridge-delivery.memory-backend.js.map +1 -1
- package/dist/runtime/subsystems/bridge/bridge-delivery.schema.js +6 -308
- package/dist/runtime/subsystems/bridge/bridge-delivery.schema.js.map +1 -1
- package/dist/runtime/subsystems/bridge/bridge-errors.js +6 -35
- package/dist/runtime/subsystems/bridge/bridge-errors.js.map +1 -1
- package/dist/runtime/subsystems/bridge/bridge-outbox-drain-hook.js +14 -606
- package/dist/runtime/subsystems/bridge/bridge-outbox-drain-hook.js.map +1 -1
- package/dist/runtime/subsystems/bridge/bridge.module.js +35 -3476
- package/dist/runtime/subsystems/bridge/bridge.module.js.map +1 -1
- package/dist/runtime/subsystems/bridge/bridge.tokens.js +9 -7
- package/dist/runtime/subsystems/bridge/bridge.tokens.js.map +1 -1
- package/dist/runtime/subsystems/bridge/event-flow.service.js +11 -137
- package/dist/runtime/subsystems/bridge/event-flow.service.js.map +1 -1
- package/dist/runtime/subsystems/bridge/generated/registry.js +4 -2
- package/dist/runtime/subsystems/bridge/generated/registry.js.map +1 -1
- package/dist/runtime/subsystems/bridge/index.js +60 -3470
- package/dist/runtime/subsystems/bridge/index.js.map +1 -1
- package/dist/runtime/subsystems/bridge/reserved-pools.js +4 -6
- package/dist/runtime/subsystems/bridge/reserved-pools.js.map +1 -1
- package/dist/runtime/subsystems/cache/cache.drizzle-backend.js +10 -133
- package/dist/runtime/subsystems/cache/cache.drizzle-backend.js.map +1 -1
- package/dist/runtime/subsystems/cache/cache.memory-backend.js +6 -101
- package/dist/runtime/subsystems/cache/cache.memory-backend.js.map +1 -1
- package/dist/runtime/subsystems/cache/cache.module.js +10 -278
- package/dist/runtime/subsystems/cache/cache.module.js.map +1 -1
- package/dist/runtime/subsystems/cache/cache.schema.js +4 -14
- package/dist/runtime/subsystems/cache/cache.schema.js.map +1 -1
- package/dist/runtime/subsystems/cache/cache.tokens.js +6 -7
- package/dist/runtime/subsystems/cache/cache.tokens.js.map +1 -1
- package/dist/runtime/subsystems/cache/index.js +20 -278
- package/dist/runtime/subsystems/cache/index.js.map +1 -1
- package/dist/runtime/subsystems/events/domain-events.schema.js +3 -72
- package/dist/runtime/subsystems/events/domain-events.schema.js.map +1 -1
- package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js +9 -413
- package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js.map +1 -1
- package/dist/runtime/subsystems/events/event-bus.memory-backend.js +7 -235
- package/dist/runtime/subsystems/events/event-bus.memory-backend.js.map +1 -1
- package/dist/runtime/subsystems/events/event-bus.redis-backend.js +8 -20
- package/dist/runtime/subsystems/events/event-bus.redis-backend.js.map +1 -1
- package/dist/runtime/subsystems/events/event-keyset-cursor.js +8 -30
- package/dist/runtime/subsystems/events/event-keyset-cursor.js.map +1 -1
- package/dist/runtime/subsystems/events/event-read.protocol.js +2 -0
- package/dist/runtime/subsystems/events/event-read.protocol.js.map +1 -1
- package/dist/runtime/subsystems/events/events-errors.js +4 -11
- package/dist/runtime/subsystems/events/events-errors.js.map +1 -1
- package/dist/runtime/subsystems/events/events.module.js +15 -949
- package/dist/runtime/subsystems/events/events.module.js.map +1 -1
- package/dist/runtime/subsystems/events/events.tokens.js +10 -11
- package/dist/runtime/subsystems/events/events.tokens.js.map +1 -1
- package/dist/runtime/subsystems/events/generated/bus.js +9 -240
- package/dist/runtime/subsystems/events/generated/bus.js.map +1 -1
- package/dist/runtime/subsystems/events/generated/index.js +23 -240
- package/dist/runtime/subsystems/events/generated/index.js.map +1 -1
- package/dist/runtime/subsystems/events/generated/registry.js +5 -82
- package/dist/runtime/subsystems/events/generated/registry.js.map +1 -1
- package/dist/runtime/subsystems/events/generated/schemas.js +12 -52
- package/dist/runtime/subsystems/events/generated/schemas.js.map +1 -1
- package/dist/runtime/subsystems/events/generated/types.js +1 -0
- package/dist/runtime/subsystems/events/index.js +32 -949
- package/dist/runtime/subsystems/events/index.js.map +1 -1
- package/dist/runtime/subsystems/index.js +171 -5912
- package/dist/runtime/subsystems/index.js.map +1 -1
- package/dist/runtime/subsystems/integration/build-change-source.js +6 -178
- package/dist/runtime/subsystems/integration/build-change-source.js.map +1 -1
- package/dist/runtime/subsystems/integration/deep-equal.differ.js +4 -109
- package/dist/runtime/subsystems/integration/deep-equal.differ.js.map +1 -1
- package/dist/runtime/subsystems/integration/detection-config.schema.js +11 -78
- package/dist/runtime/subsystems/integration/detection-config.schema.js.map +1 -1
- package/dist/runtime/subsystems/integration/entity-change-source-registry.memory.js +5 -30
- package/dist/runtime/subsystems/integration/entity-change-source-registry.memory.js.map +1 -1
- package/dist/runtime/subsystems/integration/entity-change-source-registry.protocol.js +4 -9
- package/dist/runtime/subsystems/integration/entity-change-source-registry.protocol.js.map +1 -1
- package/dist/runtime/subsystems/integration/execute-integration.use-case.js +6 -239
- package/dist/runtime/subsystems/integration/execute-integration.use-case.js.map +1 -1
- package/dist/runtime/subsystems/integration/incremental-read.js +5 -144
- package/dist/runtime/subsystems/integration/incremental-read.js.map +1 -1
- package/dist/runtime/subsystems/integration/index.js +83 -1352
- package/dist/runtime/subsystems/integration/index.js.map +1 -1
- package/dist/runtime/subsystems/integration/integration-audit.schema.js +10 -155
- package/dist/runtime/subsystems/integration/integration-audit.schema.js.map +1 -1
- package/dist/runtime/subsystems/integration/integration-cursor-store.drizzle-backend.js +7 -270
- package/dist/runtime/subsystems/integration/integration-cursor-store.drizzle-backend.js.map +1 -1
- package/dist/runtime/subsystems/integration/integration-cursor-store.memory-backend.js +4 -65
- package/dist/runtime/subsystems/integration/integration-cursor-store.memory-backend.js.map +1 -1
- package/dist/runtime/subsystems/integration/integration-errors.js +5 -15
- package/dist/runtime/subsystems/integration/integration-errors.js.map +1 -1
- package/dist/runtime/subsystems/integration/integration-field-diff.protocol.js +5 -7
- package/dist/runtime/subsystems/integration/integration-field-diff.protocol.js.map +1 -1
- package/dist/runtime/subsystems/integration/integration-run-recorder.drizzle-backend.js +8 -303
- package/dist/runtime/subsystems/integration/integration-run-recorder.drizzle-backend.js.map +1 -1
- package/dist/runtime/subsystems/integration/integration-run-recorder.memory-backend.js +5 -125
- package/dist/runtime/subsystems/integration/integration-run-recorder.memory-backend.js.map +1 -1
- package/dist/runtime/subsystems/integration/integration.module.js +13 -700
- package/dist/runtime/subsystems/integration/integration.module.js.map +1 -1
- package/dist/runtime/subsystems/integration/integration.tokens.js +11 -9
- package/dist/runtime/subsystems/integration/integration.tokens.js.map +1 -1
- package/dist/runtime/subsystems/integration/loopback.middleware.js +4 -16
- package/dist/runtime/subsystems/integration/loopback.middleware.js.map +1 -1
- package/dist/runtime/subsystems/integration/poll-change-source.js +4 -89
- package/dist/runtime/subsystems/integration/poll-change-source.js.map +1 -1
- package/dist/runtime/subsystems/integration/webhook-change-source.js +4 -70
- package/dist/runtime/subsystems/integration/webhook-change-source.js.map +1 -1
- package/dist/runtime/subsystems/jobs/bullmq.config.js +9 -140
- package/dist/runtime/subsystems/jobs/bullmq.config.js.map +1 -1
- package/dist/runtime/subsystems/jobs/index.js +88 -2691
- package/dist/runtime/subsystems/jobs/index.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-handler.base.js +10 -49
- package/dist/runtime/subsystems/jobs/job-handler.base.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-orchestration.schema.js +13 -152
- package/dist/runtime/subsystems/jobs/job-orchestration.schema.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js +36 -699
- package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.js +10 -564
- package/dist/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.js +10 -824
- package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-run-keyset-cursor.js +9 -51
- package/dist/runtime/subsystems/jobs/job-run-keyset-cursor.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.js +9 -416
- package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.js +9 -290
- package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-step-service.drizzle-backend.js +5 -213
- package/dist/runtime/subsystems/jobs/job-step-service.drizzle-backend.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-step-service.memory-backend.js +5 -131
- package/dist/runtime/subsystems/jobs/job-step-service.memory-backend.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.js +9 -175
- package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-worker.js +14 -613
- package/dist/runtime/subsystems/jobs/job-worker.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-worker.module.js +23 -2647
- package/dist/runtime/subsystems/jobs/job-worker.module.js.map +1 -1
- package/dist/runtime/subsystems/jobs/jobs-domain.module.js +19 -1897
- package/dist/runtime/subsystems/jobs/jobs-domain.module.js.map +1 -1
- package/dist/runtime/subsystems/jobs/jobs-domain.tokens.js +8 -9
- package/dist/runtime/subsystems/jobs/jobs-domain.tokens.js.map +1 -1
- package/dist/runtime/subsystems/jobs/jobs-errors.js +10 -78
- package/dist/runtime/subsystems/jobs/jobs-errors.js.map +1 -1
- package/dist/runtime/subsystems/jobs/memory-job-store.js +4 -15
- package/dist/runtime/subsystems/jobs/memory-job-store.js.map +1 -1
- package/dist/runtime/subsystems/jobs/pool-config.loader.js +9 -124
- package/dist/runtime/subsystems/jobs/pool-config.loader.js.map +1 -1
- package/dist/runtime/subsystems/observability/index.js +21 -310
- package/dist/runtime/subsystems/observability/index.js.map +1 -1
- package/dist/runtime/subsystems/observability/observability-errors.js +4 -9
- package/dist/runtime/subsystems/observability/observability-errors.js.map +1 -1
- package/dist/runtime/subsystems/observability/observability.module.js +11 -300
- package/dist/runtime/subsystems/observability/observability.module.js.map +1 -1
- package/dist/runtime/subsystems/observability/observability.service.js +9 -197
- package/dist/runtime/subsystems/observability/observability.service.js.map +1 -1
- package/dist/runtime/subsystems/observability/observability.tokens.js +5 -3
- package/dist/runtime/subsystems/observability/observability.tokens.js.map +1 -1
- package/dist/runtime/subsystems/observability/reporters/bridge-metrics.reporter.js +4 -84
- package/dist/runtime/subsystems/observability/reporters/bridge-metrics.reporter.js.map +1 -1
- package/dist/runtime/subsystems/observability/reporters/index.js +5 -84
- package/dist/runtime/subsystems/observability/reporters/index.js.map +1 -1
- package/dist/runtime/subsystems/storage/index.js +15 -200
- package/dist/runtime/subsystems/storage/index.js.map +1 -1
- package/dist/runtime/subsystems/storage/storage.local-backend.js +4 -103
- package/dist/runtime/subsystems/storage/storage.local-backend.js.map +1 -1
- package/dist/runtime/subsystems/storage/storage.memory-backend.js +5 -68
- package/dist/runtime/subsystems/storage/storage.memory-backend.js.map +1 -1
- package/dist/runtime/subsystems/storage/storage.module.js +8 -200
- package/dist/runtime/subsystems/storage/storage.module.js.map +1 -1
- package/dist/runtime/subsystems/storage/storage.tokens.js +5 -6
- package/dist/runtime/subsystems/storage/storage.tokens.js.map +1 -1
- package/dist/runtime/subsystems/storage/storage.utils.js +4 -14
- package/dist/runtime/subsystems/storage/storage.utils.js.map +1 -1
- package/dist/runtime/subsystems/token-key.js +5 -3
- package/dist/runtime/subsystems/token-key.js.map +1 -1
- package/dist/src/cli/index.js +637 -5454
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/index.js +68 -4170
- package/dist/src/index.js.map +1 -1
- package/package.json +1 -1
- package/runtime/subsystems/bridge/bridge-outbox-drain-hook.ts +44 -21
- package/runtime/subsystems/jobs/job-worker.ts +17 -11
|
@@ -0,0 +1,4042 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DetectionConfigSchema
|
|
3
|
+
} from "./chunk-3NMCDN7L.js";
|
|
4
|
+
|
|
5
|
+
// src/parser/load-entities.ts
|
|
6
|
+
import { resolve as resolve2 } from "path";
|
|
7
|
+
|
|
8
|
+
// src/utils/find-yaml-files.ts
|
|
9
|
+
import { readdirSync } from "fs";
|
|
10
|
+
import { join, resolve } from "path";
|
|
11
|
+
function isYaml(name) {
|
|
12
|
+
return name.endsWith(".yaml") || name.endsWith(".yml");
|
|
13
|
+
}
|
|
14
|
+
function findYamlFiles(dir, opts) {
|
|
15
|
+
const root = resolve(dir);
|
|
16
|
+
const out = [];
|
|
17
|
+
const excluded = new Set((opts?.excludeDirs ?? []).map((d) => resolve(d)));
|
|
18
|
+
const walk = (current) => {
|
|
19
|
+
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
20
|
+
if (entry.isDirectory()) {
|
|
21
|
+
if (entry.name.startsWith(".")) continue;
|
|
22
|
+
const child = join(current, entry.name);
|
|
23
|
+
if (excluded.has(resolve(child))) continue;
|
|
24
|
+
walk(child);
|
|
25
|
+
} else if (isYaml(entry.name)) {
|
|
26
|
+
out.push(join(current, entry.name));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
walk(root);
|
|
31
|
+
return out.sort();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/utils/yaml-loader.ts
|
|
35
|
+
import { readFileSync, existsSync } from "fs";
|
|
36
|
+
import { parse as parseYaml } from "yaml";
|
|
37
|
+
|
|
38
|
+
// src/schema/entity-definition.schema.ts
|
|
39
|
+
import { z } from "zod";
|
|
40
|
+
var FieldTypeSchema = z.enum([
|
|
41
|
+
"string",
|
|
42
|
+
"integer",
|
|
43
|
+
"decimal",
|
|
44
|
+
"boolean",
|
|
45
|
+
"uuid",
|
|
46
|
+
"date",
|
|
47
|
+
"datetime",
|
|
48
|
+
"json",
|
|
49
|
+
"entity_ref",
|
|
50
|
+
// Polymorphic reference: generates {field}EntityType + {field}EntityId columns
|
|
51
|
+
"string_array",
|
|
52
|
+
// Array of strings: generates text[] column
|
|
53
|
+
"enum"
|
|
54
|
+
// Enum type with choices or choices_from
|
|
55
|
+
]);
|
|
56
|
+
var UiTypeSchema = z.enum([
|
|
57
|
+
"text",
|
|
58
|
+
"textarea",
|
|
59
|
+
"number",
|
|
60
|
+
"money",
|
|
61
|
+
"percentage",
|
|
62
|
+
"email",
|
|
63
|
+
"url",
|
|
64
|
+
"date",
|
|
65
|
+
"datetime",
|
|
66
|
+
"boolean",
|
|
67
|
+
"enum",
|
|
68
|
+
"reference",
|
|
69
|
+
"json",
|
|
70
|
+
"badge",
|
|
71
|
+
"password"
|
|
72
|
+
]);
|
|
73
|
+
var UiImportanceSchema = z.enum(["primary", "secondary", "tertiary"]);
|
|
74
|
+
var AnalyticsAggregationSchema = z.enum([
|
|
75
|
+
"sum",
|
|
76
|
+
"min",
|
|
77
|
+
"max",
|
|
78
|
+
"count",
|
|
79
|
+
"count_distinct",
|
|
80
|
+
"average",
|
|
81
|
+
"median",
|
|
82
|
+
"percentile",
|
|
83
|
+
"sum_boolean"
|
|
84
|
+
]);
|
|
85
|
+
var AnalyticsDimensionTypeSchema = z.enum(["categorical", "time"]);
|
|
86
|
+
var AnalyticsEntityTypeSchema = z.enum(["primary", "unique", "foreign", "natural"]);
|
|
87
|
+
var AnalyticsTimeGranularitySchema = z.enum(["day", "week", "month", "quarter", "year"]);
|
|
88
|
+
var AnalyticsVisibilitySchema = z.enum(["internal", "agent", "public"]);
|
|
89
|
+
var NonAdditiveDimensionSchema = z.union([
|
|
90
|
+
z.string(),
|
|
91
|
+
z.object({
|
|
92
|
+
name: z.string(),
|
|
93
|
+
window_choice: z.string().optional(),
|
|
94
|
+
window_groupings: z.array(z.string()).optional()
|
|
95
|
+
})
|
|
96
|
+
]);
|
|
97
|
+
var SemanticMetadataSchema = z.object({
|
|
98
|
+
measure: z.boolean().optional(),
|
|
99
|
+
analytics_aggregation: AnalyticsAggregationSchema.optional(),
|
|
100
|
+
agg_time_dimension: z.string().optional(),
|
|
101
|
+
non_additive_dimension: NonAdditiveDimensionSchema.optional(),
|
|
102
|
+
dimension: z.boolean().optional(),
|
|
103
|
+
dimension_type: AnalyticsDimensionTypeSchema.optional(),
|
|
104
|
+
time_granularity: AnalyticsTimeGranularitySchema.optional(),
|
|
105
|
+
is_partition: z.boolean().optional(),
|
|
106
|
+
entity: z.boolean().optional(),
|
|
107
|
+
entity_type: AnalyticsEntityTypeSchema.optional(),
|
|
108
|
+
entity_role: z.string().optional(),
|
|
109
|
+
analytics_visibility: AnalyticsVisibilitySchema.optional(),
|
|
110
|
+
semantic_expr: z.string().optional(),
|
|
111
|
+
semantic_label: z.string().optional()
|
|
112
|
+
});
|
|
113
|
+
var UiMetadataSchema = z.object({
|
|
114
|
+
ui_label: z.string().optional(),
|
|
115
|
+
ui_type: UiTypeSchema.optional(),
|
|
116
|
+
ui_importance: UiImportanceSchema.optional(),
|
|
117
|
+
ui_group: z.string().optional(),
|
|
118
|
+
ui_sortable: z.boolean().optional(),
|
|
119
|
+
ui_filterable: z.boolean().optional(),
|
|
120
|
+
ui_visible: z.boolean().optional(),
|
|
121
|
+
ui_placeholder: z.string().optional(),
|
|
122
|
+
ui_help: z.string().optional(),
|
|
123
|
+
ui_format: z.record(z.unknown()).optional()
|
|
124
|
+
});
|
|
125
|
+
var BaseFieldSchema = z.object({
|
|
126
|
+
type: FieldTypeSchema,
|
|
127
|
+
required: z.boolean().optional().default(false),
|
|
128
|
+
nullable: z.boolean().optional().default(false),
|
|
129
|
+
// String constraints
|
|
130
|
+
max_length: z.number().int().positive().optional(),
|
|
131
|
+
min_length: z.number().int().nonnegative().optional(),
|
|
132
|
+
// Numeric constraints
|
|
133
|
+
min: z.number().optional(),
|
|
134
|
+
max: z.number().optional(),
|
|
135
|
+
// Enum/choices (inline definition)
|
|
136
|
+
choices: z.array(z.string()).optional(),
|
|
137
|
+
// Enum/choices from external file (e.g., "relationship_types.yaml")
|
|
138
|
+
// Mutually exclusive with choices - parser loads file and extracts keys
|
|
139
|
+
choices_from: z.string().optional(),
|
|
140
|
+
// Entity reference: allowed entity types for polymorphic refs
|
|
141
|
+
// Required when type is 'entity_ref'
|
|
142
|
+
allowed_types: z.array(z.string()).optional(),
|
|
143
|
+
// Default value
|
|
144
|
+
default: z.unknown().optional(),
|
|
145
|
+
// Indexing
|
|
146
|
+
index: z.boolean().optional(),
|
|
147
|
+
unique: z.boolean().optional(),
|
|
148
|
+
// Foreign key reference (e.g., "accounts.id")
|
|
149
|
+
foreign_key: z.string().optional()
|
|
150
|
+
});
|
|
151
|
+
var FieldDefinitionSchema = BaseFieldSchema.merge(UiMetadataSchema).merge(SemanticMetadataSchema).refine((data) => !(data.required === true && data.nullable === true), {
|
|
152
|
+
message: "'required: true' and 'nullable: true' cannot both be set. A required field cannot be null.",
|
|
153
|
+
path: ["required"]
|
|
154
|
+
}).refine(
|
|
155
|
+
(data) => {
|
|
156
|
+
if (data.min_length !== void 0 && data.type !== "string") {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
return true;
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
message: "'min_length' can only be used with type 'string'",
|
|
163
|
+
path: ["min_length"]
|
|
164
|
+
}
|
|
165
|
+
).refine(
|
|
166
|
+
(data) => {
|
|
167
|
+
if (data.max_length !== void 0 && data.type !== "string") {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
return true;
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
message: "'max_length' can only be used with type 'string'",
|
|
174
|
+
path: ["max_length"]
|
|
175
|
+
}
|
|
176
|
+
).refine(
|
|
177
|
+
(data) => {
|
|
178
|
+
if (data.min !== void 0 && !["integer", "decimal"].includes(data.type)) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
return true;
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
message: "'min' can only be used with numeric types",
|
|
185
|
+
path: ["min"]
|
|
186
|
+
}
|
|
187
|
+
).refine(
|
|
188
|
+
(data) => {
|
|
189
|
+
if (data.max !== void 0 && !["integer", "decimal"].includes(data.type)) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
return true;
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
message: "'max' can only be used with numeric types",
|
|
196
|
+
path: ["max"]
|
|
197
|
+
}
|
|
198
|
+
).refine(
|
|
199
|
+
(data) => {
|
|
200
|
+
if (data.type === "entity_ref" && !data.allowed_types?.length) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
return true;
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
message: "'entity_ref' type requires 'allowed_types' to be specified",
|
|
207
|
+
path: ["allowed_types"]
|
|
208
|
+
}
|
|
209
|
+
).refine(
|
|
210
|
+
(data) => {
|
|
211
|
+
if (data.allowed_types !== void 0 && data.type !== "entity_ref") {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
return true;
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
message: "'allowed_types' can only be used with type 'entity_ref'",
|
|
218
|
+
path: ["allowed_types"]
|
|
219
|
+
}
|
|
220
|
+
).refine(
|
|
221
|
+
(data) => {
|
|
222
|
+
if (data.choices !== void 0 && data.choices_from !== void 0) {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
return true;
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
message: "'choices' and 'choices_from' cannot both be specified",
|
|
229
|
+
path: ["choices_from"]
|
|
230
|
+
}
|
|
231
|
+
).refine(
|
|
232
|
+
(data) => {
|
|
233
|
+
if (data.type === "enum" && !data.choices?.length && !data.choices_from) {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
return true;
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
message: "'enum' type requires either 'choices' or 'choices_from'",
|
|
240
|
+
path: ["choices"]
|
|
241
|
+
}
|
|
242
|
+
).refine(
|
|
243
|
+
(data) => {
|
|
244
|
+
if (data.measure === true && !data.analytics_aggregation) {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
return true;
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
message: "When 'measure' is true, 'analytics_aggregation' must be specified",
|
|
251
|
+
path: ["analytics_aggregation"]
|
|
252
|
+
}
|
|
253
|
+
);
|
|
254
|
+
var RelationshipTypeSchema = z.enum(["belongs_to", "has_many", "has_one"]);
|
|
255
|
+
var OnDeleteSchema = z.enum(["restrict", "cascade", "set_null", "no_action"]);
|
|
256
|
+
var RelationshipSchema = z.object({
|
|
257
|
+
type: RelationshipTypeSchema,
|
|
258
|
+
target: z.string(),
|
|
259
|
+
// Target entity name (e.g., "account")
|
|
260
|
+
foreign_key: z.string(),
|
|
261
|
+
// FK field name (e.g., "account_id")
|
|
262
|
+
through: z.string().optional(),
|
|
263
|
+
// For transitive: "owned_opportunities.updates"
|
|
264
|
+
inverse: z.string().optional(),
|
|
265
|
+
// Name of inverse relationship on target entity
|
|
266
|
+
nullable: z.boolean().optional(),
|
|
267
|
+
// Whether the FK column allows NULL
|
|
268
|
+
on_delete: OnDeleteSchema.optional().default("restrict")
|
|
269
|
+
// FK cascade action (belongs_to only; hard-delete only)
|
|
270
|
+
}).strict().refine(
|
|
271
|
+
(data) => {
|
|
272
|
+
if (data.on_delete === "set_null" && data.nullable !== true) {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
return true;
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
message: "'on_delete: set_null' requires 'nullable: true' \u2014 Postgres cannot null a NOT NULL FK column.",
|
|
279
|
+
path: ["on_delete"]
|
|
280
|
+
}
|
|
281
|
+
);
|
|
282
|
+
var BehaviorConfigSchema = z.union([
|
|
283
|
+
z.string(),
|
|
284
|
+
z.object({
|
|
285
|
+
name: z.string(),
|
|
286
|
+
options: z.record(z.unknown()).optional()
|
|
287
|
+
})
|
|
288
|
+
]);
|
|
289
|
+
var BehaviorStrategySchema = z.enum(["base_class", "inline"]);
|
|
290
|
+
var FolderStructureSchema = z.enum(["nested", "flat"]).default("nested");
|
|
291
|
+
var FileGroupingSchema = z.enum(["separate", "grouped"]).default("separate");
|
|
292
|
+
var ExposeLayerSchema = z.enum(["repository", "rest", "trpc", "electric"]);
|
|
293
|
+
var EntityConfigSchema = z.object({
|
|
294
|
+
name: z.string().regex(
|
|
295
|
+
/^[a-z][a-z0-9_]*$/,
|
|
296
|
+
"Entity name must be lowercase with underscores (e.g., 'opportunity')"
|
|
297
|
+
),
|
|
298
|
+
plural: z.string().regex(/^[a-z][a-z0-9_]*$/, "Plural must be lowercase"),
|
|
299
|
+
table: z.string().regex(/^[a-z][a-z0-9_]*$/, "Table must be lowercase"),
|
|
300
|
+
// Layout options (orthogonal concerns)
|
|
301
|
+
// folder_structure: controls directory nesting
|
|
302
|
+
// file_grouping: controls file organization
|
|
303
|
+
folder_structure: FolderStructureSchema.optional(),
|
|
304
|
+
file_grouping: FileGroupingSchema.optional(),
|
|
305
|
+
// Per-entity behavior strategy override (overrides codegen.config.yaml)
|
|
306
|
+
behavior_strategy: BehaviorStrategySchema.optional(),
|
|
307
|
+
// Which layers to generate (default: all)
|
|
308
|
+
expose: z.array(ExposeLayerSchema).optional().default(["repository", "rest", "trpc"]),
|
|
309
|
+
// App-defined patterns (ADR-031, supersedes ADR-005 family:).
|
|
310
|
+
// `pattern:` and `patterns:` are mutually exclusive — use `pattern:`
|
|
311
|
+
// for a single pattern and `patterns:` for multi-pattern composition.
|
|
312
|
+
// Pattern names resolve against the registry at codegen time.
|
|
313
|
+
pattern: z.string().optional(),
|
|
314
|
+
patterns: z.array(z.string()).optional(),
|
|
315
|
+
// Per-pattern config, keyed by pattern name. Each value is validated
|
|
316
|
+
// against the pattern's `configSchema` in composition validation
|
|
317
|
+
// (src/patterns/validate-composition.ts — PATTERN-4).
|
|
318
|
+
config: z.record(z.string(), z.unknown()).optional(),
|
|
319
|
+
// JOB-7: marks this entity as a valid scope target for job scoping.
|
|
320
|
+
// Drives the generated ScopeEntityType union in
|
|
321
|
+
// runtime/subsystems/jobs/generated/scope-entity-type.ts.
|
|
322
|
+
scopeable: z.boolean().optional(),
|
|
323
|
+
// RFC-0001 §1/§8: the integration *surface* this entity belongs to
|
|
324
|
+
// (e.g. 'calendar', 'mail', 'crm'). Surfaces span provider contexts
|
|
325
|
+
// (ADR-0006) — one Google OAuth feeds calendar+mail+transcript. The union
|
|
326
|
+
// of `surface:` values across all entity YAML is the closed set that a
|
|
327
|
+
// provider's `surfaces:` must be a subset of (cross-checked in
|
|
328
|
+
// src/parser/validate-providers.ts). Optional: entities without an
|
|
329
|
+
// integration surface omit it. The surface-package *emission* convention
|
|
330
|
+
// is Track C (#329); this field is only the declarative input both tracks
|
|
331
|
+
// read. Lives inside the `entity:` block (next to `pattern:`/`name:`/`table:`).
|
|
332
|
+
surface: z.string().optional(),
|
|
333
|
+
// Bounded-context declaration (ADR-0004) — "which bounded context this
|
|
334
|
+
// entity belongs to". This is the DURABLE decision; it is a plain
|
|
335
|
+
// bounded-context slug, NOT a folder knob. Different features consume it:
|
|
336
|
+
//
|
|
337
|
+
// - #403 (the FIRST consumer): drives the generated code's
|
|
338
|
+
// module output folder. clean-lite-ps nests the entity's module under
|
|
339
|
+
// `<modules>/<context>/<entity>/` so same-context entities group
|
|
340
|
+
// together; untagged entities stay flat (`<modules>/<entity>/`).
|
|
341
|
+
// - ADR-0004 (deferred): a later `naming: prefix | schema` knob reads
|
|
342
|
+
// this SAME field to drive the Postgres physical layout —
|
|
343
|
+
// `prefix` → `pgTable('<context>__<table>')`, then the flip to
|
|
344
|
+
// `schema` → `pgSchema('<context>').table('<table>')`. NOT wired here.
|
|
345
|
+
//
|
|
346
|
+
// Sibling to `surface:` and orthogonal to it (ADR-0006): context = model
|
|
347
|
+
// cohesion (which domain), surface = vendor composition (which integration).
|
|
348
|
+
// Lives inside the `entity:` block (next to `pattern:`/`name:`/`table:`).
|
|
349
|
+
context: z.string().regex(
|
|
350
|
+
/^[a-z][a-z0-9_]*$/,
|
|
351
|
+
"context must be lowercase snake_case (e.g. 'integration')"
|
|
352
|
+
).optional()
|
|
353
|
+
}).strict().refine((d) => !(d.pattern && d.patterns), {
|
|
354
|
+
message: "'pattern' and 'patterns' are mutually exclusive"
|
|
355
|
+
});
|
|
356
|
+
var QueryDeclarationSchema = z.object({
|
|
357
|
+
by: z.array(z.string()).min(1),
|
|
358
|
+
unique: z.boolean().optional(),
|
|
359
|
+
select: z.array(z.string()).optional(),
|
|
360
|
+
order: z.string().optional(),
|
|
361
|
+
limit: z.boolean().optional(),
|
|
362
|
+
via: z.string().optional()
|
|
363
|
+
});
|
|
364
|
+
var SearchQueryDeclarationSchema = z.object({
|
|
365
|
+
name: z.literal("search"),
|
|
366
|
+
filters: z.array(z.string()).min(1),
|
|
367
|
+
search: z.string().optional(),
|
|
368
|
+
paginate: z.boolean().optional().default(true),
|
|
369
|
+
order: z.string().optional()
|
|
370
|
+
});
|
|
371
|
+
var AnyQueryDeclarationSchema = z.union([
|
|
372
|
+
SearchQueryDeclarationSchema,
|
|
373
|
+
QueryDeclarationSchema
|
|
374
|
+
]);
|
|
375
|
+
var IntegrationDirectionSchema = z.enum([
|
|
376
|
+
"inbound",
|
|
377
|
+
"outbound",
|
|
378
|
+
"bidirectional"
|
|
379
|
+
]);
|
|
380
|
+
var ProviderIntegrationSchema = z.object({
|
|
381
|
+
remote_entity: z.string(),
|
|
382
|
+
direction: IntegrationDirectionSchema,
|
|
383
|
+
cdc: z.boolean().optional().default(false),
|
|
384
|
+
field_mapping: z.record(z.string(), z.string()).optional(),
|
|
385
|
+
read_only_fields: z.array(z.string()).optional()
|
|
386
|
+
});
|
|
387
|
+
var IntegrationConfigSchema = z.object({
|
|
388
|
+
electric: z.boolean().optional().default(false),
|
|
389
|
+
providers: z.record(z.string(), ProviderIntegrationSchema).optional()
|
|
390
|
+
});
|
|
391
|
+
var EventDeclarationSchema = z.object({
|
|
392
|
+
name: z.string().regex(/^[a-z][a-z0-9_]*$/, "Event name must be snake_case"),
|
|
393
|
+
queue: z.string(),
|
|
394
|
+
body: z.record(z.string(), z.string()),
|
|
395
|
+
generate_handler: z.boolean().optional().default(false)
|
|
396
|
+
});
|
|
397
|
+
var SimpleMetricSchema = z.object({
|
|
398
|
+
type: z.literal("simple"),
|
|
399
|
+
measure: z.string(),
|
|
400
|
+
agg: AnalyticsAggregationSchema.optional(),
|
|
401
|
+
filter: z.string().optional(),
|
|
402
|
+
description: z.string().optional(),
|
|
403
|
+
label: z.string().optional()
|
|
404
|
+
});
|
|
405
|
+
var DerivedMetricSchema = z.object({
|
|
406
|
+
type: z.literal("derived"),
|
|
407
|
+
expr: z.string(),
|
|
408
|
+
metrics: z.array(z.string()),
|
|
409
|
+
description: z.string().optional(),
|
|
410
|
+
label: z.string().optional()
|
|
411
|
+
});
|
|
412
|
+
var RatioMetricSchema = z.object({
|
|
413
|
+
type: z.literal("ratio"),
|
|
414
|
+
numerator: z.union([z.string(), SimpleMetricSchema]),
|
|
415
|
+
denominator: z.union([z.string(), SimpleMetricSchema]),
|
|
416
|
+
filter: z.string().optional(),
|
|
417
|
+
description: z.string().optional(),
|
|
418
|
+
label: z.string().optional()
|
|
419
|
+
});
|
|
420
|
+
var CumulativeMetricSchema = z.object({
|
|
421
|
+
type: z.literal("cumulative"),
|
|
422
|
+
measure: z.string(),
|
|
423
|
+
window: z.string().optional(),
|
|
424
|
+
grain_to_date: AnalyticsTimeGranularitySchema.optional(),
|
|
425
|
+
description: z.string().optional(),
|
|
426
|
+
label: z.string().optional()
|
|
427
|
+
});
|
|
428
|
+
var MetricDefinitionSchema = z.discriminatedUnion("type", [
|
|
429
|
+
SimpleMetricSchema,
|
|
430
|
+
DerivedMetricSchema,
|
|
431
|
+
RatioMetricSchema,
|
|
432
|
+
CumulativeMetricSchema
|
|
433
|
+
]);
|
|
434
|
+
var AnalyticsBlockSchema = z.object({
|
|
435
|
+
measure_packs: z.array(z.string()).optional(),
|
|
436
|
+
cube_name: z.string().optional(),
|
|
437
|
+
metrics: z.record(z.string(), MetricDefinitionSchema).optional()
|
|
438
|
+
});
|
|
439
|
+
var GenerateConfigSchema = z.object({
|
|
440
|
+
writes: z.boolean().optional().default(true)
|
|
441
|
+
}).strict();
|
|
442
|
+
var EntityDefinitionSchema = z.object({
|
|
443
|
+
entity: EntityConfigSchema,
|
|
444
|
+
fields: z.record(z.string(), FieldDefinitionSchema),
|
|
445
|
+
relationships: z.record(z.string(), RelationshipSchema).optional(),
|
|
446
|
+
// Behaviors add cross-cutting concerns (timestamps, soft_delete, user_tracking, etc.)
|
|
447
|
+
behaviors: z.array(BehaviorConfigSchema).optional().default([]),
|
|
448
|
+
// Per-entity generation toggles (e.g. disable write-side emission)
|
|
449
|
+
generate: GenerateConfigSchema.optional(),
|
|
450
|
+
// EAV (entity-attribute-value) dual-write + paired reads (ADR-13).
|
|
451
|
+
// When `true`, codegen emits:
|
|
452
|
+
// - FindXWithFieldsUseCase + ListXWithFieldsUseCase (paired reads)
|
|
453
|
+
// - CreateX / UpdateX use cases in transactional compound-write shape
|
|
454
|
+
// (composes entity service + FieldValueService in one db.transaction,
|
|
455
|
+
// splits `{ fields, ...core }` from the DTO)
|
|
456
|
+
// - GET /:id/with-fields + GET /with-fields controller routes
|
|
457
|
+
// - Service with injected FieldValueRepository + findByIdWithFields /
|
|
458
|
+
// listWithFields paired read methods
|
|
459
|
+
//
|
|
460
|
+
// Consumer contract (must be in place before regen):
|
|
461
|
+
// - BaseService.create/update/delete accept optional `tx` parameter
|
|
462
|
+
// - `@shared/eav-helpers` exports `toEavRows(entityId, entityType, fields)`
|
|
463
|
+
// and `mergeEavRows(rows)`
|
|
464
|
+
// - FieldValueService exposes `upsertMany(rows, tx?)` (inherited from
|
|
465
|
+
// MetadataEntityService)
|
|
466
|
+
// - DRIZZLE_DB injection token available via `@shared/constants/tokens`
|
|
467
|
+
//
|
|
468
|
+
// Defaults to `false` — opt in per entity that needs dynamic/custom fields.
|
|
469
|
+
eav: z.boolean().optional().default(false),
|
|
470
|
+
// Declare this entity IS an EAV value table. When `true`, codegen emits
|
|
471
|
+
// the compound EAV methods (upsertFieldsTransactional, findMergedByEntity)
|
|
472
|
+
// on the service and the upsertCurrentValues method on the repository —
|
|
473
|
+
// no hand extension required. Companion flag `eav_definition_table`
|
|
474
|
+
// identifies the sibling entity that stores the field-key ↔ id lookup.
|
|
475
|
+
//
|
|
476
|
+
// Mutually exclusive with `eav: true` in practice — a value table holds
|
|
477
|
+
// OTHER entities' dynamic fields, it isn't itself an EAV-opt-in entity.
|
|
478
|
+
//
|
|
479
|
+
// Assumption (v1): value tables have a `user_id` column. Future
|
|
480
|
+
// `eav_user_scoped: false` flag will relax this for audit/system EAV.
|
|
481
|
+
eav_value_table: z.boolean().optional().default(false),
|
|
482
|
+
// Singular entity name of the field-definitions entity that pairs with
|
|
483
|
+
// this value table (matches the `target:` convention in relationship
|
|
484
|
+
// YAMLs). Required when `eav_value_table: true`; ignored otherwise.
|
|
485
|
+
eav_definition_table: z.string().optional(),
|
|
486
|
+
// v2: Declarative query generation (ADR-005)
|
|
487
|
+
// Generates repository + service + use case methods from declarations
|
|
488
|
+
queries: z.array(AnyQueryDeclarationSchema).optional(),
|
|
489
|
+
// v2: Integration integration configuration (CODEGEN-EVOLUTION-PLAN Phase 2)
|
|
490
|
+
// Electric SQL + provider integration (Salesforce, HubSpot, etc.)
|
|
491
|
+
integration: IntegrationConfigSchema.optional(),
|
|
492
|
+
// ADR-033.1: Provider-keyed change-source detection.
|
|
493
|
+
//
|
|
494
|
+
// Map of provider name → DetectionConfig. Single-provider entities use
|
|
495
|
+
// a one-key map; multi-provider entities list each provider as a
|
|
496
|
+
// separate key. Each value is an independent `DetectionConfig` (its
|
|
497
|
+
// own mode/cursor/mapping/filters) sourced from the canonical schema
|
|
498
|
+
// in `runtime/subsystems/integration` so this validator and the runtime
|
|
499
|
+
// parser stay in lockstep.
|
|
500
|
+
//
|
|
501
|
+
// Within-file cross-check (ADR-033.1 §6): every key here must also
|
|
502
|
+
// appear in `integration.providers` — see the superRefine on
|
|
503
|
+
// `EntityDefinitionSchema` below.
|
|
504
|
+
detection: z.record(z.string(), DetectionConfigSchema).optional(),
|
|
505
|
+
// NOTE: `surface:` and `context:` moved INTO EntityConfigSchema (the
|
|
506
|
+
// `entity:` block) in 0.12.2 — consumers write them next to
|
|
507
|
+
// `pattern:`/`name:`/`table:`, which is the natural place. They are
|
|
508
|
+
// read via `entity.surface` / `entity.context`. Clean break: no
|
|
509
|
+
// root-level placement is accepted.
|
|
510
|
+
// v2: Domain event declarations (CODEGEN-EVOLUTION-PLAN Phase 2)
|
|
511
|
+
// Generates typed event classes, handlers, and queue registration
|
|
512
|
+
events: z.array(EventDeclarationSchema).optional(),
|
|
513
|
+
// EVT-7: Opt-in typed event emission. Each entry names an `EventDefinition`
|
|
514
|
+
// (top-level `events/<type>.yaml` or an inline `events:` block entry) that
|
|
515
|
+
// the generated use-cases should publish via `TypedEventBus.publish(...)`
|
|
516
|
+
// inside a Drizzle transaction.
|
|
517
|
+
//
|
|
518
|
+
// emits: [contact_created, contact_updated]
|
|
519
|
+
//
|
|
520
|
+
// Cross-validated in `validateEntityEmits()` against the merged event
|
|
521
|
+
// registry. `undefined` ⇒ fallback to untyped lifecycle-events + warning;
|
|
522
|
+
// `[]` ⇒ explicit opt-out, no warning, no typed emission.
|
|
523
|
+
emits: z.array(
|
|
524
|
+
z.string().regex(/^[a-z][a-z0-9_]*$/, "emits entries must be snake_case event type names")
|
|
525
|
+
).optional(),
|
|
526
|
+
// v2: Analytics / semantic layer configuration
|
|
527
|
+
// Cube.js measure packs, custom cube name, and metric definitions
|
|
528
|
+
analytics: AnalyticsBlockSchema.optional(),
|
|
529
|
+
// Composite (multi-column) unique indexes (#356). Single-column uniqueness
|
|
530
|
+
// is `unique: true` on the field itself; this declares constraints that
|
|
531
|
+
// span 2+ columns, e.g. UNIQUE (conversation_id, sequence). Emitted as a
|
|
532
|
+
// `uniqueIndex(...).on(...)` entry in the generated entity's pgTable
|
|
533
|
+
// extra-config callback. `name` defaults to <table>_<col1>_<col2>_uniq.
|
|
534
|
+
unique_indexes: z.array(
|
|
535
|
+
z.object({
|
|
536
|
+
fields: z.array(z.string()).min(2, "unique_indexes entries span 2+ columns \u2014 use `unique: true` on the field for single-column uniqueness"),
|
|
537
|
+
name: z.string().optional()
|
|
538
|
+
}).strict()
|
|
539
|
+
).optional()
|
|
540
|
+
}).strict().refine(
|
|
541
|
+
(data) => !data.eav_value_table || typeof data.eav_definition_table === "string",
|
|
542
|
+
{
|
|
543
|
+
message: "`eav_definition_table` is required when `eav_value_table: true` \u2014 declare the singular entity name of the paired field-definitions entity (e.g. `eav_definition_table: 'field_definition'`).",
|
|
544
|
+
path: ["eav_definition_table"]
|
|
545
|
+
}
|
|
546
|
+
).superRefine((entity, ctx) => {
|
|
547
|
+
if (!entity.detection) return;
|
|
548
|
+
const declared = new Set(Object.keys(entity.integration?.providers ?? {}));
|
|
549
|
+
for (const provider of Object.keys(entity.detection)) {
|
|
550
|
+
if (!declared.has(provider)) {
|
|
551
|
+
ctx.addIssue({
|
|
552
|
+
code: "custom",
|
|
553
|
+
path: ["detection", provider],
|
|
554
|
+
message: `Provider '${provider}' used in detection: but not declared in integration.providers. Known providers: ${[...declared].join(", ")}`
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
// src/schema/event-definition.schema.ts
|
|
561
|
+
import { z as z2 } from "zod";
|
|
562
|
+
var EVENT_DIRECTIONS = ["inbound", "change", "outbound"];
|
|
563
|
+
var EVENT_TIERS = ["domain", "audit"];
|
|
564
|
+
var EVENT_FIELD_TYPES = [
|
|
565
|
+
"uuid",
|
|
566
|
+
"string",
|
|
567
|
+
"number",
|
|
568
|
+
"boolean",
|
|
569
|
+
"date",
|
|
570
|
+
"json",
|
|
571
|
+
"array"
|
|
572
|
+
];
|
|
573
|
+
var EVENT_ARRAY_ITEM_TYPES = [
|
|
574
|
+
"uuid",
|
|
575
|
+
"string",
|
|
576
|
+
"number",
|
|
577
|
+
"boolean",
|
|
578
|
+
"date"
|
|
579
|
+
];
|
|
580
|
+
var RESERVED_EVENT_POOLS = [
|
|
581
|
+
"events_inbound",
|
|
582
|
+
"events_change",
|
|
583
|
+
"events_outbound"
|
|
584
|
+
];
|
|
585
|
+
var EVENT_BACKOFF_STRATEGIES = ["linear", "exponential"];
|
|
586
|
+
var DIRECTION_TO_POOL = {
|
|
587
|
+
inbound: "events_inbound",
|
|
588
|
+
change: "events_change",
|
|
589
|
+
outbound: "events_outbound"
|
|
590
|
+
};
|
|
591
|
+
var EventDirectionSchema = z2.enum(EVENT_DIRECTIONS);
|
|
592
|
+
var EventTierSchema = z2.enum(EVENT_TIERS);
|
|
593
|
+
var EventFieldTypeSchema = z2.enum(EVENT_FIELD_TYPES);
|
|
594
|
+
var EventArrayItemTypeSchema = z2.enum(EVENT_ARRAY_ITEM_TYPES);
|
|
595
|
+
var EventPoolSchema = z2.enum(RESERVED_EVENT_POOLS);
|
|
596
|
+
var EventPayloadFieldSchema = z2.object({
|
|
597
|
+
type: EventFieldTypeSchema,
|
|
598
|
+
items: EventArrayItemTypeSchema.optional(),
|
|
599
|
+
nullable: z2.boolean().optional().default(false),
|
|
600
|
+
description: z2.string().optional()
|
|
601
|
+
}).strict().superRefine((data, ctx) => {
|
|
602
|
+
if (data.type === "array" && data.items === void 0) {
|
|
603
|
+
ctx.addIssue({
|
|
604
|
+
code: z2.ZodIssueCode.custom,
|
|
605
|
+
message: "'items' is required when type is 'array'",
|
|
606
|
+
path: ["items"]
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
if (data.type !== "array" && data.items !== void 0) {
|
|
610
|
+
ctx.addIssue({
|
|
611
|
+
code: z2.ZodIssueCode.custom,
|
|
612
|
+
message: `'items' is only valid when type is 'array' (got '${data.type}')`,
|
|
613
|
+
path: ["items"]
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
var RetrySchema = z2.object({
|
|
618
|
+
attempts: z2.number().int().min(0).max(20),
|
|
619
|
+
backoff: z2.enum(EVENT_BACKOFF_STRATEGIES)
|
|
620
|
+
}).strict();
|
|
621
|
+
var SNAKE_CASE_RE = /^[a-z][a-z0-9_]*$/;
|
|
622
|
+
var EventDefinitionSchemaCore = z2.object({
|
|
623
|
+
type: z2.string().regex(
|
|
624
|
+
SNAKE_CASE_RE,
|
|
625
|
+
"Event type must be snake_case starting with a letter"
|
|
626
|
+
),
|
|
627
|
+
tier: EventTierSchema.optional().default("domain"),
|
|
628
|
+
direction: EventDirectionSchema.optional(),
|
|
629
|
+
pool: EventPoolSchema.optional(),
|
|
630
|
+
aggregate: z2.string().regex(SNAKE_CASE_RE).optional(),
|
|
631
|
+
source: z2.string().min(1).optional(),
|
|
632
|
+
destination: z2.string().min(1).optional(),
|
|
633
|
+
payload: z2.record(
|
|
634
|
+
z2.string().regex(SNAKE_CASE_RE, "Payload keys must be snake_case"),
|
|
635
|
+
EventPayloadFieldSchema
|
|
636
|
+
).default({}),
|
|
637
|
+
retry: RetrySchema.optional().default({
|
|
638
|
+
attempts: 3,
|
|
639
|
+
backoff: "exponential"
|
|
640
|
+
}),
|
|
641
|
+
version: z2.number().int().min(1).optional().default(1),
|
|
642
|
+
description: z2.string().optional()
|
|
643
|
+
}).strict();
|
|
644
|
+
var EventDefinitionSchemaRefined = EventDefinitionSchemaCore.superRefine(
|
|
645
|
+
(data, ctx) => {
|
|
646
|
+
if (data.tier === "audit") {
|
|
647
|
+
if (data.pool !== void 0) {
|
|
648
|
+
ctx.addIssue({
|
|
649
|
+
code: z2.ZodIssueCode.custom,
|
|
650
|
+
message: `Event '${data.type}' is tier:audit; pool MUST be omitted (got '${data.pool}'). Audit events have no pool. See ai-docs/specs/issue-242/plan.md \xA7AUDIT-2.`,
|
|
651
|
+
path: ["pool"]
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
if (data.direction !== void 0) {
|
|
655
|
+
ctx.addIssue({
|
|
656
|
+
code: z2.ZodIssueCode.custom,
|
|
657
|
+
message: `Event '${data.type}' is tier:audit; direction MUST be omitted (got '${data.direction}'). Audit events have no direction. See ai-docs/specs/issue-242/plan.md \xA7AUDIT-2.`,
|
|
658
|
+
path: ["direction"]
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
if (data.direction === void 0) {
|
|
664
|
+
ctx.addIssue({
|
|
665
|
+
code: z2.ZodIssueCode.custom,
|
|
666
|
+
message: "'direction' is required when tier is 'domain'",
|
|
667
|
+
path: ["direction"]
|
|
668
|
+
});
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
if (data.direction === "change" && !data.aggregate) {
|
|
672
|
+
ctx.addIssue({
|
|
673
|
+
code: z2.ZodIssueCode.custom,
|
|
674
|
+
message: "'aggregate' is required when direction is 'change'",
|
|
675
|
+
path: ["aggregate"]
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
if (data.source !== void 0 && data.direction !== "inbound") {
|
|
679
|
+
ctx.addIssue({
|
|
680
|
+
code: z2.ZodIssueCode.custom,
|
|
681
|
+
message: `'source' is only valid when direction is 'inbound' (got '${data.direction}')`,
|
|
682
|
+
path: ["source"]
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
if (data.destination !== void 0 && data.direction !== "outbound") {
|
|
686
|
+
ctx.addIssue({
|
|
687
|
+
code: z2.ZodIssueCode.custom,
|
|
688
|
+
message: `'destination' is only valid when direction is 'outbound' (got '${data.direction}')`,
|
|
689
|
+
path: ["destination"]
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
if (data.pool !== void 0) {
|
|
693
|
+
const expected = DIRECTION_TO_POOL[data.direction];
|
|
694
|
+
if (data.pool !== expected) {
|
|
695
|
+
ctx.addIssue({
|
|
696
|
+
code: z2.ZodIssueCode.custom,
|
|
697
|
+
message: `pool '${data.pool}' is inconsistent with direction '${data.direction}' (expected '${expected}')`,
|
|
698
|
+
path: ["pool"]
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
);
|
|
704
|
+
var EventDefinitionSchema = EventDefinitionSchemaRefined.transform(
|
|
705
|
+
(parsed) => {
|
|
706
|
+
if (parsed.tier === "audit") {
|
|
707
|
+
return parsed;
|
|
708
|
+
}
|
|
709
|
+
const direction = parsed.direction;
|
|
710
|
+
return {
|
|
711
|
+
...parsed,
|
|
712
|
+
pool: parsed.pool ?? DIRECTION_TO_POOL[direction]
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
);
|
|
716
|
+
|
|
717
|
+
// src/schema/relationship-definition.schema.ts
|
|
718
|
+
import { z as z3 } from "zod";
|
|
719
|
+
var TypeDirectionSchema = z3.object({
|
|
720
|
+
/** Name of the inverse type when viewed from the other direction */
|
|
721
|
+
inverse: z3.string().optional(),
|
|
722
|
+
/** Both directions are equivalent — queries should check both FK columns */
|
|
723
|
+
bidirectional: z3.boolean().optional(),
|
|
724
|
+
/** Explicitly directed, no named inverse (default behavior) */
|
|
725
|
+
directed: z3.boolean().optional()
|
|
726
|
+
}).refine(
|
|
727
|
+
(data) => {
|
|
728
|
+
const set = [data.inverse, data.bidirectional, data.directed].filter(
|
|
729
|
+
(v) => v !== void 0
|
|
730
|
+
);
|
|
731
|
+
return set.length === 1;
|
|
732
|
+
},
|
|
733
|
+
{
|
|
734
|
+
message: "Exactly one of inverse, bidirectional, or directed must be specified"
|
|
735
|
+
}
|
|
736
|
+
);
|
|
737
|
+
var RelationshipTypesSchema = z3.union([
|
|
738
|
+
// Simple list: all types are directed from→to
|
|
739
|
+
z3.array(z3.string().regex(/^[a-z][a-z0-9_]*$/, "Type must be snake_case")),
|
|
740
|
+
// Object map: each type has direction metadata
|
|
741
|
+
z3.record(
|
|
742
|
+
z3.string().regex(/^[a-z][a-z0-9_]*$/, "Type key must be snake_case"),
|
|
743
|
+
TypeDirectionSchema
|
|
744
|
+
)
|
|
745
|
+
]);
|
|
746
|
+
var OnDeleteActionSchema = z3.enum(["restrict", "cascade", "set_null", "no_action"]).default("restrict");
|
|
747
|
+
var RelationshipConfigSchema = z3.object({
|
|
748
|
+
/** Relationship name (snake_case). Used for class/file naming. */
|
|
749
|
+
name: z3.string().regex(
|
|
750
|
+
/^[a-z][a-z0-9_]*$/,
|
|
751
|
+
"Relationship name must be snake_case"
|
|
752
|
+
),
|
|
753
|
+
/** Database table name. Defaults to {name}s if not specified. */
|
|
754
|
+
table: z3.string().regex(/^[a-z][a-z0-9_]*$/, "Table must be snake_case").optional(),
|
|
755
|
+
/** The "from" entity — generates {entity}_id FK column (subject). */
|
|
756
|
+
from: z3.string().regex(/^[a-z][a-z0-9_]*$/, "Entity name must be snake_case"),
|
|
757
|
+
/** The "to" entity — generates {entity}_id FK column (object). */
|
|
758
|
+
to: z3.string().regex(/^[a-z][a-z0-9_]*$/, "Entity name must be snake_case"),
|
|
759
|
+
/**
|
|
760
|
+
* Relationship subtypes. Optional — omit for untyped junctions.
|
|
761
|
+
* When present, generates a `type` enum column on the junction table.
|
|
762
|
+
*
|
|
763
|
+
* Simple list: all types are directed (from→to). Use for cross-type
|
|
764
|
+
* relationships where entity asymmetry makes direction obvious.
|
|
765
|
+
*
|
|
766
|
+
* Object map: each type declares its own direction metadata.
|
|
767
|
+
* Required for self-referential relationships (from === to).
|
|
768
|
+
*/
|
|
769
|
+
types: RelationshipTypesSchema.optional(),
|
|
770
|
+
/**
|
|
771
|
+
* Generate temporal validity fields: valid_from (date), valid_to (date?),
|
|
772
|
+
* is_current (boolean, denormalized for query performance).
|
|
773
|
+
* Default: true
|
|
774
|
+
*/
|
|
775
|
+
temporal: z3.boolean().default(true),
|
|
776
|
+
/**
|
|
777
|
+
* Generate source tracking fields: source (enum), confidence (decimal 0-1).
|
|
778
|
+
* Default: true
|
|
779
|
+
*/
|
|
780
|
+
sourced: z3.boolean().default(true),
|
|
781
|
+
/** on_delete action for the "from" endpoint FK. Default: restrict */
|
|
782
|
+
on_delete_from: OnDeleteActionSchema.optional(),
|
|
783
|
+
/** on_delete action for the "to" endpoint FK. Default: restrict */
|
|
784
|
+
on_delete_to: OnDeleteActionSchema.optional(),
|
|
785
|
+
/**
|
|
786
|
+
* Override the default unique constraint columns.
|
|
787
|
+
*
|
|
788
|
+
* Defaults:
|
|
789
|
+
* - Typed: [from_id, to_id, type]
|
|
790
|
+
* - Typed + temporal: [from_id, to_id, type, valid_from]
|
|
791
|
+
* - Untyped: [from_id, to_id]
|
|
792
|
+
*
|
|
793
|
+
* Use this when the default doesn't fit — e.g., allowing multiple
|
|
794
|
+
* relationships of the same type between the same entities at different times.
|
|
795
|
+
*/
|
|
796
|
+
unique_on: z3.array(z3.string()).optional()
|
|
797
|
+
}).strict();
|
|
798
|
+
var RelationshipQuerySchema = z3.object({
|
|
799
|
+
by: z3.array(z3.string()).min(1),
|
|
800
|
+
unique: z3.boolean().optional(),
|
|
801
|
+
select: z3.array(z3.string()).optional(),
|
|
802
|
+
order: z3.string().optional(),
|
|
803
|
+
limit: z3.boolean().optional()
|
|
804
|
+
});
|
|
805
|
+
var RelationshipDefinitionSchema = z3.object({
|
|
806
|
+
/** Relationship configuration block */
|
|
807
|
+
relationship: RelationshipConfigSchema,
|
|
808
|
+
/**
|
|
809
|
+
* Additional fields beyond auto-generated ones.
|
|
810
|
+
* These describe the relationship, not either endpoint entity.
|
|
811
|
+
* Uses the same field definition schema as entity fields.
|
|
812
|
+
*/
|
|
813
|
+
fields: z3.record(z3.string(), z3.any()).optional(),
|
|
814
|
+
/** Declarative queries — same syntax as entity queries. */
|
|
815
|
+
queries: z3.array(RelationshipQuerySchema).optional()
|
|
816
|
+
}).strict().refine(
|
|
817
|
+
(data) => {
|
|
818
|
+
if (data.relationship.from === data.relationship.to && data.relationship.types) {
|
|
819
|
+
return !Array.isArray(data.relationship.types);
|
|
820
|
+
}
|
|
821
|
+
return true;
|
|
822
|
+
},
|
|
823
|
+
{
|
|
824
|
+
message: "Self-referential relationships must use the object map form for types (with inverse/bidirectional/directed metadata), not a simple list",
|
|
825
|
+
path: ["relationship", "types"]
|
|
826
|
+
}
|
|
827
|
+
).refine(
|
|
828
|
+
(data) => {
|
|
829
|
+
if (!data.fields) return true;
|
|
830
|
+
const reserved = getReservedColumnNames(data.relationship);
|
|
831
|
+
const collisions = Object.keys(data.fields).filter(
|
|
832
|
+
(key) => reserved.has(key)
|
|
833
|
+
);
|
|
834
|
+
return collisions.length === 0;
|
|
835
|
+
},
|
|
836
|
+
{
|
|
837
|
+
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).",
|
|
838
|
+
path: ["fields"]
|
|
839
|
+
}
|
|
840
|
+
);
|
|
841
|
+
function getReservedColumnNames(config) {
|
|
842
|
+
const { fromColumn, toColumn } = deriveRelationshipFKColumns(config);
|
|
843
|
+
const reserved = /* @__PURE__ */ new Set([
|
|
844
|
+
"id",
|
|
845
|
+
"created_at",
|
|
846
|
+
"updated_at",
|
|
847
|
+
fromColumn,
|
|
848
|
+
toColumn
|
|
849
|
+
]);
|
|
850
|
+
if (config.types) {
|
|
851
|
+
reserved.add("type");
|
|
852
|
+
}
|
|
853
|
+
if (config.temporal) {
|
|
854
|
+
reserved.add("valid_from");
|
|
855
|
+
reserved.add("valid_to");
|
|
856
|
+
reserved.add("is_current");
|
|
857
|
+
}
|
|
858
|
+
if (config.sourced) {
|
|
859
|
+
reserved.add("source");
|
|
860
|
+
reserved.add("confidence");
|
|
861
|
+
}
|
|
862
|
+
return reserved;
|
|
863
|
+
}
|
|
864
|
+
function deriveRelationshipFKColumns(config) {
|
|
865
|
+
if (config.from === config.to) {
|
|
866
|
+
return {
|
|
867
|
+
fromColumn: `from_${config.from}_id`,
|
|
868
|
+
toColumn: `to_${config.to}_id`
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
return {
|
|
872
|
+
fromColumn: `${config.from}_id`,
|
|
873
|
+
toColumn: `${config.to}_id`
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
function deriveTableName(config) {
|
|
877
|
+
return config.table ?? `${config.name}s`;
|
|
878
|
+
}
|
|
879
|
+
function deriveUniqueConstraint(config) {
|
|
880
|
+
if (config.unique_on) return config.unique_on;
|
|
881
|
+
const { fromColumn, toColumn } = deriveRelationshipFKColumns(config);
|
|
882
|
+
const columns = [fromColumn, toColumn];
|
|
883
|
+
if (config.types) {
|
|
884
|
+
columns.push("type");
|
|
885
|
+
}
|
|
886
|
+
if (config.temporal && config.types) {
|
|
887
|
+
columns.push("valid_from");
|
|
888
|
+
}
|
|
889
|
+
return columns;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// src/schema/junction-definition.schema.ts
|
|
893
|
+
import { z as z4 } from "zod";
|
|
894
|
+
|
|
895
|
+
// src/patterns/library/base-junction-fields.ts
|
|
896
|
+
var BaseJunctionFields = [
|
|
897
|
+
{ name: "is_primary", type: "boolean" },
|
|
898
|
+
{ name: "started_at", type: "timestamp" },
|
|
899
|
+
{ name: "ended_at", type: "timestamp" },
|
|
900
|
+
{ name: "sourced_from", type: "text" },
|
|
901
|
+
{ name: "confidence", type: "numeric(5,4)" },
|
|
902
|
+
{ name: "matched_at", type: "timestamp" }
|
|
903
|
+
];
|
|
904
|
+
var BASE_JUNCTION_FIELD_NAMES = new Set(
|
|
905
|
+
BaseJunctionFields.map((c) => c.name)
|
|
906
|
+
);
|
|
907
|
+
|
|
908
|
+
// src/schema/junction-definition.schema.ts
|
|
909
|
+
var EntityNameSchema = z4.string().regex(/^[a-z][a-z0-9_]*$/, "Entity reference must be snake_case");
|
|
910
|
+
var JunctionDefinitionSchema = z4.object({
|
|
911
|
+
/** Discriminator literal — `pattern: Junction`. */
|
|
912
|
+
pattern: z4.literal("Junction"),
|
|
913
|
+
/**
|
|
914
|
+
* Exactly two endpoint entity names. Both intra- and cross-domain
|
|
915
|
+
* pairings are accepted; entity existence is validated by the
|
|
916
|
+
* analyzer in a later leaf.
|
|
917
|
+
*/
|
|
918
|
+
between: z4.tuple([EntityNameSchema, EntityNameSchema]),
|
|
919
|
+
/**
|
|
920
|
+
* Emit BaseJunctionFields temporal columns (`started_at`, `ended_at`,
|
|
921
|
+
* `matched_at`). Default true. Matches Relationship's `temporal` toggle.
|
|
922
|
+
*/
|
|
923
|
+
temporal: z4.boolean().optional().default(true),
|
|
924
|
+
/**
|
|
925
|
+
* Emit BaseJunctionFields sourcing columns (`sourced_from`,
|
|
926
|
+
* `confidence`). Default true. Matches Relationship's `sourced` toggle.
|
|
927
|
+
*/
|
|
928
|
+
sourced: z4.boolean().optional().default(true),
|
|
929
|
+
/**
|
|
930
|
+
* Junction-specific fields beyond `BaseJunctionFields`. Includes the
|
|
931
|
+
* per-pairing role enum (declared inline; never shared across
|
|
932
|
+
* pairings). Shape is validated downstream by the codegen layer
|
|
933
|
+
* using the existing entity FieldDefinitionSchema.
|
|
934
|
+
*/
|
|
935
|
+
fields: z4.record(z4.string(), z4.any()).optional(),
|
|
936
|
+
/**
|
|
937
|
+
* Declarative queries — same syntax as entity queries. Shape is
|
|
938
|
+
* validated downstream by the codegen layer.
|
|
939
|
+
*/
|
|
940
|
+
queries: z4.array(z4.any()).optional(),
|
|
941
|
+
/**
|
|
942
|
+
* Per-side opt-out for parent-service fan-out (CGP-60). When a side
|
|
943
|
+
* is `false`, the `_inject-parent-service-*` templates emit nothing
|
|
944
|
+
* on that side (and the corresponding module wiring is skipped).
|
|
945
|
+
* The junction service body is always emitted regardless. Defaults
|
|
946
|
+
* to `{ left: true, right: true }`.
|
|
947
|
+
*/
|
|
948
|
+
expose_on_parent: z4.object({
|
|
949
|
+
left: z4.boolean().optional().default(true),
|
|
950
|
+
right: z4.boolean().optional().default(true)
|
|
951
|
+
}).optional().default({ left: true, right: true })
|
|
952
|
+
}).strict().refine((d) => d.between[0] !== d.between[1], {
|
|
953
|
+
message: "`between` endpoints must be distinct",
|
|
954
|
+
path: ["between"]
|
|
955
|
+
}).refine(
|
|
956
|
+
(d) => {
|
|
957
|
+
const fieldNames = Object.keys(d.fields ?? {});
|
|
958
|
+
return !fieldNames.some((n) => BASE_JUNCTION_FIELD_NAMES.has(n));
|
|
959
|
+
},
|
|
960
|
+
{
|
|
961
|
+
message: "`fields:` block redeclares a reserved BaseJunctionFields column (is_primary, started_at, ended_at, sourced_from, confidence, matched_at)",
|
|
962
|
+
path: ["fields"]
|
|
963
|
+
}
|
|
964
|
+
);
|
|
965
|
+
function validateJunctionDefinition(data) {
|
|
966
|
+
return JunctionDefinitionSchema.parse(data);
|
|
967
|
+
}
|
|
968
|
+
function safeValidateJunctionDefinition(data) {
|
|
969
|
+
const result = JunctionDefinitionSchema.safeParse(data);
|
|
970
|
+
if (result.success) {
|
|
971
|
+
return { success: true, data: result.data };
|
|
972
|
+
}
|
|
973
|
+
return { success: false, error: result.error };
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// src/schema/provider-definition.schema.ts
|
|
977
|
+
import { z as z5 } from "zod";
|
|
978
|
+
var IMPORT_REF_RE = /^[^#\s]+#[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
979
|
+
var ImportRefSchema = z5.string().regex(
|
|
980
|
+
IMPORT_REF_RE,
|
|
981
|
+
"must be an 'import-path#Export' reference (e.g. '@app/foo/bar.strategy#BarStrategy')"
|
|
982
|
+
);
|
|
983
|
+
function parseImportRef(ref) {
|
|
984
|
+
const hash = ref.indexOf("#");
|
|
985
|
+
return { path: ref.slice(0, hash), exportName: ref.slice(hash + 1) };
|
|
986
|
+
}
|
|
987
|
+
var AuthTypeSchema = z5.enum(["oauth2", "api-key", "app-password"]);
|
|
988
|
+
var AuthSchema = z5.object({
|
|
989
|
+
type: AuthTypeSchema,
|
|
990
|
+
// Class implementing the auth subsystem's strategy contract (ADR-031).
|
|
991
|
+
// Pre-flight verified against a real export at codegen time.
|
|
992
|
+
strategy: ImportRefSchema,
|
|
993
|
+
// Required (and non-empty) iff type === 'oauth2'; see refine below.
|
|
994
|
+
scopes: z5.array(z5.string()).optional()
|
|
995
|
+
}).strict().refine(
|
|
996
|
+
(a) => a.type !== "oauth2" || a.scopes !== void 0 && a.scopes.length > 0,
|
|
997
|
+
{
|
|
998
|
+
message: "auth.scopes is required and must be non-empty when auth.type is 'oauth2'",
|
|
999
|
+
path: ["scopes"]
|
|
1000
|
+
}
|
|
1001
|
+
);
|
|
1002
|
+
var ClientSchema = z5.object({
|
|
1003
|
+
// API client class. Pre-flight verified against a real export.
|
|
1004
|
+
class: ImportRefSchema,
|
|
1005
|
+
base_url: z5.string().url("client.base_url must be an absolute URL")
|
|
1006
|
+
}).strict();
|
|
1007
|
+
var ProviderDefinitionSchema = z5.object({
|
|
1008
|
+
// Provider id — the canonical string used as detection: keys, audit rows,
|
|
1009
|
+
// subscription rows. kebab/lower; unique across definitions/providers/
|
|
1010
|
+
// (uniqueness is a cross-file check in validate-providers.ts).
|
|
1011
|
+
slug: z5.string().regex(
|
|
1012
|
+
/^[a-z][a-z0-9-]*$/,
|
|
1013
|
+
"slug must be kebab-case lower (e.g. 'google', 'hubspot')"
|
|
1014
|
+
),
|
|
1015
|
+
display_name: z5.string().optional(),
|
|
1016
|
+
auth: AuthSchema,
|
|
1017
|
+
client: ClientSchema,
|
|
1018
|
+
// Surfaces this provider serves (ADR-0006: surfaces span contexts — one
|
|
1019
|
+
// Google OAuth feeds calendar+mail+transcript). Each must reference a real
|
|
1020
|
+
// `surface:` declared on some entity; that cross-check is in
|
|
1021
|
+
// validate-providers.ts. Non-empty enforced here.
|
|
1022
|
+
surfaces: z5.array(z5.string()).min(1, "surfaces must list at least one surface"),
|
|
1023
|
+
// Optional auth lifecycle hints consumed by provider-module emission (D2).
|
|
1024
|
+
// `refresh_behavior` is left as a free string in D1 — its domain firms up
|
|
1025
|
+
// when D2 consumes it; carrying it now keeps the YAML lossless.
|
|
1026
|
+
token_lifetime: z5.number().int().positive().optional(),
|
|
1027
|
+
refresh_behavior: z5.string().optional()
|
|
1028
|
+
}).strict();
|
|
1029
|
+
|
|
1030
|
+
// src/utils/yaml-loader.ts
|
|
1031
|
+
function loadEntityFromYaml(filePath) {
|
|
1032
|
+
if (!existsSync(filePath)) {
|
|
1033
|
+
return {
|
|
1034
|
+
success: false,
|
|
1035
|
+
error: `File not found: ${filePath}`,
|
|
1036
|
+
filePath
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
let content;
|
|
1040
|
+
try {
|
|
1041
|
+
content = readFileSync(filePath, "utf-8");
|
|
1042
|
+
} catch (err) {
|
|
1043
|
+
return {
|
|
1044
|
+
success: false,
|
|
1045
|
+
error: `Failed to read file: ${filePath}`,
|
|
1046
|
+
details: [err instanceof Error ? err.message : String(err)],
|
|
1047
|
+
filePath
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
let parsed;
|
|
1051
|
+
try {
|
|
1052
|
+
parsed = parseYaml(content);
|
|
1053
|
+
} catch (err) {
|
|
1054
|
+
return {
|
|
1055
|
+
success: false,
|
|
1056
|
+
error: `Invalid YAML syntax in ${filePath}`,
|
|
1057
|
+
details: [err instanceof Error ? err.message : String(err)],
|
|
1058
|
+
filePath
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
const result = EntityDefinitionSchema.safeParse(parsed);
|
|
1062
|
+
if (!result.success) {
|
|
1063
|
+
return {
|
|
1064
|
+
success: false,
|
|
1065
|
+
error: `Validation failed for ${filePath}`,
|
|
1066
|
+
details: formatZodErrors(result.error),
|
|
1067
|
+
filePath
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
return {
|
|
1071
|
+
success: true,
|
|
1072
|
+
definition: result.data,
|
|
1073
|
+
filePath
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
function formatZodErrors(error) {
|
|
1077
|
+
return error.errors.map((err) => {
|
|
1078
|
+
const path2 = err.path.join(".");
|
|
1079
|
+
const location = path2 ? `at '${path2}'` : "at root";
|
|
1080
|
+
return `${err.message} ${location}`;
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
function loadEntitiesFromYaml(filePaths) {
|
|
1084
|
+
const successes = [];
|
|
1085
|
+
const failures = [];
|
|
1086
|
+
for (const filePath of filePaths) {
|
|
1087
|
+
const result = loadEntityFromYaml(filePath);
|
|
1088
|
+
if (result.success) {
|
|
1089
|
+
successes.push(result);
|
|
1090
|
+
} else {
|
|
1091
|
+
failures.push(result);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
return { successes, failures };
|
|
1095
|
+
}
|
|
1096
|
+
function loadRelationshipFromYaml(filePath) {
|
|
1097
|
+
if (!existsSync(filePath)) {
|
|
1098
|
+
return {
|
|
1099
|
+
success: false,
|
|
1100
|
+
error: `File not found: ${filePath}`,
|
|
1101
|
+
filePath
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
let content;
|
|
1105
|
+
try {
|
|
1106
|
+
content = readFileSync(filePath, "utf-8");
|
|
1107
|
+
} catch (err) {
|
|
1108
|
+
return {
|
|
1109
|
+
success: false,
|
|
1110
|
+
error: `Failed to read file: ${filePath}`,
|
|
1111
|
+
details: [err instanceof Error ? err.message : String(err)],
|
|
1112
|
+
filePath
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
let parsed;
|
|
1116
|
+
try {
|
|
1117
|
+
parsed = parseYaml(content);
|
|
1118
|
+
} catch (err) {
|
|
1119
|
+
return {
|
|
1120
|
+
success: false,
|
|
1121
|
+
error: `Invalid YAML syntax in ${filePath}`,
|
|
1122
|
+
details: [err instanceof Error ? err.message : String(err)],
|
|
1123
|
+
filePath
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
const result = RelationshipDefinitionSchema.safeParse(parsed);
|
|
1127
|
+
if (!result.success) {
|
|
1128
|
+
return {
|
|
1129
|
+
success: false,
|
|
1130
|
+
error: `Validation failed for ${filePath}`,
|
|
1131
|
+
details: formatZodErrors(result.error),
|
|
1132
|
+
filePath
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
return {
|
|
1136
|
+
success: true,
|
|
1137
|
+
definition: result.data,
|
|
1138
|
+
filePath
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
function loadEventFromYaml(filePath) {
|
|
1142
|
+
if (!existsSync(filePath)) {
|
|
1143
|
+
return {
|
|
1144
|
+
success: false,
|
|
1145
|
+
error: `File not found: ${filePath}`,
|
|
1146
|
+
filePath
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
let content;
|
|
1150
|
+
try {
|
|
1151
|
+
content = readFileSync(filePath, "utf-8");
|
|
1152
|
+
} catch (err) {
|
|
1153
|
+
return {
|
|
1154
|
+
success: false,
|
|
1155
|
+
error: `Failed to read file: ${filePath}`,
|
|
1156
|
+
details: [err instanceof Error ? err.message : String(err)],
|
|
1157
|
+
filePath
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
let parsed;
|
|
1161
|
+
try {
|
|
1162
|
+
parsed = parseYaml(content);
|
|
1163
|
+
} catch (err) {
|
|
1164
|
+
return {
|
|
1165
|
+
success: false,
|
|
1166
|
+
error: `Invalid YAML syntax in ${filePath}`,
|
|
1167
|
+
details: [err instanceof Error ? err.message : String(err)],
|
|
1168
|
+
filePath
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
const result = EventDefinitionSchema.safeParse(parsed);
|
|
1172
|
+
if (!result.success) {
|
|
1173
|
+
return {
|
|
1174
|
+
success: false,
|
|
1175
|
+
error: `Validation failed for ${filePath}`,
|
|
1176
|
+
details: formatZodErrors(result.error),
|
|
1177
|
+
filePath
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
return {
|
|
1181
|
+
success: true,
|
|
1182
|
+
definition: result.data,
|
|
1183
|
+
filePath
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
function loadJunctionFromYaml(filePath) {
|
|
1187
|
+
if (!existsSync(filePath)) {
|
|
1188
|
+
return {
|
|
1189
|
+
success: false,
|
|
1190
|
+
error: `File not found: ${filePath}`,
|
|
1191
|
+
filePath
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
let content;
|
|
1195
|
+
try {
|
|
1196
|
+
content = readFileSync(filePath, "utf-8");
|
|
1197
|
+
} catch (err) {
|
|
1198
|
+
return {
|
|
1199
|
+
success: false,
|
|
1200
|
+
error: `Failed to read file: ${filePath}`,
|
|
1201
|
+
details: [err instanceof Error ? err.message : String(err)],
|
|
1202
|
+
filePath
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
let parsed;
|
|
1206
|
+
try {
|
|
1207
|
+
parsed = parseYaml(content);
|
|
1208
|
+
} catch (err) {
|
|
1209
|
+
return {
|
|
1210
|
+
success: false,
|
|
1211
|
+
error: `Invalid YAML syntax in ${filePath}`,
|
|
1212
|
+
details: [err instanceof Error ? err.message : String(err)],
|
|
1213
|
+
filePath
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
const result = JunctionDefinitionSchema.safeParse(parsed);
|
|
1217
|
+
if (!result.success) {
|
|
1218
|
+
return {
|
|
1219
|
+
success: false,
|
|
1220
|
+
error: `Validation failed for ${filePath}`,
|
|
1221
|
+
details: formatZodErrors(result.error),
|
|
1222
|
+
filePath
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
return {
|
|
1226
|
+
success: true,
|
|
1227
|
+
definition: result.data,
|
|
1228
|
+
filePath
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
function loadProviderFromYaml(filePath) {
|
|
1232
|
+
if (!existsSync(filePath)) {
|
|
1233
|
+
return {
|
|
1234
|
+
success: false,
|
|
1235
|
+
error: `File not found: ${filePath}`,
|
|
1236
|
+
filePath
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
let content;
|
|
1240
|
+
try {
|
|
1241
|
+
content = readFileSync(filePath, "utf-8");
|
|
1242
|
+
} catch (err) {
|
|
1243
|
+
return {
|
|
1244
|
+
success: false,
|
|
1245
|
+
error: `Failed to read file: ${filePath}`,
|
|
1246
|
+
details: [err instanceof Error ? err.message : String(err)],
|
|
1247
|
+
filePath
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
let parsed;
|
|
1251
|
+
try {
|
|
1252
|
+
parsed = parseYaml(content);
|
|
1253
|
+
} catch (err) {
|
|
1254
|
+
return {
|
|
1255
|
+
success: false,
|
|
1256
|
+
error: `Invalid YAML syntax in ${filePath}`,
|
|
1257
|
+
details: [err instanceof Error ? err.message : String(err)],
|
|
1258
|
+
filePath
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
const result = ProviderDefinitionSchema.safeParse(parsed);
|
|
1262
|
+
if (!result.success) {
|
|
1263
|
+
return {
|
|
1264
|
+
success: false,
|
|
1265
|
+
error: `Validation failed for ${filePath}`,
|
|
1266
|
+
details: formatZodErrors(result.error),
|
|
1267
|
+
filePath
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
return {
|
|
1271
|
+
success: true,
|
|
1272
|
+
definition: result.data,
|
|
1273
|
+
filePath
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
function loadProvidersFromYaml(filePaths) {
|
|
1277
|
+
const successes = [];
|
|
1278
|
+
const failures = [];
|
|
1279
|
+
for (const filePath of filePaths) {
|
|
1280
|
+
const result = loadProviderFromYaml(filePath);
|
|
1281
|
+
if (result.success) {
|
|
1282
|
+
successes.push(result);
|
|
1283
|
+
} else {
|
|
1284
|
+
failures.push(result);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
return { successes, failures };
|
|
1288
|
+
}
|
|
1289
|
+
function detectYamlType(filePath) {
|
|
1290
|
+
if (!existsSync(filePath)) return "unknown";
|
|
1291
|
+
try {
|
|
1292
|
+
const content = readFileSync(filePath, "utf-8");
|
|
1293
|
+
const parsed = parseYaml(content);
|
|
1294
|
+
if (parsed && typeof parsed === "object") {
|
|
1295
|
+
if (parsed.pattern === "Junction") return "junction";
|
|
1296
|
+
if ("entity" in parsed) return "entity";
|
|
1297
|
+
if ("relationship" in parsed) return "relationship";
|
|
1298
|
+
}
|
|
1299
|
+
} catch {
|
|
1300
|
+
}
|
|
1301
|
+
return "unknown";
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// src/parser/load-entities.ts
|
|
1305
|
+
function transformToEntity(result) {
|
|
1306
|
+
const { definition, filePath } = result;
|
|
1307
|
+
const queries = definition.queries?.filter((q) => "by" in q).map((q) => ({
|
|
1308
|
+
by: q.by,
|
|
1309
|
+
unique: q.unique,
|
|
1310
|
+
select: q.select,
|
|
1311
|
+
order: q.order,
|
|
1312
|
+
limit: q.limit,
|
|
1313
|
+
via: q.via
|
|
1314
|
+
}));
|
|
1315
|
+
const entity = {
|
|
1316
|
+
name: definition.entity.name,
|
|
1317
|
+
plural: definition.entity.plural,
|
|
1318
|
+
table: definition.entity.table,
|
|
1319
|
+
pattern: definition.entity.pattern,
|
|
1320
|
+
patterns: definition.entity.patterns,
|
|
1321
|
+
patternConfig: definition.entity.config,
|
|
1322
|
+
scopeable: definition.entity.scopeable ?? false,
|
|
1323
|
+
folderStructure: definition.entity.folder_structure ?? "nested",
|
|
1324
|
+
fields: /* @__PURE__ */ new Map(),
|
|
1325
|
+
relationships: /* @__PURE__ */ new Map(),
|
|
1326
|
+
behaviors: definition.behaviors.map((b) => typeof b === "string" ? b : b.name),
|
|
1327
|
+
queries,
|
|
1328
|
+
sourcePath: filePath
|
|
1329
|
+
};
|
|
1330
|
+
for (const [name, fieldDef] of Object.entries(definition.fields)) {
|
|
1331
|
+
const field = {
|
|
1332
|
+
name,
|
|
1333
|
+
type: fieldDef.type,
|
|
1334
|
+
required: fieldDef.required ?? false,
|
|
1335
|
+
nullable: fieldDef.nullable ?? false,
|
|
1336
|
+
unique: fieldDef.unique ?? false,
|
|
1337
|
+
index: fieldDef.index ?? false,
|
|
1338
|
+
foreignKey: fieldDef.foreign_key ? parseForeignKey(fieldDef.foreign_key) : void 0,
|
|
1339
|
+
choices: fieldDef.choices,
|
|
1340
|
+
constraints: {
|
|
1341
|
+
minLength: fieldDef.min_length,
|
|
1342
|
+
maxLength: fieldDef.max_length,
|
|
1343
|
+
min: fieldDef.min,
|
|
1344
|
+
max: fieldDef.max
|
|
1345
|
+
},
|
|
1346
|
+
ui: {
|
|
1347
|
+
label: fieldDef.ui_label,
|
|
1348
|
+
type: fieldDef.ui_type,
|
|
1349
|
+
importance: fieldDef.ui_importance,
|
|
1350
|
+
group: fieldDef.ui_group,
|
|
1351
|
+
sortable: fieldDef.ui_sortable,
|
|
1352
|
+
filterable: fieldDef.ui_filterable,
|
|
1353
|
+
visible: fieldDef.ui_visible
|
|
1354
|
+
}
|
|
1355
|
+
};
|
|
1356
|
+
entity.fields.set(name, field);
|
|
1357
|
+
}
|
|
1358
|
+
if (definition.relationships) {
|
|
1359
|
+
for (const [name, relDef] of Object.entries(definition.relationships)) {
|
|
1360
|
+
const relationship = {
|
|
1361
|
+
name,
|
|
1362
|
+
type: relDef.type,
|
|
1363
|
+
target: relDef.target,
|
|
1364
|
+
foreignKey: relDef.foreign_key,
|
|
1365
|
+
inverse: relDef.inverse,
|
|
1366
|
+
through: relDef.through,
|
|
1367
|
+
resolved: false
|
|
1368
|
+
};
|
|
1369
|
+
entity.relationships.set(name, relationship);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
if (definition.integration) {
|
|
1373
|
+
const integrationDef = definition.integration;
|
|
1374
|
+
const parsedIntegration = {
|
|
1375
|
+
electric: integrationDef.electric ?? false
|
|
1376
|
+
};
|
|
1377
|
+
if (integrationDef.providers) {
|
|
1378
|
+
parsedIntegration.providers = {};
|
|
1379
|
+
for (const [providerName, providerDef] of Object.entries(integrationDef.providers)) {
|
|
1380
|
+
const parsedProvider = {
|
|
1381
|
+
remoteEntity: providerDef.remote_entity,
|
|
1382
|
+
direction: providerDef.direction,
|
|
1383
|
+
cdc: providerDef.cdc ?? false
|
|
1384
|
+
};
|
|
1385
|
+
if (providerDef.field_mapping) {
|
|
1386
|
+
parsedProvider.fieldMapping = providerDef.field_mapping;
|
|
1387
|
+
}
|
|
1388
|
+
if (providerDef.read_only_fields) {
|
|
1389
|
+
parsedProvider.readOnlyFields = providerDef.read_only_fields;
|
|
1390
|
+
}
|
|
1391
|
+
parsedIntegration.providers[providerName] = parsedProvider;
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
entity.integration = parsedIntegration;
|
|
1395
|
+
}
|
|
1396
|
+
if (definition.events) {
|
|
1397
|
+
entity.events = definition.events.map((ev) => ({
|
|
1398
|
+
name: ev.name,
|
|
1399
|
+
queue: ev.queue,
|
|
1400
|
+
body: ev.body,
|
|
1401
|
+
generateHandler: ev.generate_handler
|
|
1402
|
+
}));
|
|
1403
|
+
}
|
|
1404
|
+
if (definition.emits !== void 0) {
|
|
1405
|
+
entity.emits = definition.emits;
|
|
1406
|
+
}
|
|
1407
|
+
return entity;
|
|
1408
|
+
}
|
|
1409
|
+
function parseForeignKey(fk) {
|
|
1410
|
+
const [table, column] = fk.split(".");
|
|
1411
|
+
return { table, column: column ?? "id" };
|
|
1412
|
+
}
|
|
1413
|
+
function loadErrorToIssue(error) {
|
|
1414
|
+
const issues = [];
|
|
1415
|
+
issues.push({
|
|
1416
|
+
severity: "error",
|
|
1417
|
+
type: "parse_error",
|
|
1418
|
+
message: error.error,
|
|
1419
|
+
path: error.filePath
|
|
1420
|
+
});
|
|
1421
|
+
if (error.details) {
|
|
1422
|
+
for (const detail of error.details) {
|
|
1423
|
+
issues.push({
|
|
1424
|
+
severity: "error",
|
|
1425
|
+
type: "schema_error",
|
|
1426
|
+
message: detail,
|
|
1427
|
+
path: error.filePath
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
return issues;
|
|
1432
|
+
}
|
|
1433
|
+
function loadEntities(entitiesDir, opts) {
|
|
1434
|
+
const entities = [];
|
|
1435
|
+
const issues = [];
|
|
1436
|
+
const resolvedDir = resolve2(entitiesDir);
|
|
1437
|
+
let files;
|
|
1438
|
+
try {
|
|
1439
|
+
files = findYamlFiles(resolvedDir, { excludeDirs: opts?.excludeDirs });
|
|
1440
|
+
} catch (err) {
|
|
1441
|
+
issues.push({
|
|
1442
|
+
severity: "error",
|
|
1443
|
+
type: "parse_error",
|
|
1444
|
+
message: `Failed to read directory: ${resolvedDir}`,
|
|
1445
|
+
path: resolvedDir
|
|
1446
|
+
});
|
|
1447
|
+
return { entities, issues };
|
|
1448
|
+
}
|
|
1449
|
+
if (files.length === 0) {
|
|
1450
|
+
issues.push({
|
|
1451
|
+
severity: "warning",
|
|
1452
|
+
type: "no_files",
|
|
1453
|
+
message: `No YAML files found in directory: ${resolvedDir}`,
|
|
1454
|
+
path: resolvedDir
|
|
1455
|
+
});
|
|
1456
|
+
return { entities, issues };
|
|
1457
|
+
}
|
|
1458
|
+
for (const filePath of files) {
|
|
1459
|
+
const result = loadEntityFromYaml(filePath);
|
|
1460
|
+
if (result.success) {
|
|
1461
|
+
entities.push(transformToEntity(result));
|
|
1462
|
+
} else {
|
|
1463
|
+
issues.push(...loadErrorToIssue(result));
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
return { entities, issues };
|
|
1467
|
+
}
|
|
1468
|
+
function resolveReferences(entities) {
|
|
1469
|
+
const issues = [];
|
|
1470
|
+
const entityMap = /* @__PURE__ */ new Map();
|
|
1471
|
+
for (const entity of entities) {
|
|
1472
|
+
if (entityMap.has(entity.name)) {
|
|
1473
|
+
issues.push({
|
|
1474
|
+
severity: "error",
|
|
1475
|
+
type: "duplicate_entity",
|
|
1476
|
+
entity: entity.name,
|
|
1477
|
+
message: `Duplicate entity name: ${entity.name}`,
|
|
1478
|
+
path: entity.sourcePath
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
entityMap.set(entity.name, entity);
|
|
1482
|
+
}
|
|
1483
|
+
for (const entity of entities) {
|
|
1484
|
+
for (const [relName, rel] of entity.relationships) {
|
|
1485
|
+
const targetEntity = entityMap.get(rel.target);
|
|
1486
|
+
if (targetEntity) {
|
|
1487
|
+
rel.resolved = true;
|
|
1488
|
+
} else {
|
|
1489
|
+
issues.push({
|
|
1490
|
+
severity: "error",
|
|
1491
|
+
type: "missing_target",
|
|
1492
|
+
entity: entity.name,
|
|
1493
|
+
field: relName,
|
|
1494
|
+
message: `Relationship '${relName}' references unknown entity '${rel.target}'`,
|
|
1495
|
+
path: entity.sourcePath,
|
|
1496
|
+
suggestion: `Define entity '${rel.target}' or fix the target name`
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
for (const [fieldName, field] of entity.fields) {
|
|
1501
|
+
if (field.foreignKey) {
|
|
1502
|
+
const targetTable = field.foreignKey.table;
|
|
1503
|
+
const targetEntity = Array.from(entityMap.values()).find(
|
|
1504
|
+
(e) => e.table === targetTable
|
|
1505
|
+
);
|
|
1506
|
+
if (!targetEntity) {
|
|
1507
|
+
issues.push({
|
|
1508
|
+
severity: "warning",
|
|
1509
|
+
type: "missing_fk_target",
|
|
1510
|
+
entity: entity.name,
|
|
1511
|
+
field: fieldName,
|
|
1512
|
+
message: `Foreign key references unknown table '${targetTable}'`,
|
|
1513
|
+
path: entity.sourcePath,
|
|
1514
|
+
suggestion: `Define entity with table '${targetTable}' or fix the foreign_key reference`
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
return issues;
|
|
1521
|
+
}
|
|
1522
|
+
function transformToRelationshipDefinition(result) {
|
|
1523
|
+
const { definition, filePath } = result;
|
|
1524
|
+
const config = definition.relationship;
|
|
1525
|
+
const { fromColumn, toColumn } = deriveRelationshipFKColumns(config);
|
|
1526
|
+
const table = deriveTableName(config);
|
|
1527
|
+
const uniqueOn = deriveUniqueConstraint(config);
|
|
1528
|
+
const types = resolveTypeDirections(config.types);
|
|
1529
|
+
const fields = /* @__PURE__ */ new Map();
|
|
1530
|
+
if (definition.fields) {
|
|
1531
|
+
for (const [name, fieldDef] of Object.entries(definition.fields)) {
|
|
1532
|
+
const field = {
|
|
1533
|
+
name,
|
|
1534
|
+
type: fieldDef.type,
|
|
1535
|
+
required: fieldDef.required ?? false,
|
|
1536
|
+
nullable: fieldDef.nullable ?? false,
|
|
1537
|
+
unique: fieldDef.unique ?? false,
|
|
1538
|
+
index: fieldDef.index ?? false,
|
|
1539
|
+
foreignKey: fieldDef.foreign_key ? parseForeignKey(fieldDef.foreign_key) : void 0,
|
|
1540
|
+
choices: fieldDef.choices,
|
|
1541
|
+
constraints: {
|
|
1542
|
+
minLength: fieldDef.min_length,
|
|
1543
|
+
maxLength: fieldDef.max_length,
|
|
1544
|
+
min: fieldDef.min,
|
|
1545
|
+
max: fieldDef.max
|
|
1546
|
+
},
|
|
1547
|
+
ui: {
|
|
1548
|
+
label: fieldDef.ui_label,
|
|
1549
|
+
type: fieldDef.ui_type,
|
|
1550
|
+
importance: fieldDef.ui_importance,
|
|
1551
|
+
group: fieldDef.ui_group,
|
|
1552
|
+
sortable: fieldDef.ui_sortable,
|
|
1553
|
+
filterable: fieldDef.ui_filterable,
|
|
1554
|
+
visible: fieldDef.ui_visible
|
|
1555
|
+
}
|
|
1556
|
+
};
|
|
1557
|
+
fields.set(name, field);
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
const queries = definition.queries?.filter((q) => "by" in q).map((q) => ({
|
|
1561
|
+
by: q.by,
|
|
1562
|
+
unique: q.unique,
|
|
1563
|
+
select: q.select,
|
|
1564
|
+
order: q.order,
|
|
1565
|
+
limit: q.limit
|
|
1566
|
+
}));
|
|
1567
|
+
return {
|
|
1568
|
+
name: config.name,
|
|
1569
|
+
table,
|
|
1570
|
+
from: config.from,
|
|
1571
|
+
to: config.to,
|
|
1572
|
+
selfReferential: config.from === config.to,
|
|
1573
|
+
fromColumn,
|
|
1574
|
+
toColumn,
|
|
1575
|
+
types,
|
|
1576
|
+
hasTypes: types.length > 0,
|
|
1577
|
+
temporal: config.temporal,
|
|
1578
|
+
sourced: config.sourced,
|
|
1579
|
+
onDeleteFrom: config.on_delete_from ?? "restrict",
|
|
1580
|
+
onDeleteTo: config.on_delete_to ?? "restrict",
|
|
1581
|
+
uniqueOn,
|
|
1582
|
+
fields,
|
|
1583
|
+
queries,
|
|
1584
|
+
sourcePath: filePath
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
function resolveTypeDirections(types) {
|
|
1588
|
+
if (!types) return [];
|
|
1589
|
+
if (Array.isArray(types)) {
|
|
1590
|
+
return types.map((name) => ({
|
|
1591
|
+
name,
|
|
1592
|
+
bidirectional: false,
|
|
1593
|
+
directed: true
|
|
1594
|
+
}));
|
|
1595
|
+
}
|
|
1596
|
+
return Object.entries(types).map(([name, dir]) => {
|
|
1597
|
+
const direction = dir;
|
|
1598
|
+
return {
|
|
1599
|
+
name,
|
|
1600
|
+
inverse: direction.inverse,
|
|
1601
|
+
bidirectional: direction.bidirectional ?? false,
|
|
1602
|
+
directed: direction.directed ?? (!direction.bidirectional && !direction.inverse)
|
|
1603
|
+
};
|
|
1604
|
+
});
|
|
1605
|
+
}
|
|
1606
|
+
function loadRelationships(relationshipsDir) {
|
|
1607
|
+
const relationships = [];
|
|
1608
|
+
const issues = [];
|
|
1609
|
+
const resolvedDir = resolve2(relationshipsDir);
|
|
1610
|
+
let files;
|
|
1611
|
+
try {
|
|
1612
|
+
files = findYamlFiles(resolvedDir);
|
|
1613
|
+
} catch {
|
|
1614
|
+
return { relationships, issues };
|
|
1615
|
+
}
|
|
1616
|
+
if (files.length === 0) {
|
|
1617
|
+
return { relationships, issues };
|
|
1618
|
+
}
|
|
1619
|
+
for (const filePath of files) {
|
|
1620
|
+
const result = loadRelationshipFromYaml(filePath);
|
|
1621
|
+
if (result.success) {
|
|
1622
|
+
relationships.push(transformToRelationshipDefinition(result));
|
|
1623
|
+
} else {
|
|
1624
|
+
issues.push(...loadErrorToIssue(result));
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
return { relationships, issues };
|
|
1628
|
+
}
|
|
1629
|
+
function resolveRelationshipReferences(relationshipDefs, entities) {
|
|
1630
|
+
const issues = [];
|
|
1631
|
+
const entityNames = new Set(entities.map((e) => e.name));
|
|
1632
|
+
for (const relDef of relationshipDefs) {
|
|
1633
|
+
if (!entityNames.has(relDef.from)) {
|
|
1634
|
+
issues.push({
|
|
1635
|
+
severity: "warning",
|
|
1636
|
+
type: "missing_relationship_endpoint",
|
|
1637
|
+
entity: relDef.name,
|
|
1638
|
+
message: `Relationship '${relDef.name}' references unknown 'from' entity '${relDef.from}'`,
|
|
1639
|
+
path: relDef.sourcePath,
|
|
1640
|
+
suggestion: `Define entity '${relDef.from}' or fix the 'from' value`
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1643
|
+
if (!entityNames.has(relDef.to)) {
|
|
1644
|
+
issues.push({
|
|
1645
|
+
severity: "warning",
|
|
1646
|
+
type: "missing_relationship_endpoint",
|
|
1647
|
+
entity: relDef.name,
|
|
1648
|
+
message: `Relationship '${relDef.name}' references unknown 'to' entity '${relDef.to}'`,
|
|
1649
|
+
path: relDef.sourcePath,
|
|
1650
|
+
suggestion: `Define entity '${relDef.to}' or fix the 'to' value`
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
const dupes = relationshipDefs.filter((r) => r.name === relDef.name);
|
|
1654
|
+
if (dupes.length > 1) {
|
|
1655
|
+
issues.push({
|
|
1656
|
+
severity: "error",
|
|
1657
|
+
type: "duplicate_relationship",
|
|
1658
|
+
entity: relDef.name,
|
|
1659
|
+
message: `Duplicate relationship name: ${relDef.name}`,
|
|
1660
|
+
path: relDef.sourcePath
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
return issues;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
// src/parser/validate-providers.ts
|
|
1668
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, statSync } from "fs";
|
|
1669
|
+
import { isAbsolute, join as join2, resolve as resolve3 } from "path";
|
|
1670
|
+
import ts from "typescript";
|
|
1671
|
+
function collectEntitySurfaces(entities) {
|
|
1672
|
+
const surfaces = /* @__PURE__ */ new Set();
|
|
1673
|
+
for (const e of entities) {
|
|
1674
|
+
if (e.entity.surface) surfaces.add(e.entity.surface);
|
|
1675
|
+
}
|
|
1676
|
+
return surfaces;
|
|
1677
|
+
}
|
|
1678
|
+
function resolveImportRef(ref, opts) {
|
|
1679
|
+
const { path: path2, exportName } = parseImportRef(ref);
|
|
1680
|
+
const file = resolveModuleFile(path2, opts);
|
|
1681
|
+
if (!file) {
|
|
1682
|
+
return { status: "module-not-found", resolvedFrom: opts.sourceRoot };
|
|
1683
|
+
}
|
|
1684
|
+
const content = readFileSync2(file, "utf-8");
|
|
1685
|
+
const { names, hasWildcard } = collectExportedNames(file, content);
|
|
1686
|
+
if (names.has(exportName) || hasWildcard) {
|
|
1687
|
+
return { status: "ok", file };
|
|
1688
|
+
}
|
|
1689
|
+
return { status: "export-not-found", file };
|
|
1690
|
+
}
|
|
1691
|
+
function resolveModuleFile(importPath, opts) {
|
|
1692
|
+
let base = null;
|
|
1693
|
+
for (const [alias, target] of Object.entries(opts.aliases ?? {})) {
|
|
1694
|
+
if (importPath === alias || importPath.startsWith(`${alias}/`)) {
|
|
1695
|
+
const rest = importPath.slice(alias.length);
|
|
1696
|
+
base = join2(target, rest);
|
|
1697
|
+
break;
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
if (base === null) {
|
|
1701
|
+
base = isAbsolute(importPath) ? importPath : resolve3(opts.sourceRoot, importPath);
|
|
1702
|
+
}
|
|
1703
|
+
const candidates = [
|
|
1704
|
+
base,
|
|
1705
|
+
`${base}.ts`,
|
|
1706
|
+
`${base}.tsx`,
|
|
1707
|
+
join2(base, "index.ts"),
|
|
1708
|
+
join2(base, "index.tsx")
|
|
1709
|
+
];
|
|
1710
|
+
for (const c of candidates) {
|
|
1711
|
+
if (existsSync2(c) && statSync(c).isFile()) return c;
|
|
1712
|
+
}
|
|
1713
|
+
return null;
|
|
1714
|
+
}
|
|
1715
|
+
function collectExportedNames(fileName, content) {
|
|
1716
|
+
const sf = ts.createSourceFile(
|
|
1717
|
+
fileName,
|
|
1718
|
+
content,
|
|
1719
|
+
ts.ScriptTarget.Latest,
|
|
1720
|
+
/* setParentNodes */
|
|
1721
|
+
true
|
|
1722
|
+
);
|
|
1723
|
+
const names = /* @__PURE__ */ new Set();
|
|
1724
|
+
let hasWildcard = false;
|
|
1725
|
+
const hasExportModifier = (node) => ts.canHaveModifiers(node) && (ts.getModifiers(node)?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false);
|
|
1726
|
+
sf.forEachChild((node) => {
|
|
1727
|
+
if (hasExportModifier(node)) {
|
|
1728
|
+
if ((ts.isClassDeclaration(node) || ts.isFunctionDeclaration(node) || ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isEnumDeclaration(node) || ts.isModuleDeclaration(node)) && node.name && ts.isIdentifier(node.name)) {
|
|
1729
|
+
names.add(node.name.text);
|
|
1730
|
+
} else if (ts.isVariableStatement(node)) {
|
|
1731
|
+
for (const decl of node.declarationList.declarations) {
|
|
1732
|
+
if (ts.isIdentifier(decl.name)) names.add(decl.name.text);
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
if (ts.isExportDeclaration(node)) {
|
|
1737
|
+
if (!node.exportClause) {
|
|
1738
|
+
hasWildcard = true;
|
|
1739
|
+
} else if (ts.isNamedExports(node.exportClause)) {
|
|
1740
|
+
for (const el of node.exportClause.elements) names.add(el.name.text);
|
|
1741
|
+
} else if (ts.isNamespaceExport(node.exportClause)) {
|
|
1742
|
+
names.add(node.exportClause.name.text);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
});
|
|
1746
|
+
return { names, hasWildcard };
|
|
1747
|
+
}
|
|
1748
|
+
function validateProviders(providers, opts) {
|
|
1749
|
+
const issues = [];
|
|
1750
|
+
const knownSurfaces = new Set(opts.entitySurfaces);
|
|
1751
|
+
const slugToFiles = /* @__PURE__ */ new Map();
|
|
1752
|
+
for (const p of providers) {
|
|
1753
|
+
const files = slugToFiles.get(p.definition.slug) ?? [];
|
|
1754
|
+
files.push(p.filePath);
|
|
1755
|
+
slugToFiles.set(p.definition.slug, files);
|
|
1756
|
+
}
|
|
1757
|
+
for (const [slug, files] of slugToFiles) {
|
|
1758
|
+
if (files.length > 1) {
|
|
1759
|
+
for (const file of files) {
|
|
1760
|
+
const others = files.filter((f) => f !== file);
|
|
1761
|
+
issues.push({
|
|
1762
|
+
severity: "error",
|
|
1763
|
+
type: "provider_duplicate_slug",
|
|
1764
|
+
message: `provider slug '${slug}' is declared more than once (also in: ${others.join(", ")})`,
|
|
1765
|
+
path: file
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
for (const { definition, filePath } of providers) {
|
|
1771
|
+
const { slug } = definition;
|
|
1772
|
+
for (const surface of definition.surfaces) {
|
|
1773
|
+
if (!knownSurfaces.has(surface)) {
|
|
1774
|
+
const known = [...knownSurfaces].sort().join(", ") || "(none declared)";
|
|
1775
|
+
issues.push({
|
|
1776
|
+
severity: "error",
|
|
1777
|
+
type: "provider_unknown_surface",
|
|
1778
|
+
message: `provider ${slug}: surface '${surface}' is not declared by any entity (known surfaces: ${known})`,
|
|
1779
|
+
path: filePath
|
|
1780
|
+
});
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
if (!opts.skipImportCheck) {
|
|
1784
|
+
if (!opts.sourceRoot) {
|
|
1785
|
+
throw new Error(
|
|
1786
|
+
"validateProviders: sourceRoot is required for the import pre-flight check (or set skipImportCheck: true)"
|
|
1787
|
+
);
|
|
1788
|
+
}
|
|
1789
|
+
const resolveOpts = {
|
|
1790
|
+
sourceRoot: opts.sourceRoot,
|
|
1791
|
+
aliases: opts.aliases
|
|
1792
|
+
};
|
|
1793
|
+
const refs = [
|
|
1794
|
+
{ field: "auth.strategy", ref: definition.auth.strategy },
|
|
1795
|
+
{ field: "client.class", ref: definition.client.class }
|
|
1796
|
+
];
|
|
1797
|
+
for (const { field, ref } of refs) {
|
|
1798
|
+
const result = resolveImportRef(ref, resolveOpts);
|
|
1799
|
+
if (result.status === "module-not-found") {
|
|
1800
|
+
issues.push({
|
|
1801
|
+
severity: "error",
|
|
1802
|
+
type: "provider_import_unresolved",
|
|
1803
|
+
message: `provider ${slug}: ${field} '${ref}' not found \u2014 module could not be resolved from ${result.resolvedFrom}`,
|
|
1804
|
+
path: filePath
|
|
1805
|
+
});
|
|
1806
|
+
} else if (result.status === "export-not-found") {
|
|
1807
|
+
const { exportName } = parseImportRef(ref);
|
|
1808
|
+
issues.push({
|
|
1809
|
+
severity: "error",
|
|
1810
|
+
type: "provider_import_unresolved",
|
|
1811
|
+
message: `provider ${slug}: ${field} '${ref}' not found \u2014 export '${exportName}' is missing from ${result.file}`,
|
|
1812
|
+
path: filePath
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
return issues;
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
// src/analyzer/graph-builder.ts
|
|
1822
|
+
function inferCardinality(type) {
|
|
1823
|
+
switch (type) {
|
|
1824
|
+
case "belongs_to":
|
|
1825
|
+
return "N:1";
|
|
1826
|
+
case "has_many":
|
|
1827
|
+
return "1:N";
|
|
1828
|
+
case "has_one":
|
|
1829
|
+
return "1:1";
|
|
1830
|
+
default:
|
|
1831
|
+
return "1:N";
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
function hasReverseEdge(edges, from, to) {
|
|
1835
|
+
return edges.find((e) => e.from === to && e.to === from);
|
|
1836
|
+
}
|
|
1837
|
+
function buildDomainGraph(entities, relationshipDefinitions = []) {
|
|
1838
|
+
const entityMap = /* @__PURE__ */ new Map();
|
|
1839
|
+
const relDefMap = /* @__PURE__ */ new Map();
|
|
1840
|
+
const edges = [];
|
|
1841
|
+
for (const entity of entities) {
|
|
1842
|
+
entityMap.set(entity.name, entity);
|
|
1843
|
+
}
|
|
1844
|
+
for (const relDef of relationshipDefinitions) {
|
|
1845
|
+
relDefMap.set(relDef.name, relDef);
|
|
1846
|
+
}
|
|
1847
|
+
for (const entity of entities) {
|
|
1848
|
+
for (const [relName, rel] of entity.relationships) {
|
|
1849
|
+
if (!rel.resolved) continue;
|
|
1850
|
+
const reverseEdge = hasReverseEdge(edges, entity.name, rel.target);
|
|
1851
|
+
const edge = {
|
|
1852
|
+
from: entity.name,
|
|
1853
|
+
to: rel.target,
|
|
1854
|
+
relationship: rel,
|
|
1855
|
+
cardinality: inferCardinality(rel.type),
|
|
1856
|
+
bidirectional: reverseEdge !== void 0
|
|
1857
|
+
};
|
|
1858
|
+
if (reverseEdge) {
|
|
1859
|
+
reverseEdge.bidirectional = true;
|
|
1860
|
+
}
|
|
1861
|
+
edges.push(edge);
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
for (const relDef of relationshipDefinitions) {
|
|
1865
|
+
const fromExists = entityMap.has(relDef.from);
|
|
1866
|
+
const toExists = entityMap.has(relDef.to);
|
|
1867
|
+
if (fromExists && toExists) {
|
|
1868
|
+
const edge = {
|
|
1869
|
+
from: relDef.from,
|
|
1870
|
+
to: relDef.to,
|
|
1871
|
+
relationship: {
|
|
1872
|
+
name: relDef.name,
|
|
1873
|
+
type: "has_many",
|
|
1874
|
+
target: relDef.to,
|
|
1875
|
+
foreignKey: relDef.fromColumn,
|
|
1876
|
+
resolved: true
|
|
1877
|
+
},
|
|
1878
|
+
cardinality: "N:M",
|
|
1879
|
+
bidirectional: relDef.types.some((t) => t.bidirectional)
|
|
1880
|
+
};
|
|
1881
|
+
edges.push(edge);
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
return { entities: entityMap, relationshipDefinitions: relDefMap, edges };
|
|
1885
|
+
}
|
|
1886
|
+
function getRelatedEntities(graph, entityName, depth = 1) {
|
|
1887
|
+
const related = /* @__PURE__ */ new Set();
|
|
1888
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1889
|
+
const queue = [
|
|
1890
|
+
{ name: entityName, currentDepth: 0 }
|
|
1891
|
+
];
|
|
1892
|
+
while (queue.length > 0) {
|
|
1893
|
+
const item = queue.shift();
|
|
1894
|
+
if (!item) continue;
|
|
1895
|
+
const { name, currentDepth } = item;
|
|
1896
|
+
if (visited.has(name) || currentDepth > depth) continue;
|
|
1897
|
+
visited.add(name);
|
|
1898
|
+
for (const edge of graph.edges) {
|
|
1899
|
+
if (edge.from === name && !visited.has(edge.to)) {
|
|
1900
|
+
related.add(edge.to);
|
|
1901
|
+
queue.push({ name: edge.to, currentDepth: currentDepth + 1 });
|
|
1902
|
+
}
|
|
1903
|
+
if (edge.to === name && !visited.has(edge.from)) {
|
|
1904
|
+
related.add(edge.from);
|
|
1905
|
+
queue.push({ name: edge.from, currentDepth: currentDepth + 1 });
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
return related;
|
|
1910
|
+
}
|
|
1911
|
+
function findOrphanEntities(graph) {
|
|
1912
|
+
const orphans = [];
|
|
1913
|
+
for (const [name] of graph.entities) {
|
|
1914
|
+
const hasRelationship = graph.edges.some((e) => e.from === name || e.to === name);
|
|
1915
|
+
if (!hasRelationship) {
|
|
1916
|
+
orphans.push(name);
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
return orphans;
|
|
1920
|
+
}
|
|
1921
|
+
function findCircularDependencies(graph) {
|
|
1922
|
+
const cycles = [];
|
|
1923
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1924
|
+
const recursionStack = /* @__PURE__ */ new Set();
|
|
1925
|
+
function dfs(node, path2) {
|
|
1926
|
+
visited.add(node);
|
|
1927
|
+
recursionStack.add(node);
|
|
1928
|
+
const outgoingEdges = graph.edges.filter((e) => e.from === node);
|
|
1929
|
+
for (const edge of outgoingEdges) {
|
|
1930
|
+
if (!visited.has(edge.to)) {
|
|
1931
|
+
dfs(edge.to, [...path2, edge.to]);
|
|
1932
|
+
} else if (recursionStack.has(edge.to)) {
|
|
1933
|
+
const cycleStart = path2.indexOf(edge.to);
|
|
1934
|
+
if (cycleStart !== -1) {
|
|
1935
|
+
cycles.push([...path2.slice(cycleStart), edge.to]);
|
|
1936
|
+
} else {
|
|
1937
|
+
cycles.push([...path2, edge.to]);
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
recursionStack.delete(node);
|
|
1942
|
+
}
|
|
1943
|
+
for (const [name] of graph.entities) {
|
|
1944
|
+
if (!visited.has(name)) {
|
|
1945
|
+
dfs(name, [name]);
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
const uniqueCycles = [];
|
|
1949
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1950
|
+
for (const cycle of cycles) {
|
|
1951
|
+
const minIndex = cycle.indexOf(
|
|
1952
|
+
cycle.reduce((min, val) => val < min ? val : min, cycle[0])
|
|
1953
|
+
);
|
|
1954
|
+
const normalized = [...cycle.slice(minIndex), ...cycle.slice(0, minIndex)];
|
|
1955
|
+
const key = normalized.join("->");
|
|
1956
|
+
if (!seen.has(key)) {
|
|
1957
|
+
seen.add(key);
|
|
1958
|
+
uniqueCycles.push(cycle);
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
return uniqueCycles;
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
// src/behaviors/external-id-tracking.ts
|
|
1965
|
+
var externalIdTrackingBehavior = {
|
|
1966
|
+
name: "external_id_tracking",
|
|
1967
|
+
description: "Adds external_id, provider, and provider_metadata fields for external system sync tracking",
|
|
1968
|
+
fields: [
|
|
1969
|
+
{
|
|
1970
|
+
name: "external_id",
|
|
1971
|
+
camelName: "externalId",
|
|
1972
|
+
type: "string",
|
|
1973
|
+
tsType: "string | null",
|
|
1974
|
+
drizzleType: "varchar",
|
|
1975
|
+
drizzleImports: ["varchar", "index"],
|
|
1976
|
+
zodType: "z.string().nullable()",
|
|
1977
|
+
nullable: true,
|
|
1978
|
+
ui: {
|
|
1979
|
+
label: "External ID",
|
|
1980
|
+
type: "text",
|
|
1981
|
+
importance: "tertiary",
|
|
1982
|
+
group: "metadata",
|
|
1983
|
+
visible: false
|
|
1984
|
+
}
|
|
1985
|
+
},
|
|
1986
|
+
{
|
|
1987
|
+
name: "provider",
|
|
1988
|
+
camelName: "provider",
|
|
1989
|
+
type: "string",
|
|
1990
|
+
tsType: "string | null",
|
|
1991
|
+
drizzleType: "varchar",
|
|
1992
|
+
drizzleImports: ["varchar"],
|
|
1993
|
+
zodType: "z.string().nullable()",
|
|
1994
|
+
nullable: true,
|
|
1995
|
+
ui: {
|
|
1996
|
+
label: "Provider",
|
|
1997
|
+
type: "text",
|
|
1998
|
+
importance: "tertiary",
|
|
1999
|
+
group: "metadata",
|
|
2000
|
+
visible: false
|
|
2001
|
+
}
|
|
2002
|
+
},
|
|
2003
|
+
{
|
|
2004
|
+
name: "provider_metadata",
|
|
2005
|
+
camelName: "providerMetadata",
|
|
2006
|
+
type: "json",
|
|
2007
|
+
tsType: "unknown | null",
|
|
2008
|
+
drizzleType: "jsonb",
|
|
2009
|
+
drizzleImports: ["jsonb"],
|
|
2010
|
+
zodType: "z.unknown().nullable()",
|
|
2011
|
+
nullable: true,
|
|
2012
|
+
ui: {
|
|
2013
|
+
label: "Provider Metadata",
|
|
2014
|
+
type: "json",
|
|
2015
|
+
importance: "tertiary",
|
|
2016
|
+
group: "metadata",
|
|
2017
|
+
visible: false
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
],
|
|
2021
|
+
drizzleImports: ["varchar", "jsonb", "index"],
|
|
2022
|
+
configKey: "externalIdTracking"
|
|
2023
|
+
};
|
|
2024
|
+
|
|
2025
|
+
// src/behaviors/soft-delete.ts
|
|
2026
|
+
var softDeleteBehavior = {
|
|
2027
|
+
name: "soft_delete",
|
|
2028
|
+
description: "Adds deleted_at field for soft delete functionality",
|
|
2029
|
+
fields: [
|
|
2030
|
+
{
|
|
2031
|
+
name: "deleted_at",
|
|
2032
|
+
camelName: "deletedAt",
|
|
2033
|
+
type: "datetime",
|
|
2034
|
+
tsType: "Date | null",
|
|
2035
|
+
drizzleType: "timestamp",
|
|
2036
|
+
drizzleImports: ["timestamp"],
|
|
2037
|
+
zodType: "z.coerce.date().nullable()",
|
|
2038
|
+
nullable: true,
|
|
2039
|
+
ui: {
|
|
2040
|
+
label: "Deleted At",
|
|
2041
|
+
type: "datetime",
|
|
2042
|
+
importance: "tertiary",
|
|
2043
|
+
group: "metadata",
|
|
2044
|
+
visible: false
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
],
|
|
2048
|
+
drizzleImports: ["timestamp"],
|
|
2049
|
+
methods: [
|
|
2050
|
+
"softDelete",
|
|
2051
|
+
"restore",
|
|
2052
|
+
"findWithDeleted",
|
|
2053
|
+
"findOnlyDeleted",
|
|
2054
|
+
"baseQuery"
|
|
2055
|
+
// Modified to filter deleted records
|
|
2056
|
+
],
|
|
2057
|
+
configKey: "softDelete"
|
|
2058
|
+
};
|
|
2059
|
+
|
|
2060
|
+
// src/behaviors/timestamps.ts
|
|
2061
|
+
var timestampsBehavior = {
|
|
2062
|
+
name: "timestamps",
|
|
2063
|
+
description: "Adds created_at and updated_at timestamp fields",
|
|
2064
|
+
fields: [
|
|
2065
|
+
{
|
|
2066
|
+
name: "created_at",
|
|
2067
|
+
camelName: "createdAt",
|
|
2068
|
+
type: "datetime",
|
|
2069
|
+
tsType: "Date",
|
|
2070
|
+
drizzleType: "timestamp",
|
|
2071
|
+
drizzleImports: ["timestamp"],
|
|
2072
|
+
zodType: "z.coerce.date()",
|
|
2073
|
+
nullable: false,
|
|
2074
|
+
default: "now()",
|
|
2075
|
+
ui: {
|
|
2076
|
+
label: "Created At",
|
|
2077
|
+
type: "datetime",
|
|
2078
|
+
importance: "tertiary",
|
|
2079
|
+
group: "metadata",
|
|
2080
|
+
visible: false
|
|
2081
|
+
}
|
|
2082
|
+
},
|
|
2083
|
+
{
|
|
2084
|
+
name: "updated_at",
|
|
2085
|
+
camelName: "updatedAt",
|
|
2086
|
+
type: "datetime",
|
|
2087
|
+
tsType: "Date",
|
|
2088
|
+
drizzleType: "timestamp",
|
|
2089
|
+
drizzleImports: ["timestamp"],
|
|
2090
|
+
zodType: "z.coerce.date()",
|
|
2091
|
+
nullable: false,
|
|
2092
|
+
default: "now()",
|
|
2093
|
+
ui: {
|
|
2094
|
+
label: "Updated At",
|
|
2095
|
+
type: "datetime",
|
|
2096
|
+
importance: "tertiary",
|
|
2097
|
+
group: "metadata",
|
|
2098
|
+
visible: false
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
],
|
|
2102
|
+
drizzleImports: ["timestamp"],
|
|
2103
|
+
methods: ["applyTimestampsOnCreate", "applyTimestampsOnUpdate"],
|
|
2104
|
+
configKey: "timestamps"
|
|
2105
|
+
};
|
|
2106
|
+
|
|
2107
|
+
// src/behaviors/user-tracking.ts
|
|
2108
|
+
var userTrackingBehavior = {
|
|
2109
|
+
name: "user_tracking",
|
|
2110
|
+
description: "Adds created_by and updated_by user reference fields",
|
|
2111
|
+
fields: [
|
|
2112
|
+
{
|
|
2113
|
+
name: "created_by",
|
|
2114
|
+
camelName: "createdBy",
|
|
2115
|
+
type: "uuid",
|
|
2116
|
+
tsType: "string | null",
|
|
2117
|
+
drizzleType: "uuid",
|
|
2118
|
+
drizzleImports: ["uuid"],
|
|
2119
|
+
zodType: "z.string().uuid().nullable()",
|
|
2120
|
+
nullable: true,
|
|
2121
|
+
foreignKey: "users.id",
|
|
2122
|
+
ui: {
|
|
2123
|
+
label: "Created By",
|
|
2124
|
+
type: "reference",
|
|
2125
|
+
importance: "tertiary",
|
|
2126
|
+
group: "metadata",
|
|
2127
|
+
visible: false
|
|
2128
|
+
}
|
|
2129
|
+
},
|
|
2130
|
+
{
|
|
2131
|
+
name: "updated_by",
|
|
2132
|
+
camelName: "updatedBy",
|
|
2133
|
+
type: "uuid",
|
|
2134
|
+
tsType: "string | null",
|
|
2135
|
+
drizzleType: "uuid",
|
|
2136
|
+
drizzleImports: ["uuid"],
|
|
2137
|
+
zodType: "z.string().uuid().nullable()",
|
|
2138
|
+
nullable: true,
|
|
2139
|
+
foreignKey: "users.id",
|
|
2140
|
+
ui: {
|
|
2141
|
+
label: "Updated By",
|
|
2142
|
+
type: "reference",
|
|
2143
|
+
importance: "tertiary",
|
|
2144
|
+
group: "metadata",
|
|
2145
|
+
visible: false
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
],
|
|
2149
|
+
drizzleImports: ["uuid"],
|
|
2150
|
+
methods: ["applyUserTrackingOnCreate", "applyUserTrackingOnUpdate"],
|
|
2151
|
+
configKey: "userTracking"
|
|
2152
|
+
};
|
|
2153
|
+
|
|
2154
|
+
// src/behaviors/index.ts
|
|
2155
|
+
var behaviorRegistry = /* @__PURE__ */ new Map([
|
|
2156
|
+
["timestamps", timestampsBehavior],
|
|
2157
|
+
["soft_delete", softDeleteBehavior],
|
|
2158
|
+
["user_tracking", userTrackingBehavior],
|
|
2159
|
+
["external_id_tracking", externalIdTrackingBehavior]
|
|
2160
|
+
]);
|
|
2161
|
+
function getBehavior(name) {
|
|
2162
|
+
return behaviorRegistry.get(name);
|
|
2163
|
+
}
|
|
2164
|
+
function normalizeBehaviorConfig(config) {
|
|
2165
|
+
if (typeof config === "string") {
|
|
2166
|
+
return { name: config, options: {} };
|
|
2167
|
+
}
|
|
2168
|
+
return { name: config.name, options: config.options ?? {} };
|
|
2169
|
+
}
|
|
2170
|
+
function normalizeBehaviorConfigs(configs) {
|
|
2171
|
+
return configs.map(normalizeBehaviorConfig);
|
|
2172
|
+
}
|
|
2173
|
+
function resolveBehaviorFields(configs) {
|
|
2174
|
+
const normalized = normalizeBehaviorConfigs(configs);
|
|
2175
|
+
const fields = [];
|
|
2176
|
+
const addedFieldNames = /* @__PURE__ */ new Set();
|
|
2177
|
+
for (const config of normalized) {
|
|
2178
|
+
const behavior = getBehavior(config.name);
|
|
2179
|
+
if (!behavior) continue;
|
|
2180
|
+
for (const field of behavior.fields) {
|
|
2181
|
+
if (!addedFieldNames.has(field.name)) {
|
|
2182
|
+
fields.push(field);
|
|
2183
|
+
addedFieldNames.add(field.name);
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
return fields;
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
// src/analyzer/consistency-checker.ts
|
|
2191
|
+
function checkConsistency(graph) {
|
|
2192
|
+
const issues = [];
|
|
2193
|
+
for (const [name, entity] of graph.entities) {
|
|
2194
|
+
issues.push(...checkEntityConsistency(entity));
|
|
2195
|
+
issues.push(...checkRelationshipConsistency(entity, graph));
|
|
2196
|
+
issues.push(...checkNamingConventions(entity));
|
|
2197
|
+
issues.push(...checkMissingIndexes(entity));
|
|
2198
|
+
issues.push(...checkUiMetadata(entity));
|
|
2199
|
+
if (entity.queries !== void 0) {
|
|
2200
|
+
issues.push(...checkQueryFieldReferences(entity));
|
|
2201
|
+
}
|
|
2202
|
+
if (entity.integration !== void 0) {
|
|
2203
|
+
issues.push(...checkIntegrationFieldMappingReferences(entity));
|
|
2204
|
+
issues.push(...checkExternalIdTrackingCollision(entity));
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
issues.push(...checkOrphanEntities(graph));
|
|
2208
|
+
issues.push(...checkCircularReferences(graph));
|
|
2209
|
+
issues.push(...checkMissingInverses(graph));
|
|
2210
|
+
return issues;
|
|
2211
|
+
}
|
|
2212
|
+
function checkEntityConsistency(entity) {
|
|
2213
|
+
const issues = [];
|
|
2214
|
+
if (!entity.fields.has("id")) {
|
|
2215
|
+
issues.push({
|
|
2216
|
+
severity: "info",
|
|
2217
|
+
type: "missing_id",
|
|
2218
|
+
entity: entity.name,
|
|
2219
|
+
message: 'Entity missing standard "id" field',
|
|
2220
|
+
suggestion: 'Add an "id" field with type "uuid"'
|
|
2221
|
+
});
|
|
2222
|
+
}
|
|
2223
|
+
const hasCreatedAt = entity.fields.has("created_at");
|
|
2224
|
+
const hasTimestampsBehavior = entity.behaviors.includes("timestamps");
|
|
2225
|
+
if (!hasCreatedAt && !hasTimestampsBehavior) {
|
|
2226
|
+
issues.push({
|
|
2227
|
+
severity: "info",
|
|
2228
|
+
type: "missing_timestamps",
|
|
2229
|
+
entity: entity.name,
|
|
2230
|
+
message: 'Entity missing "created_at" field and "timestamps" behavior',
|
|
2231
|
+
suggestion: 'Add "timestamps" to behaviors or add created_at/updated_at fields'
|
|
2232
|
+
});
|
|
2233
|
+
}
|
|
2234
|
+
return issues;
|
|
2235
|
+
}
|
|
2236
|
+
function checkRelationshipConsistency(entity, graph) {
|
|
2237
|
+
const issues = [];
|
|
2238
|
+
for (const [relName, rel] of entity.relationships) {
|
|
2239
|
+
if (rel.type === "belongs_to") {
|
|
2240
|
+
const fkField = entity.fields.get(rel.foreignKey);
|
|
2241
|
+
if (!fkField) {
|
|
2242
|
+
issues.push({
|
|
2243
|
+
severity: "warning",
|
|
2244
|
+
type: "missing_fk_field",
|
|
2245
|
+
entity: entity.name,
|
|
2246
|
+
field: relName,
|
|
2247
|
+
message: `Relationship "${relName}" references foreign key "${rel.foreignKey}" but field doesn't exist`,
|
|
2248
|
+
suggestion: `Add field "${rel.foreignKey}" with foreign_key reference`
|
|
2249
|
+
});
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
if (rel.type === "has_many" || rel.type === "has_one") {
|
|
2253
|
+
const targetEntity = graph.entities.get(rel.target);
|
|
2254
|
+
if (targetEntity) {
|
|
2255
|
+
const targetFkField = targetEntity.fields.get(rel.foreignKey);
|
|
2256
|
+
if (!targetFkField) {
|
|
2257
|
+
issues.push({
|
|
2258
|
+
severity: "warning",
|
|
2259
|
+
type: "missing_target_fk",
|
|
2260
|
+
entity: entity.name,
|
|
2261
|
+
field: relName,
|
|
2262
|
+
message: `Relationship "${relName}" expects foreign key "${rel.foreignKey}" on "${rel.target}" but field doesn't exist`,
|
|
2263
|
+
suggestion: `Add field "${rel.foreignKey}" to "${rel.target}" entity`
|
|
2264
|
+
});
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
return issues;
|
|
2270
|
+
}
|
|
2271
|
+
function checkNamingConventions(entity) {
|
|
2272
|
+
const issues = [];
|
|
2273
|
+
if (entity.name !== entity.name.toLowerCase()) {
|
|
2274
|
+
issues.push({
|
|
2275
|
+
severity: "warning",
|
|
2276
|
+
type: "naming_convention",
|
|
2277
|
+
entity: entity.name,
|
|
2278
|
+
message: "Entity name should be lowercase",
|
|
2279
|
+
suggestion: `Use "${entity.name.toLowerCase()}"`
|
|
2280
|
+
});
|
|
2281
|
+
}
|
|
2282
|
+
for (const [fieldName] of entity.fields) {
|
|
2283
|
+
if (fieldName !== fieldName.toLowerCase()) {
|
|
2284
|
+
issues.push({
|
|
2285
|
+
severity: "warning",
|
|
2286
|
+
type: "naming_convention",
|
|
2287
|
+
entity: entity.name,
|
|
2288
|
+
field: fieldName,
|
|
2289
|
+
message: "Field name should be snake_case",
|
|
2290
|
+
suggestion: `Use "${toSnakeCase(fieldName)}"`
|
|
2291
|
+
});
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
for (const [relName] of entity.relationships) {
|
|
2295
|
+
if (relName !== relName.toLowerCase()) {
|
|
2296
|
+
issues.push({
|
|
2297
|
+
severity: "warning",
|
|
2298
|
+
type: "naming_convention",
|
|
2299
|
+
entity: entity.name,
|
|
2300
|
+
field: relName,
|
|
2301
|
+
message: "Relationship name should be snake_case",
|
|
2302
|
+
suggestion: `Use "${toSnakeCase(relName)}"`
|
|
2303
|
+
});
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
return issues;
|
|
2307
|
+
}
|
|
2308
|
+
function checkMissingIndexes(entity) {
|
|
2309
|
+
const issues = [];
|
|
2310
|
+
for (const [fieldName, field] of entity.fields) {
|
|
2311
|
+
if (field.ui.filterable && !field.index && !field.unique) {
|
|
2312
|
+
issues.push({
|
|
2313
|
+
severity: "warning",
|
|
2314
|
+
type: "missing_index",
|
|
2315
|
+
entity: entity.name,
|
|
2316
|
+
field: fieldName,
|
|
2317
|
+
message: `Field "${fieldName}" is filterable but has no index`,
|
|
2318
|
+
suggestion: 'Add "index: true" to improve query performance'
|
|
2319
|
+
});
|
|
2320
|
+
}
|
|
2321
|
+
if (field.foreignKey && !field.index && !field.unique) {
|
|
2322
|
+
issues.push({
|
|
2323
|
+
severity: "info",
|
|
2324
|
+
type: "missing_fk_index",
|
|
2325
|
+
entity: entity.name,
|
|
2326
|
+
field: fieldName,
|
|
2327
|
+
message: `Foreign key field "${fieldName}" has no index`,
|
|
2328
|
+
suggestion: 'Add "index: true" for better join performance'
|
|
2329
|
+
});
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
return issues;
|
|
2333
|
+
}
|
|
2334
|
+
function checkUiMetadata(entity) {
|
|
2335
|
+
const issues = [];
|
|
2336
|
+
const systemFields = /* @__PURE__ */ new Set(["id", "created_at", "updated_at", "deleted_at", "tenant_id"]);
|
|
2337
|
+
for (const [fieldName, field] of entity.fields) {
|
|
2338
|
+
if (systemFields.has(fieldName)) continue;
|
|
2339
|
+
const hasAnyUiMeta = field.ui.label !== void 0 || field.ui.type !== void 0 || field.ui.group !== void 0;
|
|
2340
|
+
if (!hasAnyUiMeta) {
|
|
2341
|
+
issues.push({
|
|
2342
|
+
severity: "info",
|
|
2343
|
+
type: "missing_ui_metadata",
|
|
2344
|
+
entity: entity.name,
|
|
2345
|
+
field: fieldName,
|
|
2346
|
+
message: `Field "${fieldName}" has no UI metadata`,
|
|
2347
|
+
suggestion: "Add ui_label, ui_type, ui_group for better admin panel display"
|
|
2348
|
+
});
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
return issues;
|
|
2352
|
+
}
|
|
2353
|
+
function checkOrphanEntities(graph) {
|
|
2354
|
+
const orphans = findOrphanEntities(graph);
|
|
2355
|
+
return orphans.map((name) => ({
|
|
2356
|
+
severity: "info",
|
|
2357
|
+
type: "orphan_entity",
|
|
2358
|
+
entity: name,
|
|
2359
|
+
message: `Entity "${name}" has no relationships to other entities`,
|
|
2360
|
+
suggestion: "Consider if this entity should be related to others"
|
|
2361
|
+
}));
|
|
2362
|
+
}
|
|
2363
|
+
function checkCircularReferences(graph) {
|
|
2364
|
+
const cycles = findCircularDependencies(graph);
|
|
2365
|
+
return cycles.map((cycle) => ({
|
|
2366
|
+
severity: "info",
|
|
2367
|
+
type: "circular_dependency",
|
|
2368
|
+
entity: cycle[0],
|
|
2369
|
+
message: `Circular reference detected: ${cycle.join(" -> ")}`,
|
|
2370
|
+
suggestion: "Verify this is intentional (e.g., self-referential hierarchy)"
|
|
2371
|
+
}));
|
|
2372
|
+
}
|
|
2373
|
+
function checkMissingInverses(graph) {
|
|
2374
|
+
const issues = [];
|
|
2375
|
+
for (const edge of graph.edges) {
|
|
2376
|
+
const { from, to, relationship } = edge;
|
|
2377
|
+
const targetEntity = graph.entities.get(to);
|
|
2378
|
+
if (!targetEntity) continue;
|
|
2379
|
+
const hasInverse = Array.from(targetEntity.relationships.values()).some(
|
|
2380
|
+
(rel) => rel.target === from
|
|
2381
|
+
);
|
|
2382
|
+
if (!hasInverse && relationship.type !== "belongs_to") {
|
|
2383
|
+
issues.push({
|
|
2384
|
+
severity: "info",
|
|
2385
|
+
type: "missing_inverse",
|
|
2386
|
+
entity: from,
|
|
2387
|
+
field: relationship.name,
|
|
2388
|
+
message: `Relationship "${relationship.name}" to "${to}" has no inverse defined on target`,
|
|
2389
|
+
suggestion: `Add inverse relationship on "${to}" pointing back to "${from}"`
|
|
2390
|
+
});
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
return issues;
|
|
2394
|
+
}
|
|
2395
|
+
function getAvailableFieldNames(entity) {
|
|
2396
|
+
const entityFieldNames = Array.from(entity.fields.keys());
|
|
2397
|
+
const behaviorFields = resolveBehaviorFields(entity.behaviors);
|
|
2398
|
+
const behaviorFieldNames = behaviorFields.map((f) => f.name);
|
|
2399
|
+
const belongsToFkNames = [];
|
|
2400
|
+
for (const rel of entity.relationships.values()) {
|
|
2401
|
+
if (rel.type === "belongs_to" && rel.foreignKey) {
|
|
2402
|
+
belongsToFkNames.push(rel.foreignKey);
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
return [.../* @__PURE__ */ new Set([...entityFieldNames, ...behaviorFieldNames, ...belongsToFkNames])];
|
|
2406
|
+
}
|
|
2407
|
+
function checkQueryFieldReferences(entity) {
|
|
2408
|
+
const issues = [];
|
|
2409
|
+
const availableFields = getAvailableFieldNames(entity);
|
|
2410
|
+
const availableSet = new Set(availableFields);
|
|
2411
|
+
for (const query of entity.queries ?? []) {
|
|
2412
|
+
if (!query.via) {
|
|
2413
|
+
for (const fieldName of query.by) {
|
|
2414
|
+
if (!availableSet.has(fieldName)) {
|
|
2415
|
+
issues.push({
|
|
2416
|
+
severity: "error",
|
|
2417
|
+
type: "unknown_query_field",
|
|
2418
|
+
entity: entity.name,
|
|
2419
|
+
field: fieldName,
|
|
2420
|
+
message: `Query references unknown field "${fieldName}" in entity "${entity.name}". Available fields: ${availableFields.join(", ")}`
|
|
2421
|
+
});
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
for (const fieldName of query.select ?? []) {
|
|
2426
|
+
if (!availableSet.has(fieldName)) {
|
|
2427
|
+
issues.push({
|
|
2428
|
+
severity: "error",
|
|
2429
|
+
type: "unknown_query_field",
|
|
2430
|
+
entity: entity.name,
|
|
2431
|
+
field: fieldName,
|
|
2432
|
+
message: `Query references unknown field "${fieldName}" in entity "${entity.name}". Available fields: ${availableFields.join(", ")}`
|
|
2433
|
+
});
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
return issues;
|
|
2438
|
+
}
|
|
2439
|
+
function checkIntegrationFieldMappingReferences(entity) {
|
|
2440
|
+
const issues = [];
|
|
2441
|
+
const availableFields = getAvailableFieldNames(entity);
|
|
2442
|
+
const availableSet = new Set(availableFields);
|
|
2443
|
+
for (const [providerName, provider] of Object.entries(entity.integration?.providers ?? {})) {
|
|
2444
|
+
for (const fieldName of Object.keys(provider.fieldMapping ?? {})) {
|
|
2445
|
+
if (!availableSet.has(fieldName)) {
|
|
2446
|
+
issues.push({
|
|
2447
|
+
severity: "warning",
|
|
2448
|
+
type: "unknown_integration_field_mapping",
|
|
2449
|
+
entity: entity.name,
|
|
2450
|
+
field: fieldName,
|
|
2451
|
+
message: `Integration field mapping references unknown field "${fieldName}" for provider "${providerName}" in entity "${entity.name}"`
|
|
2452
|
+
});
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
for (const fieldName of provider.readOnlyFields ?? []) {
|
|
2456
|
+
if (!availableSet.has(fieldName)) {
|
|
2457
|
+
issues.push({
|
|
2458
|
+
severity: "warning",
|
|
2459
|
+
type: "unknown_integration_field_mapping",
|
|
2460
|
+
entity: entity.name,
|
|
2461
|
+
field: fieldName,
|
|
2462
|
+
message: `Integration field mapping references unknown field "${fieldName}" for provider "${providerName}" in entity "${entity.name}"`
|
|
2463
|
+
});
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
return issues;
|
|
2468
|
+
}
|
|
2469
|
+
function checkExternalIdTrackingCollision(entity) {
|
|
2470
|
+
const issues = [];
|
|
2471
|
+
const hasExternalIdTracking = entity.behaviors.includes("external_id_tracking");
|
|
2472
|
+
if (!hasExternalIdTracking) return issues;
|
|
2473
|
+
for (const [providerName, provider] of Object.entries(entity.integration?.providers ?? {})) {
|
|
2474
|
+
if (provider.fieldMapping && "external_id" in provider.fieldMapping) {
|
|
2475
|
+
issues.push({
|
|
2476
|
+
severity: "warning",
|
|
2477
|
+
type: "external_id_tracking_collision",
|
|
2478
|
+
entity: entity.name,
|
|
2479
|
+
field: "external_id",
|
|
2480
|
+
message: `Entity "${entity.name}" has external_id_tracking behavior and also maps "external_id" in integration field_mapping for provider "${providerName}". The behavior-added field may collide with the mapped field.`
|
|
2481
|
+
});
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
return issues;
|
|
2485
|
+
}
|
|
2486
|
+
function toSnakeCase(str) {
|
|
2487
|
+
return str.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
// src/analyzer/statistics.ts
|
|
2491
|
+
function computeStatistics(graph) {
|
|
2492
|
+
const entities = Array.from(graph.entities.values());
|
|
2493
|
+
const fieldsByType = {};
|
|
2494
|
+
const relationshipsByType = {};
|
|
2495
|
+
let totalFields = 0;
|
|
2496
|
+
let totalRelationships = 0;
|
|
2497
|
+
let entitiesWithBehaviors = 0;
|
|
2498
|
+
for (const entity of entities) {
|
|
2499
|
+
totalFields += entity.fields.size;
|
|
2500
|
+
totalRelationships += entity.relationships.size;
|
|
2501
|
+
if (entity.behaviors.length > 0) {
|
|
2502
|
+
entitiesWithBehaviors++;
|
|
2503
|
+
}
|
|
2504
|
+
for (const field of entity.fields.values()) {
|
|
2505
|
+
fieldsByType[field.type] = (fieldsByType[field.type] ?? 0) + 1;
|
|
2506
|
+
}
|
|
2507
|
+
for (const rel of entity.relationships.values()) {
|
|
2508
|
+
relationshipsByType[rel.type] = (relationshipsByType[rel.type] ?? 0) + 1;
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
return {
|
|
2512
|
+
totalEntities: entities.length,
|
|
2513
|
+
totalFields,
|
|
2514
|
+
totalRelationships,
|
|
2515
|
+
fieldsByType,
|
|
2516
|
+
relationshipsByType,
|
|
2517
|
+
entitiesWithBehaviors,
|
|
2518
|
+
averageFieldsPerEntity: entities.length > 0 ? totalFields / entities.length : 0
|
|
2519
|
+
};
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
// src/analyzer/transitive-suggester.ts
|
|
2523
|
+
var DEFAULT_OPTIONS = {
|
|
2524
|
+
maxDepth: 3,
|
|
2525
|
+
excludeEntities: ["workspace", "tenant"],
|
|
2526
|
+
excludePatterns: [/_audit$/, /_log$/, /_history$/]
|
|
2527
|
+
};
|
|
2528
|
+
function suggestTransitiveRelationships(graph, options) {
|
|
2529
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
2530
|
+
const suggestions = [];
|
|
2531
|
+
for (const [entityName, entity] of graph.entities) {
|
|
2532
|
+
if (shouldExcludeEntity(entityName, opts)) continue;
|
|
2533
|
+
const paths = findTransitivePaths(graph, entityName, opts);
|
|
2534
|
+
for (const path2 of paths) {
|
|
2535
|
+
suggestions.push(createSuggestion(path2));
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
return suggestions;
|
|
2539
|
+
}
|
|
2540
|
+
function shouldExcludeEntity(entityName, opts) {
|
|
2541
|
+
if (opts.excludeEntities.includes(entityName)) {
|
|
2542
|
+
return true;
|
|
2543
|
+
}
|
|
2544
|
+
for (const pattern of opts.excludePatterns) {
|
|
2545
|
+
if (pattern.test(entityName)) {
|
|
2546
|
+
return true;
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
return false;
|
|
2550
|
+
}
|
|
2551
|
+
function findTransitivePaths(graph, sourceEntity, opts) {
|
|
2552
|
+
const paths = [];
|
|
2553
|
+
const sourceEntityData = graph.entities.get(sourceEntity);
|
|
2554
|
+
if (!sourceEntityData) return paths;
|
|
2555
|
+
const queue = [
|
|
2556
|
+
{
|
|
2557
|
+
entity: sourceEntity,
|
|
2558
|
+
depth: 0,
|
|
2559
|
+
path: [],
|
|
2560
|
+
visited: /* @__PURE__ */ new Set([sourceEntity])
|
|
2561
|
+
}
|
|
2562
|
+
];
|
|
2563
|
+
while (queue.length > 0) {
|
|
2564
|
+
const current = queue.shift();
|
|
2565
|
+
if (!current) continue;
|
|
2566
|
+
const { entity, depth, path: path2, visited } = current;
|
|
2567
|
+
if (depth >= opts.maxDepth) continue;
|
|
2568
|
+
const currentEntity = graph.entities.get(entity);
|
|
2569
|
+
if (!currentEntity) continue;
|
|
2570
|
+
for (const [relName, rel] of currentEntity.relationships) {
|
|
2571
|
+
if (rel.through) continue;
|
|
2572
|
+
if (rel.type !== "has_many" && rel.type !== "has_one") continue;
|
|
2573
|
+
const target = rel.target;
|
|
2574
|
+
if (shouldExcludeEntity(target, opts)) continue;
|
|
2575
|
+
if (visited.has(target)) continue;
|
|
2576
|
+
const newPath = [
|
|
2577
|
+
...path2,
|
|
2578
|
+
{
|
|
2579
|
+
via: entity,
|
|
2580
|
+
relationship: relName,
|
|
2581
|
+
foreignKey: rel.foreignKey
|
|
2582
|
+
}
|
|
2583
|
+
];
|
|
2584
|
+
if (depth >= 1) {
|
|
2585
|
+
if (!hasDirectRelationship(sourceEntityData, target)) {
|
|
2586
|
+
const transitivePath = buildTransitivePath(
|
|
2587
|
+
sourceEntity,
|
|
2588
|
+
target,
|
|
2589
|
+
newPath
|
|
2590
|
+
);
|
|
2591
|
+
if (transitivePath) {
|
|
2592
|
+
paths.push(transitivePath);
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
if (depth + 1 < opts.maxDepth) {
|
|
2597
|
+
queue.push({
|
|
2598
|
+
entity: target,
|
|
2599
|
+
depth: depth + 1,
|
|
2600
|
+
path: newPath,
|
|
2601
|
+
visited: /* @__PURE__ */ new Set([...visited, target])
|
|
2602
|
+
});
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
return paths;
|
|
2607
|
+
}
|
|
2608
|
+
function hasDirectRelationship(sourceEntity, targetName) {
|
|
2609
|
+
for (const rel of sourceEntity.relationships.values()) {
|
|
2610
|
+
if (rel.target === targetName && !rel.through) {
|
|
2611
|
+
return true;
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
return false;
|
|
2615
|
+
}
|
|
2616
|
+
function buildTransitivePath(source, target, hops) {
|
|
2617
|
+
if (hops.length === 0) return null;
|
|
2618
|
+
const throughPath = hops.map((hop) => hop.relationship).join(".");
|
|
2619
|
+
const suggestedName = generateSemanticName(source, target, hops);
|
|
2620
|
+
const yamlSnippet = generateYamlSnippet(suggestedName, target, throughPath);
|
|
2621
|
+
return {
|
|
2622
|
+
source,
|
|
2623
|
+
target,
|
|
2624
|
+
hops,
|
|
2625
|
+
suggestedName,
|
|
2626
|
+
throughPath,
|
|
2627
|
+
yamlSnippet
|
|
2628
|
+
};
|
|
2629
|
+
}
|
|
2630
|
+
function generateSemanticName(source, target, hops) {
|
|
2631
|
+
const firstHop = hops[0].relationship;
|
|
2632
|
+
if (hops.length === 2) {
|
|
2633
|
+
const prefix = firstHop.replace(/s$/, "");
|
|
2634
|
+
return `${prefix}_${target}`;
|
|
2635
|
+
}
|
|
2636
|
+
const parts = [firstHop.replace(/s$/, ""), target];
|
|
2637
|
+
return parts.join("_");
|
|
2638
|
+
}
|
|
2639
|
+
function generateYamlSnippet(name, target, throughPath) {
|
|
2640
|
+
return ` ${name}:
|
|
2641
|
+
type: has_many
|
|
2642
|
+
target: ${target}
|
|
2643
|
+
through: "${throughPath}"`;
|
|
2644
|
+
}
|
|
2645
|
+
function createSuggestion(path2) {
|
|
2646
|
+
const pathDescription = [path2.source, ...path2.hops.map((h) => h.via), path2.target].join(" -> ");
|
|
2647
|
+
return {
|
|
2648
|
+
severity: "info",
|
|
2649
|
+
type: "transitive_suggestion",
|
|
2650
|
+
entity: path2.source,
|
|
2651
|
+
message: `Potential transitive relationship: ${pathDescription}`,
|
|
2652
|
+
suggestion: `Add "${path2.suggestedName}" relationship via "${path2.throughPath}"`,
|
|
2653
|
+
path: path2
|
|
2654
|
+
};
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
// src/analyzer/manifest.ts
|
|
2658
|
+
import { createHash } from "crypto";
|
|
2659
|
+
import { readFileSync as readFileSync3, writeFileSync, existsSync as existsSync3, mkdirSync, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
2660
|
+
import { join as join3 } from "path";
|
|
2661
|
+
var MANIFEST_FILE = "manifest.json";
|
|
2662
|
+
var MANIFEST_VERSION = 1;
|
|
2663
|
+
function getManifestDir() {
|
|
2664
|
+
return process.env.CODEGEN_MANIFEST_DIR || ".codegen";
|
|
2665
|
+
}
|
|
2666
|
+
function getManifestPaths(projectRoot) {
|
|
2667
|
+
const dir = join3(projectRoot, getManifestDir());
|
|
2668
|
+
const file = join3(dir, MANIFEST_FILE);
|
|
2669
|
+
return { dir, file };
|
|
2670
|
+
}
|
|
2671
|
+
async function computeEntityFilesHash(entitiesDir) {
|
|
2672
|
+
if (!existsSync3(entitiesDir)) {
|
|
2673
|
+
return createHash("sha256").update("").digest("hex");
|
|
2674
|
+
}
|
|
2675
|
+
const yamlFiles = [];
|
|
2676
|
+
function walkDir(dir) {
|
|
2677
|
+
const entries = readdirSync2(dir);
|
|
2678
|
+
for (const entry of entries) {
|
|
2679
|
+
const fullPath = join3(dir, entry);
|
|
2680
|
+
const stat = statSync2(fullPath);
|
|
2681
|
+
if (stat.isDirectory()) {
|
|
2682
|
+
walkDir(fullPath);
|
|
2683
|
+
} else if (stat.isFile() && (entry.endsWith(".yaml") || entry.endsWith(".yml"))) {
|
|
2684
|
+
yamlFiles.push(fullPath);
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
walkDir(entitiesDir);
|
|
2689
|
+
yamlFiles.sort();
|
|
2690
|
+
const hash = createHash("sha256");
|
|
2691
|
+
for (const file of yamlFiles) {
|
|
2692
|
+
const content = readFileSync3(file, "utf-8");
|
|
2693
|
+
hash.update(file);
|
|
2694
|
+
hash.update(content);
|
|
2695
|
+
}
|
|
2696
|
+
return hash.digest("hex");
|
|
2697
|
+
}
|
|
2698
|
+
function readManifest(projectRoot) {
|
|
2699
|
+
const { file } = getManifestPaths(projectRoot);
|
|
2700
|
+
if (!existsSync3(file)) {
|
|
2701
|
+
return null;
|
|
2702
|
+
}
|
|
2703
|
+
try {
|
|
2704
|
+
const content = readFileSync3(file, "utf-8");
|
|
2705
|
+
const manifest = JSON.parse(content);
|
|
2706
|
+
if (manifest.version !== MANIFEST_VERSION) {
|
|
2707
|
+
return null;
|
|
2708
|
+
}
|
|
2709
|
+
return manifest;
|
|
2710
|
+
} catch (error) {
|
|
2711
|
+
return null;
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
function writeManifest(projectRoot, manifest) {
|
|
2715
|
+
const { dir, file } = getManifestPaths(projectRoot);
|
|
2716
|
+
if (!existsSync3(dir)) {
|
|
2717
|
+
mkdirSync(dir, { recursive: true });
|
|
2718
|
+
}
|
|
2719
|
+
const content = JSON.stringify(manifest, null, 2);
|
|
2720
|
+
writeFileSync(file, content, "utf-8");
|
|
2721
|
+
}
|
|
2722
|
+
async function isManifestStale(projectRoot, entitiesDir) {
|
|
2723
|
+
const manifest = readManifest(projectRoot);
|
|
2724
|
+
if (!manifest) {
|
|
2725
|
+
return true;
|
|
2726
|
+
}
|
|
2727
|
+
const currentHash = await computeEntityFilesHash(entitiesDir);
|
|
2728
|
+
return manifest.entityFilesHash !== currentHash;
|
|
2729
|
+
}
|
|
2730
|
+
function toManifestEntity(entity) {
|
|
2731
|
+
const fields = {};
|
|
2732
|
+
for (const [name, field] of entity.fields) {
|
|
2733
|
+
fields[name] = {
|
|
2734
|
+
name: field.name,
|
|
2735
|
+
type: field.type,
|
|
2736
|
+
required: field.required,
|
|
2737
|
+
nullable: field.nullable,
|
|
2738
|
+
unique: field.unique,
|
|
2739
|
+
index: field.index,
|
|
2740
|
+
foreignKey: field.foreignKey,
|
|
2741
|
+
choices: field.choices
|
|
2742
|
+
};
|
|
2743
|
+
}
|
|
2744
|
+
const relationships = {};
|
|
2745
|
+
for (const [name, rel] of entity.relationships) {
|
|
2746
|
+
relationships[name] = {
|
|
2747
|
+
type: rel.type,
|
|
2748
|
+
target: rel.target,
|
|
2749
|
+
foreignKey: rel.foreignKey,
|
|
2750
|
+
through: rel.through,
|
|
2751
|
+
inverse: rel.inverse
|
|
2752
|
+
};
|
|
2753
|
+
}
|
|
2754
|
+
return {
|
|
2755
|
+
sourcePath: entity.sourcePath,
|
|
2756
|
+
table: entity.table,
|
|
2757
|
+
plural: entity.plural,
|
|
2758
|
+
fields,
|
|
2759
|
+
relationships,
|
|
2760
|
+
behaviors: entity.behaviors
|
|
2761
|
+
};
|
|
2762
|
+
}
|
|
2763
|
+
function mergeSuggestions(newSuggestions, existingManifest) {
|
|
2764
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2765
|
+
const existingSuggestions = existingManifest?.suggestions.transitive || [];
|
|
2766
|
+
const existingMap = /* @__PURE__ */ new Map();
|
|
2767
|
+
for (const existing of existingSuggestions) {
|
|
2768
|
+
existingMap.set(existing.id, existing);
|
|
2769
|
+
}
|
|
2770
|
+
const merged = [];
|
|
2771
|
+
for (const suggestion of newSuggestions) {
|
|
2772
|
+
const id = `${suggestion.path.source}->${suggestion.path.target}`;
|
|
2773
|
+
const existing = existingMap.get(id);
|
|
2774
|
+
if (existing) {
|
|
2775
|
+
merged.push({
|
|
2776
|
+
id,
|
|
2777
|
+
source: suggestion.path.source,
|
|
2778
|
+
target: suggestion.path.target,
|
|
2779
|
+
throughPath: suggestion.path.throughPath,
|
|
2780
|
+
suggestedName: suggestion.path.suggestedName,
|
|
2781
|
+
yamlSnippet: suggestion.path.yamlSnippet,
|
|
2782
|
+
status: existing.status,
|
|
2783
|
+
// Preserve status
|
|
2784
|
+
detectedAt: existing.detectedAt,
|
|
2785
|
+
// Preserve original detection time
|
|
2786
|
+
resolvedAt: existing.resolvedAt
|
|
2787
|
+
});
|
|
2788
|
+
existingMap.delete(id);
|
|
2789
|
+
} else {
|
|
2790
|
+
merged.push({
|
|
2791
|
+
id,
|
|
2792
|
+
source: suggestion.path.source,
|
|
2793
|
+
target: suggestion.path.target,
|
|
2794
|
+
throughPath: suggestion.path.throughPath,
|
|
2795
|
+
suggestedName: suggestion.path.suggestedName,
|
|
2796
|
+
yamlSnippet: suggestion.path.yamlSnippet,
|
|
2797
|
+
status: "pending",
|
|
2798
|
+
detectedAt: now
|
|
2799
|
+
});
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
for (const [id, existing] of existingMap) {
|
|
2803
|
+
if (existing.status !== "pending") {
|
|
2804
|
+
merged.push(existing);
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
return merged;
|
|
2808
|
+
}
|
|
2809
|
+
async function buildManifest(analysis, transitiveSuggestions, entitiesDir, existingManifest) {
|
|
2810
|
+
const entities = {};
|
|
2811
|
+
for (const entity of analysis.entities) {
|
|
2812
|
+
entities[entity.name] = toManifestEntity(entity);
|
|
2813
|
+
}
|
|
2814
|
+
const orphans = findOrphanEntities(analysis.graph);
|
|
2815
|
+
const cycles = findCircularDependencies(analysis.graph);
|
|
2816
|
+
const mergedSuggestions = mergeSuggestions(transitiveSuggestions, existingManifest);
|
|
2817
|
+
const entityFilesHash = await computeEntityFilesHash(entitiesDir);
|
|
2818
|
+
const manifest = {
|
|
2819
|
+
version: MANIFEST_VERSION,
|
|
2820
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2821
|
+
entityFilesHash,
|
|
2822
|
+
entities,
|
|
2823
|
+
graph: {
|
|
2824
|
+
edges: analysis.graph.edges.map((edge) => ({
|
|
2825
|
+
from: edge.from,
|
|
2826
|
+
to: edge.to,
|
|
2827
|
+
relationship: edge.relationship.name,
|
|
2828
|
+
cardinality: edge.cardinality === "N:M" ? "1:N" : edge.cardinality,
|
|
2829
|
+
bidirectional: edge.bidirectional
|
|
2830
|
+
})),
|
|
2831
|
+
orphans,
|
|
2832
|
+
cycles
|
|
2833
|
+
},
|
|
2834
|
+
suggestions: {
|
|
2835
|
+
transitive: mergedSuggestions
|
|
2836
|
+
},
|
|
2837
|
+
statistics: {
|
|
2838
|
+
totalEntities: analysis.statistics.totalEntities,
|
|
2839
|
+
totalFields: analysis.statistics.totalFields,
|
|
2840
|
+
totalRelationships: analysis.statistics.totalRelationships,
|
|
2841
|
+
transitivePathsDetected: transitiveSuggestions.length
|
|
2842
|
+
}
|
|
2843
|
+
};
|
|
2844
|
+
return manifest;
|
|
2845
|
+
}
|
|
2846
|
+
function updateSuggestionStatus(projectRoot, suggestionId, status) {
|
|
2847
|
+
const manifest = readManifest(projectRoot);
|
|
2848
|
+
if (!manifest) {
|
|
2849
|
+
return false;
|
|
2850
|
+
}
|
|
2851
|
+
const suggestion = manifest.suggestions.transitive.find((s) => s.id === suggestionId);
|
|
2852
|
+
if (!suggestion) {
|
|
2853
|
+
return false;
|
|
2854
|
+
}
|
|
2855
|
+
suggestion.status = status;
|
|
2856
|
+
suggestion.resolvedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2857
|
+
writeManifest(projectRoot, manifest);
|
|
2858
|
+
return true;
|
|
2859
|
+
}
|
|
2860
|
+
function updateAllSuggestionStatus(projectRoot, status) {
|
|
2861
|
+
const manifest = readManifest(projectRoot);
|
|
2862
|
+
if (!manifest) {
|
|
2863
|
+
return 0;
|
|
2864
|
+
}
|
|
2865
|
+
let count = 0;
|
|
2866
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2867
|
+
for (const suggestion of manifest.suggestions.transitive) {
|
|
2868
|
+
if (suggestion.status === "pending") {
|
|
2869
|
+
suggestion.status = status;
|
|
2870
|
+
suggestion.resolvedAt = now;
|
|
2871
|
+
count++;
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
if (count > 0) {
|
|
2875
|
+
writeManifest(projectRoot, manifest);
|
|
2876
|
+
}
|
|
2877
|
+
return count;
|
|
2878
|
+
}
|
|
2879
|
+
function getPendingSuggestions(projectRoot) {
|
|
2880
|
+
const manifest = readManifest(projectRoot);
|
|
2881
|
+
if (!manifest) {
|
|
2882
|
+
return [];
|
|
2883
|
+
}
|
|
2884
|
+
return manifest.suggestions.transitive.filter((s) => s.status === "pending");
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
// src/analyzer/serialize-graph.ts
|
|
2888
|
+
function serializeFields(fields) {
|
|
2889
|
+
const result = {};
|
|
2890
|
+
for (const [key, f] of fields) {
|
|
2891
|
+
result[key] = {
|
|
2892
|
+
name: f.name,
|
|
2893
|
+
type: f.type,
|
|
2894
|
+
required: f.required,
|
|
2895
|
+
nullable: f.nullable,
|
|
2896
|
+
unique: f.unique,
|
|
2897
|
+
index: f.index,
|
|
2898
|
+
foreignKey: f.foreignKey,
|
|
2899
|
+
choices: f.choices
|
|
2900
|
+
};
|
|
2901
|
+
}
|
|
2902
|
+
return result;
|
|
2903
|
+
}
|
|
2904
|
+
function serializeRelationships(rels) {
|
|
2905
|
+
const result = {};
|
|
2906
|
+
for (const [key, r] of rels) {
|
|
2907
|
+
result[key] = {
|
|
2908
|
+
name: r.name,
|
|
2909
|
+
type: r.type,
|
|
2910
|
+
target: r.target,
|
|
2911
|
+
foreignKey: r.foreignKey,
|
|
2912
|
+
inverse: r.inverse,
|
|
2913
|
+
through: r.through,
|
|
2914
|
+
resolved: r.resolved
|
|
2915
|
+
};
|
|
2916
|
+
}
|
|
2917
|
+
return result;
|
|
2918
|
+
}
|
|
2919
|
+
function serializeDomainGraph(graph) {
|
|
2920
|
+
const entities = {};
|
|
2921
|
+
for (const [key, entity] of graph.entities) {
|
|
2922
|
+
entities[key] = {
|
|
2923
|
+
name: entity.name,
|
|
2924
|
+
plural: entity.plural,
|
|
2925
|
+
table: entity.table,
|
|
2926
|
+
pattern: entity.pattern,
|
|
2927
|
+
patterns: entity.patterns,
|
|
2928
|
+
patternConfig: entity.patternConfig,
|
|
2929
|
+
fields: serializeFields(entity.fields),
|
|
2930
|
+
relationships: serializeRelationships(entity.relationships),
|
|
2931
|
+
behaviors: entity.behaviors,
|
|
2932
|
+
queries: entity.queries?.map((q) => ({
|
|
2933
|
+
by: q.by,
|
|
2934
|
+
unique: q.unique,
|
|
2935
|
+
order: q.order
|
|
2936
|
+
})),
|
|
2937
|
+
sourcePath: entity.sourcePath
|
|
2938
|
+
};
|
|
2939
|
+
}
|
|
2940
|
+
const relationshipDefinitions = {};
|
|
2941
|
+
for (const [key, relDef] of graph.relationshipDefinitions) {
|
|
2942
|
+
relationshipDefinitions[key] = {
|
|
2943
|
+
name: relDef.name,
|
|
2944
|
+
table: relDef.table,
|
|
2945
|
+
from: relDef.from,
|
|
2946
|
+
to: relDef.to,
|
|
2947
|
+
selfReferential: relDef.selfReferential,
|
|
2948
|
+
fromColumn: relDef.fromColumn,
|
|
2949
|
+
toColumn: relDef.toColumn,
|
|
2950
|
+
types: relDef.types,
|
|
2951
|
+
hasTypes: relDef.hasTypes,
|
|
2952
|
+
temporal: relDef.temporal,
|
|
2953
|
+
sourced: relDef.sourced,
|
|
2954
|
+
onDeleteFrom: relDef.onDeleteFrom,
|
|
2955
|
+
onDeleteTo: relDef.onDeleteTo,
|
|
2956
|
+
uniqueOn: relDef.uniqueOn,
|
|
2957
|
+
fields: serializeFields(relDef.fields),
|
|
2958
|
+
queries: relDef.queries?.map((q) => ({
|
|
2959
|
+
by: q.by,
|
|
2960
|
+
unique: q.unique,
|
|
2961
|
+
order: q.order
|
|
2962
|
+
})),
|
|
2963
|
+
sourcePath: relDef.sourcePath
|
|
2964
|
+
};
|
|
2965
|
+
}
|
|
2966
|
+
return {
|
|
2967
|
+
entities,
|
|
2968
|
+
relationshipDefinitions,
|
|
2969
|
+
edges: graph.edges.map((e) => ({
|
|
2970
|
+
from: e.from,
|
|
2971
|
+
to: e.to,
|
|
2972
|
+
relationship: {
|
|
2973
|
+
name: e.relationship.name,
|
|
2974
|
+
type: e.relationship.type,
|
|
2975
|
+
target: e.relationship.target,
|
|
2976
|
+
foreignKey: e.relationship.foreignKey,
|
|
2977
|
+
inverse: e.relationship.inverse,
|
|
2978
|
+
through: e.relationship.through,
|
|
2979
|
+
resolved: e.relationship.resolved
|
|
2980
|
+
},
|
|
2981
|
+
cardinality: e.cardinality,
|
|
2982
|
+
bidirectional: e.bidirectional
|
|
2983
|
+
}))
|
|
2984
|
+
};
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2987
|
+
// src/patterns/registry.ts
|
|
2988
|
+
import { glob } from "glob";
|
|
2989
|
+
import path from "path";
|
|
2990
|
+
import { pathToFileURL } from "url";
|
|
2991
|
+
|
|
2992
|
+
// src/patterns/pattern-definition.ts
|
|
2993
|
+
function definePattern(def) {
|
|
2994
|
+
return def;
|
|
2995
|
+
}
|
|
2996
|
+
function isPatternDefinition(val) {
|
|
2997
|
+
return typeof val === "object" && val !== null && "name" in val && typeof val.name === "string";
|
|
2998
|
+
}
|
|
2999
|
+
function isOrchestrationPattern(def) {
|
|
3000
|
+
return def.kind === "orchestration";
|
|
3001
|
+
}
|
|
3002
|
+
function isDomainPattern(def) {
|
|
3003
|
+
return !isOrchestrationPattern(def);
|
|
3004
|
+
}
|
|
3005
|
+
function defineOrchestrationPattern(def) {
|
|
3006
|
+
return def;
|
|
3007
|
+
}
|
|
3008
|
+
|
|
3009
|
+
// src/patterns/registry.ts
|
|
3010
|
+
var LIBRARY_PATTERNS = /* @__PURE__ */ new Map();
|
|
3011
|
+
var APP_PATTERNS = /* @__PURE__ */ new Map();
|
|
3012
|
+
var ORCHESTRATION_APP_PATTERNS = /* @__PURE__ */ new Map();
|
|
3013
|
+
function assertHasContribution(def) {
|
|
3014
|
+
const hasColumns = Array.isArray(def.columns) && def.columns.length > 0;
|
|
3015
|
+
const hasRepo = typeof def.repositoryClass === "string" && def.repositoryClass.length > 0;
|
|
3016
|
+
const hasService = typeof def.serviceClass === "string" && def.serviceClass.length > 0;
|
|
3017
|
+
if (!hasColumns && !hasRepo && !hasService) {
|
|
3018
|
+
throw new Error(
|
|
3019
|
+
`Pattern '${def.name}' contributes nothing \u2014 at least one of \`columns\`, \`repositoryClass\`, or \`serviceClass\` is required.`
|
|
3020
|
+
);
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
3023
|
+
function assertOrchestrationContribution(def) {
|
|
3024
|
+
if (!def.registry || typeof def.registry !== "object") {
|
|
3025
|
+
throw new Error(
|
|
3026
|
+
`Orchestration pattern '${def.name}' is missing a 'registry' field.`
|
|
3027
|
+
);
|
|
3028
|
+
}
|
|
3029
|
+
if (typeof def.registry.keyType !== "string" || def.registry.keyType.length === 0) {
|
|
3030
|
+
throw new Error(
|
|
3031
|
+
`Orchestration pattern '${def.name}' registry.keyType must be a non-empty string.`
|
|
3032
|
+
);
|
|
3033
|
+
}
|
|
3034
|
+
if (typeof def.registry.valueType !== "string" || def.registry.valueType.length === 0) {
|
|
3035
|
+
throw new Error(
|
|
3036
|
+
`Orchestration pattern '${def.name}' registry.valueType must be a non-empty string.`
|
|
3037
|
+
);
|
|
3038
|
+
}
|
|
3039
|
+
if (!Array.isArray(def.registry.entries) || def.registry.entries.length === 0) {
|
|
3040
|
+
throw new Error(
|
|
3041
|
+
`Orchestration pattern '${def.name}' registry.entries must contain at least one entry.`
|
|
3042
|
+
);
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
function registerLibraryPattern(def) {
|
|
3046
|
+
assertHasContribution(def);
|
|
3047
|
+
LIBRARY_PATTERNS.set(def.name, def);
|
|
3048
|
+
}
|
|
3049
|
+
function getPattern(name) {
|
|
3050
|
+
return APP_PATTERNS.get(name) ?? LIBRARY_PATTERNS.get(name);
|
|
3051
|
+
}
|
|
3052
|
+
function getAllPatternNames() {
|
|
3053
|
+
const set = /* @__PURE__ */ new Set([
|
|
3054
|
+
...LIBRARY_PATTERNS.keys(),
|
|
3055
|
+
...APP_PATTERNS.keys()
|
|
3056
|
+
]);
|
|
3057
|
+
return [...set].sort();
|
|
3058
|
+
}
|
|
3059
|
+
function getLibraryPatternNames() {
|
|
3060
|
+
return [...LIBRARY_PATTERNS.keys()].sort();
|
|
3061
|
+
}
|
|
3062
|
+
function getAppPatternNames() {
|
|
3063
|
+
return [...APP_PATTERNS.keys()].sort();
|
|
3064
|
+
}
|
|
3065
|
+
function getOrchestrationPattern(name) {
|
|
3066
|
+
return ORCHESTRATION_APP_PATTERNS.get(name);
|
|
3067
|
+
}
|
|
3068
|
+
function getOrchestrationPatternNames() {
|
|
3069
|
+
return [...ORCHESTRATION_APP_PATTERNS.keys()].sort();
|
|
3070
|
+
}
|
|
3071
|
+
function getAllOrchestrationPatterns() {
|
|
3072
|
+
return getOrchestrationPatternNames().map(
|
|
3073
|
+
(n) => ORCHESTRATION_APP_PATTERNS.get(n)
|
|
3074
|
+
);
|
|
3075
|
+
}
|
|
3076
|
+
async function loadAppPatterns(manifestPaths, cwd) {
|
|
3077
|
+
const loaded = /* @__PURE__ */ new Set();
|
|
3078
|
+
const errors = [];
|
|
3079
|
+
const files = /* @__PURE__ */ new Set();
|
|
3080
|
+
for (const raw of manifestPaths) {
|
|
3081
|
+
try {
|
|
3082
|
+
const expanded = await glob(raw, { cwd, absolute: true, nodir: true });
|
|
3083
|
+
for (const filePath of expanded) {
|
|
3084
|
+
files.add(filePath);
|
|
3085
|
+
}
|
|
3086
|
+
} catch (err) {
|
|
3087
|
+
errors.push(
|
|
3088
|
+
`Failed to expand pattern glob '${raw}': ${stringifyError(err)}`
|
|
3089
|
+
);
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
const sortedFiles = [...files].sort();
|
|
3093
|
+
for (const filePath of sortedFiles) {
|
|
3094
|
+
try {
|
|
3095
|
+
const mod = await import(pathToFileURL(filePath).href);
|
|
3096
|
+
for (const [key, val] of Object.entries(mod)) {
|
|
3097
|
+
if (!key.endsWith("Pattern")) continue;
|
|
3098
|
+
if (!isPatternDefinition(val)) continue;
|
|
3099
|
+
if (isOrchestrationPattern(val)) {
|
|
3100
|
+
const orch = val;
|
|
3101
|
+
try {
|
|
3102
|
+
assertOrchestrationContribution(orch);
|
|
3103
|
+
} catch (assertErr) {
|
|
3104
|
+
errors.push(
|
|
3105
|
+
`Orchestration pattern '${orch.name}' in ${relPath(filePath, cwd)} is invalid: ${stringifyError(assertErr)}`
|
|
3106
|
+
);
|
|
3107
|
+
continue;
|
|
3108
|
+
}
|
|
3109
|
+
const existingOrch = ORCHESTRATION_APP_PATTERNS.get(orch.name);
|
|
3110
|
+
if (existingOrch && existingOrch !== orch) {
|
|
3111
|
+
errors.push(
|
|
3112
|
+
`Orchestration pattern '${orch.name}' in ${relPath(filePath, cwd)} duplicates a previously loaded orchestration pattern. Pattern names must be unique.`
|
|
3113
|
+
);
|
|
3114
|
+
continue;
|
|
3115
|
+
}
|
|
3116
|
+
ORCHESTRATION_APP_PATTERNS.set(orch.name, orch);
|
|
3117
|
+
loaded.add(orch.name);
|
|
3118
|
+
} else {
|
|
3119
|
+
try {
|
|
3120
|
+
assertHasContribution(val);
|
|
3121
|
+
} catch (assertErr) {
|
|
3122
|
+
errors.push(
|
|
3123
|
+
`Pattern '${val.name}' in ${relPath(filePath, cwd)} is invalid: ${stringifyError(assertErr)}`
|
|
3124
|
+
);
|
|
3125
|
+
continue;
|
|
3126
|
+
}
|
|
3127
|
+
const existingDom = APP_PATTERNS.get(val.name);
|
|
3128
|
+
if (existingDom && existingDom !== val) {
|
|
3129
|
+
errors.push(
|
|
3130
|
+
`Pattern '${val.name}' in ${relPath(filePath, cwd)} duplicates a previously loaded app pattern. Pattern names must be unique.`
|
|
3131
|
+
);
|
|
3132
|
+
continue;
|
|
3133
|
+
}
|
|
3134
|
+
APP_PATTERNS.set(val.name, val);
|
|
3135
|
+
loaded.add(val.name);
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
} catch (err) {
|
|
3139
|
+
errors.push(
|
|
3140
|
+
`Failed to load pattern file '${relPath(filePath, cwd)}': ${stringifyError(err)}`
|
|
3141
|
+
);
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
return {
|
|
3145
|
+
loaded: [...loaded].sort(),
|
|
3146
|
+
errors
|
|
3147
|
+
};
|
|
3148
|
+
}
|
|
3149
|
+
function _resetRegistryForTests(opts = {}) {
|
|
3150
|
+
APP_PATTERNS.clear();
|
|
3151
|
+
ORCHESTRATION_APP_PATTERNS.clear();
|
|
3152
|
+
if (opts.includeLibrary) {
|
|
3153
|
+
LIBRARY_PATTERNS.clear();
|
|
3154
|
+
}
|
|
3155
|
+
}
|
|
3156
|
+
function stringifyError(err) {
|
|
3157
|
+
if (err instanceof Error) return err.message;
|
|
3158
|
+
return String(err);
|
|
3159
|
+
}
|
|
3160
|
+
function relPath(abs, cwd) {
|
|
3161
|
+
try {
|
|
3162
|
+
return path.relative(cwd, abs) || abs;
|
|
3163
|
+
} catch {
|
|
3164
|
+
return abs;
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
|
|
3168
|
+
// src/patterns/validate-composition.ts
|
|
3169
|
+
function validatePatternComposition(entity) {
|
|
3170
|
+
const issues = [];
|
|
3171
|
+
const patternNames = entity.patterns ?? (entity.pattern ? [entity.pattern] : []);
|
|
3172
|
+
if (patternNames.length === 0) return issues;
|
|
3173
|
+
const columnSources = /* @__PURE__ */ new Map();
|
|
3174
|
+
for (const [name] of entity.fields) {
|
|
3175
|
+
columnSources.set(name, `entity field '${name}'`);
|
|
3176
|
+
}
|
|
3177
|
+
const behaviorFields = resolveBehaviorFields(entity.behaviors);
|
|
3178
|
+
for (const bf of behaviorFields) {
|
|
3179
|
+
const existing = columnSources.get(bf.name);
|
|
3180
|
+
if (existing) {
|
|
3181
|
+
issues.push({
|
|
3182
|
+
severity: "error",
|
|
3183
|
+
type: "pattern_column_conflict",
|
|
3184
|
+
entity: entity.name,
|
|
3185
|
+
message: `Behavior-contributed field '${bf.name}' conflicts with ${existing}.`
|
|
3186
|
+
});
|
|
3187
|
+
continue;
|
|
3188
|
+
}
|
|
3189
|
+
columnSources.set(bf.name, `behavior field '${bf.name}'`);
|
|
3190
|
+
}
|
|
3191
|
+
const impliedBehaviors = new Set(entity.behaviors);
|
|
3192
|
+
for (const patternName of patternNames) {
|
|
3193
|
+
const def = getPattern(patternName);
|
|
3194
|
+
if (!def) {
|
|
3195
|
+
issues.push({
|
|
3196
|
+
severity: "error",
|
|
3197
|
+
type: "pattern_unknown",
|
|
3198
|
+
entity: entity.name,
|
|
3199
|
+
message: `Unknown pattern '${patternName}'. Library patterns are pre-registered; app patterns are loaded from globs in codegen.config.yaml 'patterns:' (default 'src/patterns/*.pattern.ts').`
|
|
3200
|
+
});
|
|
3201
|
+
continue;
|
|
3202
|
+
}
|
|
3203
|
+
if (def.configSchema) {
|
|
3204
|
+
const rawConfig = entity.patternConfig?.[patternName];
|
|
3205
|
+
const result = def.configSchema.safeParse(rawConfig ?? {});
|
|
3206
|
+
if (!result.success) {
|
|
3207
|
+
const detail = result.error.issues.map((i) => `${i.path.join(".") || "<root>"}: ${i.message}`).join(", ");
|
|
3208
|
+
issues.push({
|
|
3209
|
+
severity: "error",
|
|
3210
|
+
type: "pattern_config_invalid",
|
|
3211
|
+
entity: entity.name,
|
|
3212
|
+
message: `Pattern '${patternName}' config failed validation: ${detail}`
|
|
3213
|
+
});
|
|
3214
|
+
}
|
|
3215
|
+
}
|
|
3216
|
+
for (const col of def.columns ?? []) {
|
|
3217
|
+
const existing = columnSources.get(col.name);
|
|
3218
|
+
if (existing) {
|
|
3219
|
+
issues.push({
|
|
3220
|
+
severity: "error",
|
|
3221
|
+
type: "pattern_column_conflict",
|
|
3222
|
+
entity: entity.name,
|
|
3223
|
+
message: `Pattern '${patternName}' contributes column '${col.name}' which conflicts with ${existing}.`
|
|
3224
|
+
});
|
|
3225
|
+
continue;
|
|
3226
|
+
}
|
|
3227
|
+
columnSources.set(col.name, `pattern '${patternName}'`);
|
|
3228
|
+
}
|
|
3229
|
+
for (const b of def.impliedBehaviors ?? []) {
|
|
3230
|
+
impliedBehaviors.add(b);
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
if (entity.patternConfig) {
|
|
3234
|
+
const declared = new Set(patternNames);
|
|
3235
|
+
for (const key of Object.keys(entity.patternConfig)) {
|
|
3236
|
+
if (!declared.has(key)) {
|
|
3237
|
+
issues.push({
|
|
3238
|
+
severity: "warning",
|
|
3239
|
+
type: "pattern_config_unused",
|
|
3240
|
+
entity: entity.name,
|
|
3241
|
+
message: `Config block has key '${key}' but pattern '${key}' is not declared in 'pattern:' or 'patterns:'. Remove the entry or add the pattern.`
|
|
3242
|
+
});
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
return issues;
|
|
3247
|
+
}
|
|
3248
|
+
function validatePatternProject(ctx) {
|
|
3249
|
+
const issues = [];
|
|
3250
|
+
if (ctx.architecture === "clean") {
|
|
3251
|
+
const withPatterns = ctx.entities.filter(
|
|
3252
|
+
(e) => e.patterns && e.patterns.length > 0 || !!e.pattern
|
|
3253
|
+
);
|
|
3254
|
+
for (const e of withPatterns) {
|
|
3255
|
+
issues.push({
|
|
3256
|
+
severity: "warning",
|
|
3257
|
+
type: "pattern_clean_pipeline_noop",
|
|
3258
|
+
entity: e.name,
|
|
3259
|
+
message: `'pattern:' is declared but 'generate.architecture: clean' does not yet consume patterns. This declaration is a no-op. Patterns are consumed by 'clean-lite-ps' today; 'clean' integration is Phase 3+ additive work (ADR-031).`
|
|
3260
|
+
});
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
3263
|
+
return issues;
|
|
3264
|
+
}
|
|
3265
|
+
|
|
3266
|
+
// src/patterns/validate-orchestration.ts
|
|
3267
|
+
var PROJECT_SENTINEL = "<project>";
|
|
3268
|
+
function validateOrchestrationProject(ctx) {
|
|
3269
|
+
const issues = [];
|
|
3270
|
+
const domainNameSet = new Set(ctx.domainPatternNames);
|
|
3271
|
+
for (const orch of ctx.orchestrationPatterns) {
|
|
3272
|
+
if (domainNameSet.has(orch.name)) {
|
|
3273
|
+
issues.push({
|
|
3274
|
+
severity: "error",
|
|
3275
|
+
type: "pattern_name_collision",
|
|
3276
|
+
entity: PROJECT_SENTINEL,
|
|
3277
|
+
message: `Orchestration pattern '${orch.name}' shares a name with a domain pattern. Pattern names are globally unique across kinds (ADR-032 \xA7Composition rules).`
|
|
3278
|
+
});
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
for (const orch of ctx.orchestrationPatterns) {
|
|
3282
|
+
const allRegistries = [
|
|
3283
|
+
orch.registry,
|
|
3284
|
+
...orch.coKeyedRegistries ?? []
|
|
3285
|
+
];
|
|
3286
|
+
for (const reg of allRegistries) {
|
|
3287
|
+
if (!Array.isArray(reg.entries) || reg.entries.length === 0) {
|
|
3288
|
+
issues.push({
|
|
3289
|
+
severity: "error",
|
|
3290
|
+
type: "pattern_entries_empty",
|
|
3291
|
+
entity: PROJECT_SENTINEL,
|
|
3292
|
+
message: `Orchestration pattern '${orch.name}' declares a registry with no entries. Provide at least one { key, provider } pair.`
|
|
3293
|
+
});
|
|
3294
|
+
continue;
|
|
3295
|
+
}
|
|
3296
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3297
|
+
for (const entry of reg.entries) {
|
|
3298
|
+
if (typeof entry.key !== "string" || entry.key.length === 0) {
|
|
3299
|
+
issues.push({
|
|
3300
|
+
severity: "error",
|
|
3301
|
+
type: "pattern_entry_malformed",
|
|
3302
|
+
entity: PROJECT_SENTINEL,
|
|
3303
|
+
message: `Orchestration pattern '${orch.name}' has an entry with a missing or non-string 'key'.`
|
|
3304
|
+
});
|
|
3305
|
+
continue;
|
|
3306
|
+
}
|
|
3307
|
+
if (typeof entry.provider !== "string" || entry.provider.length === 0) {
|
|
3308
|
+
issues.push({
|
|
3309
|
+
severity: "error",
|
|
3310
|
+
type: "pattern_entry_malformed",
|
|
3311
|
+
entity: PROJECT_SENTINEL,
|
|
3312
|
+
message: `Orchestration pattern '${orch.name}' entry '${entry.key}' has a missing or non-string 'provider'.`
|
|
3313
|
+
});
|
|
3314
|
+
continue;
|
|
3315
|
+
}
|
|
3316
|
+
if (seen.has(entry.key)) {
|
|
3317
|
+
issues.push({
|
|
3318
|
+
severity: "error",
|
|
3319
|
+
type: "pattern_entry_key_duplicate",
|
|
3320
|
+
entity: PROJECT_SENTINEL,
|
|
3321
|
+
message: `Orchestration pattern '${orch.name}' has duplicate entry key '${entry.key}'. Keys must be unique within a registry.`
|
|
3322
|
+
});
|
|
3323
|
+
continue;
|
|
3324
|
+
}
|
|
3325
|
+
seen.add(entry.key);
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
if (orch.coKeyedRegistries && orch.coKeyedRegistries.length > 0) {
|
|
3329
|
+
const primaryKeyType = orch.registry.keyType;
|
|
3330
|
+
for (const reg of orch.coKeyedRegistries) {
|
|
3331
|
+
if (reg.keyType !== primaryKeyType) {
|
|
3332
|
+
issues.push({
|
|
3333
|
+
severity: "error",
|
|
3334
|
+
type: "pattern_cokeyed_keytype_mismatch",
|
|
3335
|
+
entity: PROJECT_SENTINEL,
|
|
3336
|
+
message: `Orchestration pattern '${orch.name}' co-keyed registry has keyType '${reg.keyType}', expected '${primaryKeyType}'. Co-keyed registries must share the primary registry's key space (ADR-032 Decision 2).`
|
|
3337
|
+
});
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
return issues;
|
|
3343
|
+
}
|
|
3344
|
+
|
|
3345
|
+
// src/formatters/console-formatter.ts
|
|
3346
|
+
var colors = {
|
|
3347
|
+
reset: "\x1B[0m",
|
|
3348
|
+
bold: "\x1B[1m",
|
|
3349
|
+
dim: "\x1B[2m",
|
|
3350
|
+
red: "\x1B[31m",
|
|
3351
|
+
green: "\x1B[32m",
|
|
3352
|
+
yellow: "\x1B[33m",
|
|
3353
|
+
blue: "\x1B[34m",
|
|
3354
|
+
magenta: "\x1B[35m",
|
|
3355
|
+
cyan: "\x1B[36m",
|
|
3356
|
+
white: "\x1B[37m",
|
|
3357
|
+
bgRed: "\x1B[41m",
|
|
3358
|
+
bgGreen: "\x1B[42m",
|
|
3359
|
+
bgYellow: "\x1B[43m"
|
|
3360
|
+
};
|
|
3361
|
+
function color(text, ...styles) {
|
|
3362
|
+
return `${styles.join("")}${text}${colors.reset}`;
|
|
3363
|
+
}
|
|
3364
|
+
function severityColor(severity) {
|
|
3365
|
+
switch (severity) {
|
|
3366
|
+
case "error":
|
|
3367
|
+
return colors.red;
|
|
3368
|
+
case "warning":
|
|
3369
|
+
return colors.yellow;
|
|
3370
|
+
case "info":
|
|
3371
|
+
return colors.cyan;
|
|
3372
|
+
}
|
|
3373
|
+
}
|
|
3374
|
+
function severityIcon(severity) {
|
|
3375
|
+
switch (severity) {
|
|
3376
|
+
case "error":
|
|
3377
|
+
return "X";
|
|
3378
|
+
case "warning":
|
|
3379
|
+
return "!";
|
|
3380
|
+
case "info":
|
|
3381
|
+
return "i";
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
3384
|
+
function formatConsole(result) {
|
|
3385
|
+
const lines = [];
|
|
3386
|
+
lines.push("");
|
|
3387
|
+
lines.push(color("=".repeat(60), colors.dim));
|
|
3388
|
+
lines.push(color(" Domain Analysis Report", colors.bold, colors.cyan));
|
|
3389
|
+
lines.push(color("=".repeat(60), colors.dim));
|
|
3390
|
+
lines.push("");
|
|
3391
|
+
lines.push(...formatStatistics(result));
|
|
3392
|
+
lines.push(...formatEntities(result));
|
|
3393
|
+
lines.push(...formatRelationships(result));
|
|
3394
|
+
if (result.issues.length > 0) {
|
|
3395
|
+
lines.push(...formatIssues(result.issues));
|
|
3396
|
+
}
|
|
3397
|
+
lines.push("");
|
|
3398
|
+
lines.push(color("-".repeat(60), colors.dim));
|
|
3399
|
+
const errors = result.issues.filter((i) => i.severity === "error");
|
|
3400
|
+
const warnings = result.issues.filter((i) => i.severity === "warning");
|
|
3401
|
+
const infos = result.issues.filter((i) => i.severity === "info");
|
|
3402
|
+
if (result.isValid) {
|
|
3403
|
+
lines.push(
|
|
3404
|
+
color(
|
|
3405
|
+
`[OK] Domain is valid (${warnings.length} warnings, ${infos.length} info)`,
|
|
3406
|
+
colors.green
|
|
3407
|
+
)
|
|
3408
|
+
);
|
|
3409
|
+
} else {
|
|
3410
|
+
lines.push(color(`[FAIL] Domain has ${errors.length} errors`, colors.red));
|
|
3411
|
+
}
|
|
3412
|
+
lines.push(color("-".repeat(60), colors.dim));
|
|
3413
|
+
lines.push("");
|
|
3414
|
+
return lines.join("\n");
|
|
3415
|
+
}
|
|
3416
|
+
function formatStatistics(result) {
|
|
3417
|
+
const lines = [];
|
|
3418
|
+
const stats = result.statistics;
|
|
3419
|
+
lines.push(color("Statistics:", colors.bold));
|
|
3420
|
+
lines.push("");
|
|
3421
|
+
lines.push(` Entities: ${stats.totalEntities}`);
|
|
3422
|
+
lines.push(
|
|
3423
|
+
` Fields: ${stats.totalFields} (avg ${stats.averageFieldsPerEntity.toFixed(1)}/entity)`
|
|
3424
|
+
);
|
|
3425
|
+
lines.push(` Relationships: ${stats.totalRelationships}`);
|
|
3426
|
+
lines.push(` With behaviors: ${stats.entitiesWithBehaviors}`);
|
|
3427
|
+
lines.push("");
|
|
3428
|
+
lines.push(" Field types:");
|
|
3429
|
+
const sortedTypes = Object.entries(stats.fieldsByType).sort((a, b) => b[1] - a[1]);
|
|
3430
|
+
for (const [type, count] of sortedTypes) {
|
|
3431
|
+
const bar = color("|".repeat(Math.min(count, 20)), colors.blue);
|
|
3432
|
+
lines.push(` ${type.padEnd(12)} ${bar} ${count}`);
|
|
3433
|
+
}
|
|
3434
|
+
lines.push("");
|
|
3435
|
+
if (stats.totalRelationships > 0) {
|
|
3436
|
+
lines.push(" Relationship types:");
|
|
3437
|
+
const sortedRels = Object.entries(stats.relationshipsByType).sort((a, b) => b[1] - a[1]);
|
|
3438
|
+
for (const [type, count] of sortedRels) {
|
|
3439
|
+
const bar = color("|".repeat(Math.min(count, 20)), colors.magenta);
|
|
3440
|
+
lines.push(` ${type.padEnd(12)} ${bar} ${count}`);
|
|
3441
|
+
}
|
|
3442
|
+
lines.push("");
|
|
3443
|
+
}
|
|
3444
|
+
return lines;
|
|
3445
|
+
}
|
|
3446
|
+
function formatEntities(result) {
|
|
3447
|
+
const lines = [];
|
|
3448
|
+
lines.push(color("Entities:", colors.bold));
|
|
3449
|
+
lines.push("");
|
|
3450
|
+
for (const entity of result.entities) {
|
|
3451
|
+
const fieldCount = entity.fields.size;
|
|
3452
|
+
const relCount = entity.relationships.size;
|
|
3453
|
+
lines.push(
|
|
3454
|
+
` ${color(entity.name, colors.cyan)} (${fieldCount} fields, ${relCount} relationships)`
|
|
3455
|
+
);
|
|
3456
|
+
if (entity.behaviors.length > 0) {
|
|
3457
|
+
lines.push(color(` behaviors: ${entity.behaviors.join(", ")}`, colors.dim));
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
lines.push("");
|
|
3461
|
+
return lines;
|
|
3462
|
+
}
|
|
3463
|
+
function formatRelationships(result) {
|
|
3464
|
+
const lines = [];
|
|
3465
|
+
if (result.graph.edges.length === 0) {
|
|
3466
|
+
return lines;
|
|
3467
|
+
}
|
|
3468
|
+
lines.push(color("Relationships:", colors.bold));
|
|
3469
|
+
lines.push("");
|
|
3470
|
+
for (const edge of result.graph.edges) {
|
|
3471
|
+
const arrow = getCardinalityArrow(edge.cardinality);
|
|
3472
|
+
const bidir = edge.bidirectional ? color(" (bidirectional)", colors.dim) : "";
|
|
3473
|
+
lines.push(
|
|
3474
|
+
` ${edge.from.padEnd(20)} ${arrow} ${edge.to} ${color(`(${edge.relationship.type})`, colors.dim)}${bidir}`
|
|
3475
|
+
);
|
|
3476
|
+
}
|
|
3477
|
+
lines.push("");
|
|
3478
|
+
return lines;
|
|
3479
|
+
}
|
|
3480
|
+
function getCardinalityArrow(cardinality) {
|
|
3481
|
+
switch (cardinality) {
|
|
3482
|
+
case "1:N":
|
|
3483
|
+
return color("--<", colors.magenta);
|
|
3484
|
+
case "N:1":
|
|
3485
|
+
return color(">--", colors.magenta);
|
|
3486
|
+
case "1:1":
|
|
3487
|
+
return color("---", colors.magenta);
|
|
3488
|
+
case "N:M":
|
|
3489
|
+
return color(">-<", colors.magenta);
|
|
3490
|
+
default:
|
|
3491
|
+
return color("-->", colors.magenta);
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3494
|
+
function formatIssues(issues) {
|
|
3495
|
+
const lines = [];
|
|
3496
|
+
const errors = issues.filter((i) => i.severity === "error");
|
|
3497
|
+
const warnings = issues.filter((i) => i.severity === "warning");
|
|
3498
|
+
const infos = issues.filter((i) => i.severity === "info");
|
|
3499
|
+
if (errors.length > 0) {
|
|
3500
|
+
lines.push(color(`Errors (${errors.length}):`, colors.bold, colors.red));
|
|
3501
|
+
lines.push("");
|
|
3502
|
+
lines.push(...formatIssueList(errors, "error"));
|
|
3503
|
+
lines.push("");
|
|
3504
|
+
}
|
|
3505
|
+
if (warnings.length > 0) {
|
|
3506
|
+
lines.push(color(`Warnings (${warnings.length}):`, colors.bold, colors.yellow));
|
|
3507
|
+
lines.push("");
|
|
3508
|
+
lines.push(...formatIssueList(warnings, "warning"));
|
|
3509
|
+
lines.push("");
|
|
3510
|
+
}
|
|
3511
|
+
if (infos.length > 0) {
|
|
3512
|
+
lines.push(color(`Info (${infos.length}):`, colors.bold, colors.cyan));
|
|
3513
|
+
lines.push("");
|
|
3514
|
+
lines.push(...formatIssueList(infos, "info", 5));
|
|
3515
|
+
lines.push("");
|
|
3516
|
+
}
|
|
3517
|
+
return lines;
|
|
3518
|
+
}
|
|
3519
|
+
function formatIssueList(issues, severity, limit) {
|
|
3520
|
+
const lines = [];
|
|
3521
|
+
const displayIssues = limit ? issues.slice(0, limit) : issues;
|
|
3522
|
+
const byType = /* @__PURE__ */ new Map();
|
|
3523
|
+
for (const issue of displayIssues) {
|
|
3524
|
+
const list = byType.get(issue.type) ?? [];
|
|
3525
|
+
list.push(issue);
|
|
3526
|
+
byType.set(issue.type, list);
|
|
3527
|
+
}
|
|
3528
|
+
for (const [type, typeIssues] of byType) {
|
|
3529
|
+
lines.push(color(` ${type} (${typeIssues.length}):`, colors.dim));
|
|
3530
|
+
for (const issue of typeIssues.slice(0, 5)) {
|
|
3531
|
+
const location = issue.entity ? `${issue.entity}${issue.field ? "." + issue.field : ""}` : issue.path ?? "unknown";
|
|
3532
|
+
const icon = color(`[${severityIcon(severity)}]`, severityColor(severity));
|
|
3533
|
+
lines.push(` ${icon} ${color(location, colors.bold)}: ${issue.message}`);
|
|
3534
|
+
if (issue.suggestion) {
|
|
3535
|
+
lines.push(color(` -> ${issue.suggestion}`, colors.dim));
|
|
3536
|
+
}
|
|
3537
|
+
}
|
|
3538
|
+
if (typeIssues.length > 5) {
|
|
3539
|
+
lines.push(color(` ... and ${typeIssues.length - 5} more`, colors.dim));
|
|
3540
|
+
}
|
|
3541
|
+
}
|
|
3542
|
+
if (limit && issues.length > limit) {
|
|
3543
|
+
lines.push(color(` ... and ${issues.length - limit} more info messages`, colors.dim));
|
|
3544
|
+
}
|
|
3545
|
+
return lines;
|
|
3546
|
+
}
|
|
3547
|
+
|
|
3548
|
+
// src/formatters/json-formatter.ts
|
|
3549
|
+
function mapToObject(map) {
|
|
3550
|
+
const obj = {};
|
|
3551
|
+
for (const [key, value] of map) {
|
|
3552
|
+
obj[key] = value;
|
|
3553
|
+
}
|
|
3554
|
+
return obj;
|
|
3555
|
+
}
|
|
3556
|
+
function serializeEntity(entity) {
|
|
3557
|
+
return {
|
|
3558
|
+
name: entity.name,
|
|
3559
|
+
plural: entity.plural,
|
|
3560
|
+
table: entity.table,
|
|
3561
|
+
folderStructure: entity.folderStructure,
|
|
3562
|
+
fields: mapToObject(entity.fields),
|
|
3563
|
+
relationships: mapToObject(entity.relationships),
|
|
3564
|
+
behaviors: entity.behaviors,
|
|
3565
|
+
sourcePath: entity.sourcePath
|
|
3566
|
+
};
|
|
3567
|
+
}
|
|
3568
|
+
function serializeGraph(graph) {
|
|
3569
|
+
const entities = {};
|
|
3570
|
+
for (const [name, entity] of graph.entities) {
|
|
3571
|
+
entities[name] = serializeEntity(entity);
|
|
3572
|
+
}
|
|
3573
|
+
return {
|
|
3574
|
+
entities,
|
|
3575
|
+
edges: graph.edges
|
|
3576
|
+
};
|
|
3577
|
+
}
|
|
3578
|
+
function formatJson(result, pretty = true) {
|
|
3579
|
+
const output = {
|
|
3580
|
+
isValid: result.isValid,
|
|
3581
|
+
summary: {
|
|
3582
|
+
entities: result.statistics.totalEntities,
|
|
3583
|
+
fields: result.statistics.totalFields,
|
|
3584
|
+
relationships: result.statistics.totalRelationships,
|
|
3585
|
+
errors: result.issues.filter((i) => i.severity === "error").length,
|
|
3586
|
+
warnings: result.issues.filter((i) => i.severity === "warning").length,
|
|
3587
|
+
info: result.issues.filter((i) => i.severity === "info").length
|
|
3588
|
+
},
|
|
3589
|
+
entities: result.entities.map(serializeEntity),
|
|
3590
|
+
graph: serializeGraph(result.graph),
|
|
3591
|
+
issues: result.issues,
|
|
3592
|
+
statistics: result.statistics,
|
|
3593
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3594
|
+
};
|
|
3595
|
+
return pretty ? JSON.stringify(output, null, 2) : JSON.stringify(output);
|
|
3596
|
+
}
|
|
3597
|
+
function formatStatsJson(result, pretty = true) {
|
|
3598
|
+
const output = {
|
|
3599
|
+
statistics: result.statistics,
|
|
3600
|
+
isValid: result.isValid,
|
|
3601
|
+
issueCount: {
|
|
3602
|
+
errors: result.issues.filter((i) => i.severity === "error").length,
|
|
3603
|
+
warnings: result.issues.filter((i) => i.severity === "warning").length,
|
|
3604
|
+
info: result.issues.filter((i) => i.severity === "info").length
|
|
3605
|
+
},
|
|
3606
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3607
|
+
};
|
|
3608
|
+
return pretty ? JSON.stringify(output, null, 2) : JSON.stringify(output);
|
|
3609
|
+
}
|
|
3610
|
+
|
|
3611
|
+
// src/formatters/markdown-formatter.ts
|
|
3612
|
+
function formatMarkdown(result) {
|
|
3613
|
+
const lines = [];
|
|
3614
|
+
lines.push("# Domain Model Documentation");
|
|
3615
|
+
lines.push("");
|
|
3616
|
+
lines.push(`Generated: ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
3617
|
+
lines.push("");
|
|
3618
|
+
lines.push("## Overview");
|
|
3619
|
+
lines.push("");
|
|
3620
|
+
lines.push("| Metric | Value |");
|
|
3621
|
+
lines.push("|--------|-------|");
|
|
3622
|
+
lines.push(`| Entities | ${result.statistics.totalEntities} |`);
|
|
3623
|
+
lines.push(`| Total Fields | ${result.statistics.totalFields} |`);
|
|
3624
|
+
lines.push(`| Total Relationships | ${result.statistics.totalRelationships} |`);
|
|
3625
|
+
lines.push(
|
|
3626
|
+
`| Avg Fields/Entity | ${result.statistics.averageFieldsPerEntity.toFixed(1)} |`
|
|
3627
|
+
);
|
|
3628
|
+
lines.push("");
|
|
3629
|
+
lines.push("### Field Type Distribution");
|
|
3630
|
+
lines.push("");
|
|
3631
|
+
lines.push("| Type | Count |");
|
|
3632
|
+
lines.push("|------|-------|");
|
|
3633
|
+
const sortedTypes = Object.entries(result.statistics.fieldsByType).sort(
|
|
3634
|
+
(a, b) => b[1] - a[1]
|
|
3635
|
+
);
|
|
3636
|
+
for (const [type, count] of sortedTypes) {
|
|
3637
|
+
lines.push(`| ${type} | ${count} |`);
|
|
3638
|
+
}
|
|
3639
|
+
lines.push("");
|
|
3640
|
+
if (result.statistics.totalRelationships > 0) {
|
|
3641
|
+
lines.push("### Relationship Type Distribution");
|
|
3642
|
+
lines.push("");
|
|
3643
|
+
lines.push("| Type | Count |");
|
|
3644
|
+
lines.push("|------|-------|");
|
|
3645
|
+
const sortedRels = Object.entries(result.statistics.relationshipsByType).sort(
|
|
3646
|
+
(a, b) => b[1] - a[1]
|
|
3647
|
+
);
|
|
3648
|
+
for (const [type, count] of sortedRels) {
|
|
3649
|
+
lines.push(`| ${type} | ${count} |`);
|
|
3650
|
+
}
|
|
3651
|
+
lines.push("");
|
|
3652
|
+
}
|
|
3653
|
+
lines.push("## Entity Relationship Diagram");
|
|
3654
|
+
lines.push("");
|
|
3655
|
+
lines.push("```mermaid");
|
|
3656
|
+
lines.push(...generateMermaidErDiagram(result));
|
|
3657
|
+
lines.push("```");
|
|
3658
|
+
lines.push("");
|
|
3659
|
+
lines.push("## Entities");
|
|
3660
|
+
lines.push("");
|
|
3661
|
+
for (const entity of result.entities) {
|
|
3662
|
+
lines.push(...formatEntitySection(entity));
|
|
3663
|
+
}
|
|
3664
|
+
const errors = result.issues.filter((i) => i.severity === "error");
|
|
3665
|
+
const warnings = result.issues.filter((i) => i.severity === "warning");
|
|
3666
|
+
const infos = result.issues.filter((i) => i.severity === "info");
|
|
3667
|
+
if (result.issues.length > 0) {
|
|
3668
|
+
lines.push("## Analysis Issues");
|
|
3669
|
+
lines.push("");
|
|
3670
|
+
if (errors.length > 0) {
|
|
3671
|
+
lines.push("### Errors");
|
|
3672
|
+
lines.push("");
|
|
3673
|
+
for (const issue of errors) {
|
|
3674
|
+
const location = issue.entity ? `**${issue.entity}${issue.field ? "." + issue.field : ""}**` : issue.path ?? "unknown";
|
|
3675
|
+
lines.push(`- [${issue.type}] ${location}: ${issue.message}`);
|
|
3676
|
+
if (issue.suggestion) {
|
|
3677
|
+
lines.push(` - Suggestion: ${issue.suggestion}`);
|
|
3678
|
+
}
|
|
3679
|
+
}
|
|
3680
|
+
lines.push("");
|
|
3681
|
+
}
|
|
3682
|
+
if (warnings.length > 0) {
|
|
3683
|
+
lines.push("### Warnings");
|
|
3684
|
+
lines.push("");
|
|
3685
|
+
for (const issue of warnings) {
|
|
3686
|
+
const location = issue.entity ? `**${issue.entity}${issue.field ? "." + issue.field : ""}**` : issue.path ?? "unknown";
|
|
3687
|
+
lines.push(`- [${issue.type}] ${location}: ${issue.message}`);
|
|
3688
|
+
if (issue.suggestion) {
|
|
3689
|
+
lines.push(` - Suggestion: ${issue.suggestion}`);
|
|
3690
|
+
}
|
|
3691
|
+
}
|
|
3692
|
+
lines.push("");
|
|
3693
|
+
}
|
|
3694
|
+
if (infos.length > 0) {
|
|
3695
|
+
lines.push("### Info");
|
|
3696
|
+
lines.push("");
|
|
3697
|
+
lines.push("<details>");
|
|
3698
|
+
lines.push("<summary>Show info messages</summary>");
|
|
3699
|
+
lines.push("");
|
|
3700
|
+
for (const issue of infos) {
|
|
3701
|
+
const location = issue.entity ? `**${issue.entity}${issue.field ? "." + issue.field : ""}**` : issue.path ?? "unknown";
|
|
3702
|
+
lines.push(`- [${issue.type}] ${location}: ${issue.message}`);
|
|
3703
|
+
}
|
|
3704
|
+
lines.push("");
|
|
3705
|
+
lines.push("</details>");
|
|
3706
|
+
lines.push("");
|
|
3707
|
+
}
|
|
3708
|
+
}
|
|
3709
|
+
return lines.join("\n");
|
|
3710
|
+
}
|
|
3711
|
+
function formatEntitySection(entity) {
|
|
3712
|
+
const lines = [];
|
|
3713
|
+
lines.push(`### ${entity.name}`);
|
|
3714
|
+
lines.push("");
|
|
3715
|
+
lines.push(`**Table:** \`${entity.table}\``);
|
|
3716
|
+
lines.push(`**Plural:** ${entity.plural}`);
|
|
3717
|
+
if (entity.behaviors.length > 0) {
|
|
3718
|
+
lines.push(`**Behaviors:** ${entity.behaviors.join(", ")}`);
|
|
3719
|
+
}
|
|
3720
|
+
lines.push("");
|
|
3721
|
+
lines.push("#### Fields");
|
|
3722
|
+
lines.push("");
|
|
3723
|
+
lines.push("| Name | Type | Required | Nullable | Index | Foreign Key |");
|
|
3724
|
+
lines.push("|------|------|----------|----------|-------|-------------|");
|
|
3725
|
+
for (const [name, field] of entity.fields) {
|
|
3726
|
+
const required = field.required ? "Yes" : "";
|
|
3727
|
+
const nullable = field.nullable ? "Yes" : "";
|
|
3728
|
+
const index = field.index ? "Yes" : field.unique ? "Unique" : "";
|
|
3729
|
+
const fk = field.foreignKey ? `${field.foreignKey.table}.${field.foreignKey.column}` : "";
|
|
3730
|
+
lines.push(`| ${name} | ${field.type} | ${required} | ${nullable} | ${index} | ${fk} |`);
|
|
3731
|
+
}
|
|
3732
|
+
lines.push("");
|
|
3733
|
+
if (entity.relationships.size > 0) {
|
|
3734
|
+
lines.push("#### Relationships");
|
|
3735
|
+
lines.push("");
|
|
3736
|
+
lines.push("| Name | Type | Target | Foreign Key |");
|
|
3737
|
+
lines.push("|------|------|--------|-------------|");
|
|
3738
|
+
for (const [name, rel] of entity.relationships) {
|
|
3739
|
+
lines.push(`| ${name} | ${rel.type} | ${rel.target} | ${rel.foreignKey} |`);
|
|
3740
|
+
}
|
|
3741
|
+
lines.push("");
|
|
3742
|
+
}
|
|
3743
|
+
return lines;
|
|
3744
|
+
}
|
|
3745
|
+
function generateMermaidErDiagram(result) {
|
|
3746
|
+
const lines = [];
|
|
3747
|
+
lines.push("erDiagram");
|
|
3748
|
+
for (const entity of result.entities) {
|
|
3749
|
+
const entityName = entity.name.toUpperCase();
|
|
3750
|
+
lines.push(` ${entityName} {`);
|
|
3751
|
+
const keyFields = Array.from(entity.fields.entries()).filter(
|
|
3752
|
+
([name, field]) => field.foreignKey || field.unique || field.index || name === "id" || name === "name"
|
|
3753
|
+
).slice(0, 6);
|
|
3754
|
+
for (const [name, field] of keyFields) {
|
|
3755
|
+
const typeStr = field.type;
|
|
3756
|
+
const pk = name === "id" ? "PK" : "";
|
|
3757
|
+
const fk = field.foreignKey ? "FK" : "";
|
|
3758
|
+
const marker = pk || fk ? ` "${pk}${fk}"` : "";
|
|
3759
|
+
lines.push(` ${typeStr} ${name}${marker}`);
|
|
3760
|
+
}
|
|
3761
|
+
if (entity.fields.size > keyFields.length) {
|
|
3762
|
+
lines.push(` string _more_fields`);
|
|
3763
|
+
}
|
|
3764
|
+
lines.push(" }");
|
|
3765
|
+
}
|
|
3766
|
+
for (const edge of result.graph.edges) {
|
|
3767
|
+
const from = edge.from.toUpperCase();
|
|
3768
|
+
const to = edge.to.toUpperCase();
|
|
3769
|
+
const cardinalitySymbol = getCardinalitySymbol(edge.cardinality);
|
|
3770
|
+
const label = edge.relationship.name;
|
|
3771
|
+
lines.push(` ${from} ${cardinalitySymbol} ${to} : "${label}"`);
|
|
3772
|
+
}
|
|
3773
|
+
return lines;
|
|
3774
|
+
}
|
|
3775
|
+
function getCardinalitySymbol(cardinality) {
|
|
3776
|
+
switch (cardinality) {
|
|
3777
|
+
case "1:N":
|
|
3778
|
+
return "||--o{";
|
|
3779
|
+
case "N:1":
|
|
3780
|
+
return "}o--||";
|
|
3781
|
+
case "1:1":
|
|
3782
|
+
return "||--||";
|
|
3783
|
+
case "N:M":
|
|
3784
|
+
return "}o--o{";
|
|
3785
|
+
default:
|
|
3786
|
+
return "||--o{";
|
|
3787
|
+
}
|
|
3788
|
+
}
|
|
3789
|
+
function formatMermaidGraph(result) {
|
|
3790
|
+
const lines = [];
|
|
3791
|
+
lines.push("```mermaid");
|
|
3792
|
+
lines.push("graph LR");
|
|
3793
|
+
lines.push(" classDef entity fill:#e1f5fe,stroke:#01579b");
|
|
3794
|
+
for (const entity of result.entities) {
|
|
3795
|
+
lines.push(` ${entity.name}["${entity.name}\\n(${entity.fields.size} fields)"]`);
|
|
3796
|
+
}
|
|
3797
|
+
for (const edge of result.graph.edges) {
|
|
3798
|
+
const style = edge.bidirectional ? "<-->" : "-->";
|
|
3799
|
+
lines.push(` ${edge.from} ${style}|${edge.relationship.type}| ${edge.to}`);
|
|
3800
|
+
}
|
|
3801
|
+
const entityList = result.entities.map((e) => e.name).join(",");
|
|
3802
|
+
if (entityList) {
|
|
3803
|
+
lines.push(` class ${entityList} entity`);
|
|
3804
|
+
}
|
|
3805
|
+
lines.push("```");
|
|
3806
|
+
return lines.join("\n");
|
|
3807
|
+
}
|
|
3808
|
+
|
|
3809
|
+
// src/patterns/library/activity.pattern.ts
|
|
3810
|
+
var ActivityPattern = definePattern({
|
|
3811
|
+
name: "Activity",
|
|
3812
|
+
extends: ["Base"],
|
|
3813
|
+
repositoryClass: "ActivityEntityRepository",
|
|
3814
|
+
serviceClass: "ActivityEntityService",
|
|
3815
|
+
repositoryImport: "@shared/base-classes/activity-entity-repository",
|
|
3816
|
+
serviceImport: "@shared/base-classes/activity-entity-service",
|
|
3817
|
+
repositoryInheritedMethods: [
|
|
3818
|
+
"findById, findByIds, list, count, exists, create, update, delete, upsertMany",
|
|
3819
|
+
"findByDateRange, findByUserId, findByOpportunityId, findRecentByOpportunityId"
|
|
3820
|
+
],
|
|
3821
|
+
serviceInheritedMethods: [
|
|
3822
|
+
"findById, findByIds, list, count, exists, create, update, delete",
|
|
3823
|
+
"findByDateRange, findByUserId, findByOpportunityId, findRecentByOpportunityId"
|
|
3824
|
+
],
|
|
3825
|
+
description: "Time-bounded interaction entities \u2014 date-range + opportunity scoped lookups"
|
|
3826
|
+
});
|
|
3827
|
+
|
|
3828
|
+
// src/patterns/library/base.pattern.ts
|
|
3829
|
+
var BasePattern = definePattern({
|
|
3830
|
+
name: "Base",
|
|
3831
|
+
repositoryClass: "BaseRepository",
|
|
3832
|
+
serviceClass: "BaseService",
|
|
3833
|
+
repositoryImport: "@shared/base-classes/base-repository",
|
|
3834
|
+
serviceImport: "@shared/base-classes/base-service",
|
|
3835
|
+
repositoryInheritedMethods: [
|
|
3836
|
+
"findById, findByIds, list, count, exists, create, update, delete, upsertMany"
|
|
3837
|
+
],
|
|
3838
|
+
serviceInheritedMethods: [
|
|
3839
|
+
"findById, findByIds, list, count, exists, create, update, delete"
|
|
3840
|
+
],
|
|
3841
|
+
description: "Identity pattern \u2014 base CRUD, no extra columns or methods"
|
|
3842
|
+
});
|
|
3843
|
+
|
|
3844
|
+
// src/patterns/library/junction.pattern.ts
|
|
3845
|
+
import { z as z6 } from "zod";
|
|
3846
|
+
var JunctionPatternConfigSchema = z6.object({}).strict();
|
|
3847
|
+
var JunctionPattern = definePattern({
|
|
3848
|
+
name: "Junction",
|
|
3849
|
+
description: "Explicit many-to-many junction with role + temporal + sourcing metadata",
|
|
3850
|
+
columns: [...BaseJunctionFields],
|
|
3851
|
+
configSchema: JunctionPatternConfigSchema
|
|
3852
|
+
});
|
|
3853
|
+
|
|
3854
|
+
// src/patterns/library/knowledge.pattern.ts
|
|
3855
|
+
var KnowledgePattern = definePattern({
|
|
3856
|
+
name: "Knowledge",
|
|
3857
|
+
extends: ["Base"],
|
|
3858
|
+
repositoryClass: "KnowledgeEntityRepository",
|
|
3859
|
+
serviceClass: "KnowledgeEntityService",
|
|
3860
|
+
repositoryImport: "@shared/base-classes/knowledge-entity-repository",
|
|
3861
|
+
serviceImport: "@shared/base-classes/knowledge-entity-service",
|
|
3862
|
+
repositoryInheritedMethods: [
|
|
3863
|
+
"findById, findByIds, list, count, exists, create, update, delete, upsertMany",
|
|
3864
|
+
"semanticSearch, findPendingByOpportunityId, updateStatus, updateStatusBatch"
|
|
3865
|
+
],
|
|
3866
|
+
serviceInheritedMethods: [
|
|
3867
|
+
"findById, findByIds, list, count, exists, create, update, delete",
|
|
3868
|
+
"semanticSearch, findPendingByOpportunityId, updateStatus, updateStatusBatch"
|
|
3869
|
+
],
|
|
3870
|
+
description: "Knowledge entities \u2014 semantic search + workflow status"
|
|
3871
|
+
});
|
|
3872
|
+
|
|
3873
|
+
// src/patterns/library/metadata.pattern.ts
|
|
3874
|
+
var MetadataPattern = definePattern({
|
|
3875
|
+
name: "Metadata",
|
|
3876
|
+
extends: ["Base"],
|
|
3877
|
+
repositoryClass: "MetadataEntityRepository",
|
|
3878
|
+
serviceClass: "MetadataEntityService",
|
|
3879
|
+
repositoryImport: "@shared/base-classes/metadata-entity-repository",
|
|
3880
|
+
serviceImport: "@shared/base-classes/metadata-entity-service",
|
|
3881
|
+
repositoryInheritedMethods: [
|
|
3882
|
+
"findById, findByIds, list, count, exists, create, update, delete, upsertMany",
|
|
3883
|
+
"findByEntityIdAndType, listByEntityId, listHistoryByEntityId"
|
|
3884
|
+
],
|
|
3885
|
+
serviceInheritedMethods: [
|
|
3886
|
+
"findById, findByIds, list, count, exists, create, update, delete",
|
|
3887
|
+
"findByEntityIdAndType, listByEntityId, listHistoryByEntityId"
|
|
3888
|
+
],
|
|
3889
|
+
description: "History-tracked metadata rows \u2014 entity-id + type scoped lookups"
|
|
3890
|
+
});
|
|
3891
|
+
|
|
3892
|
+
// src/patterns/library/integrated.pattern.ts
|
|
3893
|
+
var IntegratedPattern = definePattern({
|
|
3894
|
+
name: "Integrated",
|
|
3895
|
+
extends: ["Base"],
|
|
3896
|
+
repositoryClass: "IntegratedEntityRepository",
|
|
3897
|
+
serviceClass: "IntegratedEntityService",
|
|
3898
|
+
repositoryImport: "@shared/base-classes/integrated-entity-repository",
|
|
3899
|
+
serviceImport: "@shared/base-classes/integrated-entity-service",
|
|
3900
|
+
repositoryInheritedMethods: [
|
|
3901
|
+
"findById, findByIds, list, count, exists, create, update, delete, upsertMany",
|
|
3902
|
+
"findByExternalId, findManyByExternalIds, findAllByUserId, findVisibleByUserId",
|
|
3903
|
+
"integrationUpsertOne, findByExternalIdProjected, softDeleteByExternalId, integrationUpsert"
|
|
3904
|
+
],
|
|
3905
|
+
serviceInheritedMethods: [
|
|
3906
|
+
"findById, findByIds, list, count, exists, create, update, delete",
|
|
3907
|
+
"findByExternalId, findAllByUserId, findVisibleByUserId"
|
|
3908
|
+
],
|
|
3909
|
+
impliedBehaviors: ["external_id_tracking"],
|
|
3910
|
+
description: "External CRM/system integration columns and integrationUpsert methods"
|
|
3911
|
+
});
|
|
3912
|
+
|
|
3913
|
+
// src/patterns/library/index.ts
|
|
3914
|
+
registerLibraryPattern(BasePattern);
|
|
3915
|
+
registerLibraryPattern(IntegratedPattern);
|
|
3916
|
+
registerLibraryPattern(ActivityPattern);
|
|
3917
|
+
registerLibraryPattern(KnowledgePattern);
|
|
3918
|
+
registerLibraryPattern(MetadataPattern);
|
|
3919
|
+
registerLibraryPattern(JunctionPattern);
|
|
3920
|
+
|
|
3921
|
+
// src/index.ts
|
|
3922
|
+
async function analyzeDomain(entitiesDir, relationshipsOrOptions) {
|
|
3923
|
+
const opts = typeof relationshipsOrOptions === "string" ? { relationshipsDir: relationshipsOrOptions } : relationshipsOrOptions ?? {};
|
|
3924
|
+
const relationshipsDir = opts.relationshipsDir;
|
|
3925
|
+
const { entities, issues: loadIssues } = loadEntities(entitiesDir);
|
|
3926
|
+
const { relationships: relationshipDefinitions, issues: relLoadIssues } = relationshipsDir ? loadRelationships(relationshipsDir) : { relationships: [], issues: [] };
|
|
3927
|
+
const resolveIssues = resolveReferences(entities);
|
|
3928
|
+
const relResolveIssues = resolveRelationshipReferences(
|
|
3929
|
+
relationshipDefinitions,
|
|
3930
|
+
entities
|
|
3931
|
+
);
|
|
3932
|
+
const graph = buildDomainGraph(entities, relationshipDefinitions);
|
|
3933
|
+
const consistencyIssues = checkConsistency(graph);
|
|
3934
|
+
const patternIssues = entities.flatMap((e) => validatePatternComposition(e));
|
|
3935
|
+
const patternProjectIssues = validatePatternProject({
|
|
3936
|
+
entities,
|
|
3937
|
+
architecture: opts.architecture
|
|
3938
|
+
});
|
|
3939
|
+
const orchestrationProjectIssues = validateOrchestrationProject({
|
|
3940
|
+
orchestrationPatterns: getAllOrchestrationPatterns(),
|
|
3941
|
+
domainPatternNames: getAllPatternNames()
|
|
3942
|
+
});
|
|
3943
|
+
const statistics = computeStatistics(graph);
|
|
3944
|
+
const allIssues = [
|
|
3945
|
+
...loadIssues,
|
|
3946
|
+
...relLoadIssues,
|
|
3947
|
+
...resolveIssues,
|
|
3948
|
+
...relResolveIssues,
|
|
3949
|
+
...consistencyIssues,
|
|
3950
|
+
...patternIssues,
|
|
3951
|
+
...patternProjectIssues,
|
|
3952
|
+
...orchestrationProjectIssues
|
|
3953
|
+
];
|
|
3954
|
+
const hasErrors = allIssues.some((i) => i.severity === "error");
|
|
3955
|
+
return {
|
|
3956
|
+
isValid: !hasErrors,
|
|
3957
|
+
entities,
|
|
3958
|
+
relationshipDefinitions,
|
|
3959
|
+
graph,
|
|
3960
|
+
issues: allIssues,
|
|
3961
|
+
statistics
|
|
3962
|
+
};
|
|
3963
|
+
}
|
|
3964
|
+
function validateEntities(entitiesDir) {
|
|
3965
|
+
const { entities, issues } = loadEntities(entitiesDir);
|
|
3966
|
+
const errors = issues.filter((i) => i.severity === "error").map((i) => i.message);
|
|
3967
|
+
return {
|
|
3968
|
+
valid: errors.length === 0,
|
|
3969
|
+
errors
|
|
3970
|
+
};
|
|
3971
|
+
}
|
|
3972
|
+
|
|
3973
|
+
export {
|
|
3974
|
+
findYamlFiles,
|
|
3975
|
+
EVENT_FIELD_TYPES,
|
|
3976
|
+
DIRECTION_TO_POOL,
|
|
3977
|
+
BaseJunctionFields,
|
|
3978
|
+
BASE_JUNCTION_FIELD_NAMES,
|
|
3979
|
+
JunctionDefinitionSchema,
|
|
3980
|
+
validateJunctionDefinition,
|
|
3981
|
+
safeValidateJunctionDefinition,
|
|
3982
|
+
parseImportRef,
|
|
3983
|
+
loadEntityFromYaml,
|
|
3984
|
+
loadEntitiesFromYaml,
|
|
3985
|
+
loadRelationshipFromYaml,
|
|
3986
|
+
loadEventFromYaml,
|
|
3987
|
+
loadJunctionFromYaml,
|
|
3988
|
+
loadProvidersFromYaml,
|
|
3989
|
+
detectYamlType,
|
|
3990
|
+
loadEntities,
|
|
3991
|
+
loadRelationships,
|
|
3992
|
+
collectEntitySurfaces,
|
|
3993
|
+
validateProviders,
|
|
3994
|
+
buildDomainGraph,
|
|
3995
|
+
getRelatedEntities,
|
|
3996
|
+
findOrphanEntities,
|
|
3997
|
+
findCircularDependencies,
|
|
3998
|
+
checkConsistency,
|
|
3999
|
+
computeStatistics,
|
|
4000
|
+
suggestTransitiveRelationships,
|
|
4001
|
+
getManifestDir,
|
|
4002
|
+
readManifest,
|
|
4003
|
+
writeManifest,
|
|
4004
|
+
isManifestStale,
|
|
4005
|
+
buildManifest,
|
|
4006
|
+
updateSuggestionStatus,
|
|
4007
|
+
updateAllSuggestionStatus,
|
|
4008
|
+
getPendingSuggestions,
|
|
4009
|
+
serializeDomainGraph,
|
|
4010
|
+
definePattern,
|
|
4011
|
+
isPatternDefinition,
|
|
4012
|
+
isOrchestrationPattern,
|
|
4013
|
+
isDomainPattern,
|
|
4014
|
+
defineOrchestrationPattern,
|
|
4015
|
+
registerLibraryPattern,
|
|
4016
|
+
getPattern,
|
|
4017
|
+
getAllPatternNames,
|
|
4018
|
+
getLibraryPatternNames,
|
|
4019
|
+
getAppPatternNames,
|
|
4020
|
+
getOrchestrationPattern,
|
|
4021
|
+
getOrchestrationPatternNames,
|
|
4022
|
+
getAllOrchestrationPatterns,
|
|
4023
|
+
loadAppPatterns,
|
|
4024
|
+
_resetRegistryForTests,
|
|
4025
|
+
validatePatternComposition,
|
|
4026
|
+
validatePatternProject,
|
|
4027
|
+
validateOrchestrationProject,
|
|
4028
|
+
formatConsole,
|
|
4029
|
+
formatJson,
|
|
4030
|
+
formatStatsJson,
|
|
4031
|
+
formatMarkdown,
|
|
4032
|
+
formatMermaidGraph,
|
|
4033
|
+
ActivityPattern,
|
|
4034
|
+
BasePattern,
|
|
4035
|
+
JunctionPattern,
|
|
4036
|
+
KnowledgePattern,
|
|
4037
|
+
MetadataPattern,
|
|
4038
|
+
IntegratedPattern,
|
|
4039
|
+
analyzeDomain,
|
|
4040
|
+
validateEntities
|
|
4041
|
+
};
|
|
4042
|
+
//# sourceMappingURL=chunk-32DOFN3T.js.map
|