@venizia/ignis 0.0.10-1 → 0.0.10-3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +469 -784
- package/dist/base/applications/base.d.ts +1 -2
- package/dist/base/applications/base.d.ts.map +1 -1
- package/dist/base/applications/base.js +5 -10
- package/dist/base/applications/base.js.map +1 -1
- package/dist/base/controllers/common/types.d.ts +47 -1
- package/dist/base/controllers/common/types.d.ts.map +1 -1
- package/dist/base/controllers/common/types.js.map +1 -1
- package/dist/base/controllers/factory/controller.d.ts +11 -44
- package/dist/base/controllers/factory/controller.d.ts.map +1 -1
- package/dist/base/controllers/factory/controller.js +26 -17
- package/dist/base/controllers/factory/controller.js.map +1 -1
- package/dist/base/controllers/factory/definition.d.ts +8 -7
- package/dist/base/controllers/factory/definition.d.ts.map +1 -1
- package/dist/base/controllers/factory/definition.js +24 -11
- package/dist/base/controllers/factory/definition.js.map +1 -1
- package/dist/base/controllers/grpc/abstract.d.ts.map +1 -1
- package/dist/base/controllers/grpc/abstract.js +13 -1
- package/dist/base/controllers/grpc/abstract.js.map +1 -1
- package/dist/base/controllers/grpc/adapter.d.ts.map +1 -1
- package/dist/base/controllers/grpc/adapter.js +7 -1
- package/dist/base/controllers/grpc/adapter.js.map +1 -1
- package/dist/base/controllers/rest/abstract.d.ts.map +1 -1
- package/dist/base/controllers/rest/abstract.js +6 -0
- package/dist/base/controllers/rest/abstract.js.map +1 -1
- package/dist/base/controllers/rest/base.d.ts +3 -1
- package/dist/base/controllers/rest/base.d.ts.map +1 -1
- package/dist/base/controllers/rest/base.js +4 -3
- package/dist/base/controllers/rest/base.js.map +1 -1
- package/dist/base/datasources/abstract-datasource.d.ts +27 -0
- package/dist/base/datasources/abstract-datasource.d.ts.map +1 -0
- package/dist/base/datasources/abstract-datasource.js +73 -0
- package/dist/base/datasources/abstract-datasource.js.map +1 -0
- package/dist/base/datasources/common/types.d.ts +22 -30
- package/dist/base/datasources/common/types.d.ts.map +1 -1
- package/dist/base/datasources/common/types.js +3 -17
- package/dist/base/datasources/common/types.js.map +1 -1
- package/dist/base/datasources/index.d.ts +1 -1
- package/dist/base/datasources/index.d.ts.map +1 -1
- package/dist/base/datasources/index.js +1 -1
- package/dist/base/datasources/index.js.map +1 -1
- package/dist/base/metadata/persistents.d.ts +2 -3
- package/dist/base/metadata/persistents.d.ts.map +1 -1
- package/dist/base/metadata/persistents.js +4 -0
- package/dist/base/metadata/persistents.js.map +1 -1
- package/dist/base/metadata/routes/rest.d.ts.map +1 -1
- package/dist/base/metadata/routes/rest.js.map +1 -1
- package/dist/base/metadata/routes/rpc.d.ts.map +1 -1
- package/dist/base/metadata/routes/rpc.js.map +1 -1
- package/dist/base/mixins/repository.mixin.d.ts +2 -3
- package/dist/base/mixins/repository.mixin.d.ts.map +1 -1
- package/dist/base/mixins/repository.mixin.js +1 -1
- package/dist/base/mixins/repository.mixin.js.map +1 -1
- package/dist/base/mixins/types.d.ts +1 -2
- package/dist/base/mixins/types.d.ts.map +1 -1
- package/dist/base/models/base.d.ts +10 -19
- package/dist/base/models/base.d.ts.map +1 -1
- package/dist/base/models/base.js +9 -33
- package/dist/base/models/base.js.map +1 -1
- package/dist/base/models/common/constants.d.ts +2 -1
- package/dist/base/models/common/constants.d.ts.map +1 -1
- package/dist/base/models/common/constants.js +4 -3
- package/dist/base/models/common/constants.js.map +1 -1
- package/dist/base/models/common/index.d.ts +0 -1
- package/dist/base/models/common/index.d.ts.map +1 -1
- package/dist/base/models/common/index.js +0 -1
- package/dist/base/models/common/index.js.map +1 -1
- package/dist/base/models/common/types.d.ts +3 -30
- package/dist/base/models/common/types.d.ts.map +1 -1
- package/dist/base/models/common/types.js +1 -5
- package/dist/base/models/common/types.js.map +1 -1
- package/dist/base/models/index.d.ts +0 -1
- package/dist/base/models/index.d.ts.map +1 -1
- package/dist/base/models/index.js +0 -1
- package/dist/base/models/index.js.map +1 -1
- package/dist/base/repositories/common/index.d.ts +1 -1
- package/dist/base/repositories/common/index.d.ts.map +1 -1
- package/dist/base/repositories/common/index.js +1 -1
- package/dist/base/repositories/common/index.js.map +1 -1
- package/dist/base/repositories/common/operators.d.ts +49 -0
- package/dist/base/repositories/common/operators.d.ts.map +1 -0
- package/dist/base/repositories/common/operators.js +118 -0
- package/dist/base/repositories/common/operators.js.map +1 -0
- package/dist/base/repositories/common/types.d.ts +57 -73
- package/dist/base/repositories/common/types.d.ts.map +1 -1
- package/dist/base/repositories/common/types.js +2 -5
- package/dist/base/repositories/common/types.js.map +1 -1
- package/dist/base/repositories/core/abstract-repository.d.ts +218 -0
- package/dist/base/repositories/core/abstract-repository.d.ts.map +1 -0
- package/dist/base/repositories/core/abstract-repository.js +107 -0
- package/dist/base/repositories/core/abstract-repository.js.map +1 -0
- package/dist/base/repositories/core/index.d.ts +1 -5
- package/dist/base/repositories/core/index.d.ts.map +1 -1
- package/dist/base/repositories/core/index.js +1 -5
- package/dist/base/repositories/core/index.js.map +1 -1
- package/dist/base/repositories/index.d.ts +0 -1
- package/dist/base/repositories/index.d.ts.map +1 -1
- package/dist/base/repositories/index.js +0 -1
- package/dist/base/repositories/index.js.map +1 -1
- package/dist/components/auth/authenticate/component.d.ts.map +1 -1
- package/dist/components/auth/authenticate/component.js +3 -2
- package/dist/components/auth/authenticate/component.js.map +1 -1
- package/dist/components/auth/authenticate/controllers/factory.d.ts.map +1 -1
- package/dist/components/auth/authenticate/controllers/factory.js +13 -9
- package/dist/components/auth/authenticate/controllers/factory.js.map +1 -1
- package/dist/components/auth/authenticate/providers/authentication.provider.js.map +1 -1
- package/dist/components/auth/authorize/adapters/base-filtered.d.ts +6 -20
- package/dist/components/auth/authorize/adapters/base-filtered.d.ts.map +1 -1
- package/dist/components/auth/authorize/adapters/base-filtered.js +2 -8
- package/dist/components/auth/authorize/adapters/base-filtered.js.map +1 -1
- package/dist/components/auth/authorize/adapters/scoped-casbin.adapter.d.ts +19 -55
- package/dist/components/auth/authorize/adapters/scoped-casbin.adapter.d.ts.map +1 -1
- package/dist/components/auth/authorize/adapters/scoped-casbin.adapter.js +19 -55
- package/dist/components/auth/authorize/adapters/scoped-casbin.adapter.js.map +1 -1
- package/dist/components/auth/authorize/adapters/types.d.ts +16 -0
- package/dist/components/auth/authorize/adapters/types.d.ts.map +1 -1
- package/dist/components/auth/authorize/common/resolve-request-domain.js.map +1 -1
- package/dist/components/auth/models/entities/user.model.d.ts.map +1 -1
- package/dist/components/auth/models/entities/user.model.js +3 -3
- package/dist/components/auth/models/entities/user.model.js.map +1 -1
- package/dist/components/mail/providers/mail-transporter.provider.d.ts +1 -1
- package/dist/components/mail/providers/mail-transporter.provider.d.ts.map +1 -1
- package/dist/components/mail/providers/mail-transporter.provider.js +4 -3
- package/dist/components/mail/providers/mail-transporter.provider.js.map +1 -1
- package/dist/components/static-asset/common/types.d.ts +4 -4
- package/dist/components/static-asset/common/types.d.ts.map +1 -1
- package/dist/components/static-asset/models/base.model.d.ts +2 -2
- package/dist/components/static-asset/models/base.model.d.ts.map +1 -1
- package/dist/components/static-asset/models/base.model.js +2 -2
- package/dist/components/static-asset/models/base.model.js.map +1 -1
- package/dist/components/static-asset/repositories/base.repository.d.ts +2 -10
- package/dist/components/static-asset/repositories/base.repository.d.ts.map +1 -1
- package/dist/components/static-asset/repositories/base.repository.js +2 -10
- package/dist/components/static-asset/repositories/base.repository.js.map +1 -1
- package/dist/components/websocket/handlers/bun.handler.d.ts +1 -1
- package/dist/components/websocket/handlers/bun.handler.d.ts.map +1 -1
- package/dist/components/websocket/handlers/bun.handler.js.map +1 -1
- package/dist/connectors/index.d.ts +3 -0
- package/dist/connectors/index.d.ts.map +1 -0
- package/dist/{base/repositories/mixins → connectors}/index.js +2 -2
- package/dist/connectors/index.js.map +1 -0
- package/dist/connectors/memory/datasources/datasource.d.ts +35 -0
- package/dist/connectors/memory/datasources/datasource.d.ts.map +1 -0
- package/dist/connectors/memory/datasources/datasource.js +51 -0
- package/dist/connectors/memory/datasources/datasource.js.map +1 -0
- package/dist/connectors/memory/datasources/index.d.ts +2 -0
- package/dist/connectors/memory/datasources/index.d.ts.map +1 -0
- package/dist/connectors/memory/datasources/index.js +18 -0
- package/dist/connectors/memory/datasources/index.js.map +1 -0
- package/dist/connectors/memory/index.d.ts +3 -0
- package/dist/connectors/memory/index.d.ts.map +1 -0
- package/dist/connectors/memory/index.js +19 -0
- package/dist/connectors/memory/index.js.map +1 -0
- package/dist/connectors/memory/internal/index.d.ts +2 -0
- package/dist/connectors/memory/internal/index.d.ts.map +1 -0
- package/dist/connectors/memory/internal/index.js +18 -0
- package/dist/connectors/memory/internal/index.js.map +1 -0
- package/dist/connectors/memory/internal/where-matcher.d.ts +13 -0
- package/dist/connectors/memory/internal/where-matcher.d.ts.map +1 -0
- package/dist/connectors/memory/internal/where-matcher.js +326 -0
- package/dist/connectors/memory/internal/where-matcher.js.map +1 -0
- package/dist/connectors/memory/repositories/index.d.ts +2 -0
- package/dist/connectors/memory/repositories/index.d.ts.map +1 -0
- package/dist/connectors/memory/repositories/index.js +18 -0
- package/dist/connectors/memory/repositories/index.js.map +1 -0
- package/dist/connectors/memory/repositories/repository.d.ts +175 -0
- package/dist/connectors/memory/repositories/repository.d.ts.map +1 -0
- package/dist/connectors/memory/repositories/repository.js +345 -0
- package/dist/connectors/memory/repositories/repository.js.map +1 -0
- package/dist/connectors/postgres/datasources/abstract-datasource.d.ts +13 -0
- package/dist/connectors/postgres/datasources/abstract-datasource.d.ts.map +1 -0
- package/dist/connectors/postgres/datasources/abstract-datasource.js +12 -0
- package/dist/connectors/postgres/datasources/abstract-datasource.js.map +1 -0
- package/dist/connectors/postgres/datasources/base-datasource.d.ts +20 -0
- package/dist/connectors/postgres/datasources/base-datasource.d.ts.map +1 -0
- package/dist/{base/datasources/base.js → connectors/postgres/datasources/base-datasource.js} +11 -22
- package/dist/connectors/postgres/datasources/base-datasource.js.map +1 -0
- package/dist/connectors/postgres/datasources/common/guards.d.ts +5 -0
- package/dist/connectors/postgres/datasources/common/guards.d.ts.map +1 -0
- package/dist/connectors/postgres/datasources/common/guards.js +9 -0
- package/dist/connectors/postgres/datasources/common/guards.js.map +1 -0
- package/dist/connectors/postgres/datasources/common/index.d.ts +3 -0
- package/dist/connectors/postgres/datasources/common/index.d.ts.map +1 -0
- package/dist/connectors/postgres/datasources/common/index.js +19 -0
- package/dist/connectors/postgres/datasources/common/index.js.map +1 -0
- package/dist/connectors/postgres/datasources/common/types.d.ts +37 -0
- package/dist/connectors/postgres/datasources/common/types.d.ts.map +1 -0
- package/dist/connectors/postgres/datasources/common/types.js +19 -0
- package/dist/connectors/postgres/datasources/common/types.js.map +1 -0
- package/dist/connectors/postgres/datasources/index.d.ts +5 -0
- package/dist/connectors/postgres/datasources/index.d.ts.map +1 -0
- package/dist/connectors/postgres/datasources/index.js +24 -0
- package/dist/connectors/postgres/datasources/index.js.map +1 -0
- package/dist/connectors/postgres/index.d.ts +4 -0
- package/dist/connectors/postgres/index.d.ts.map +1 -0
- package/dist/connectors/postgres/index.js +20 -0
- package/dist/connectors/postgres/index.js.map +1 -0
- package/dist/connectors/postgres/models/base.d.ts +29 -0
- package/dist/connectors/postgres/models/base.d.ts.map +1 -0
- package/dist/connectors/postgres/models/base.js +45 -0
- package/dist/connectors/postgres/models/base.js.map +1 -0
- package/dist/connectors/postgres/models/common/columns.d.ts +13 -0
- package/dist/connectors/postgres/models/common/columns.d.ts.map +1 -0
- package/dist/{base → connectors/postgres}/models/common/columns.js +2 -12
- package/dist/connectors/postgres/models/common/columns.js.map +1 -0
- package/dist/connectors/postgres/models/common/index.d.ts +3 -0
- package/dist/connectors/postgres/models/common/index.d.ts.map +1 -0
- package/dist/connectors/postgres/models/common/index.js +19 -0
- package/dist/connectors/postgres/models/common/index.js.map +1 -0
- package/dist/connectors/postgres/models/common/types.d.ts +89 -0
- package/dist/connectors/postgres/models/common/types.d.ts.map +1 -0
- package/dist/connectors/postgres/models/common/types.js +8 -0
- package/dist/connectors/postgres/models/common/types.js.map +1 -0
- package/dist/{base → connectors/postgres}/models/enrichers/data-type.enricher.d.ts +1 -11
- package/dist/connectors/postgres/models/enrichers/data-type.enricher.d.ts.map +1 -0
- package/dist/connectors/postgres/models/enrichers/data-type.enricher.js.map +1 -0
- package/dist/{base → connectors/postgres}/models/enrichers/id.enricher.d.ts +2 -17
- package/dist/connectors/postgres/models/enrichers/id.enricher.d.ts.map +1 -0
- package/dist/connectors/postgres/models/enrichers/id.enricher.js.map +1 -0
- package/dist/connectors/postgres/models/enrichers/index.d.ts.map +1 -0
- package/dist/connectors/postgres/models/enrichers/index.js.map +1 -0
- package/dist/{base → connectors/postgres}/models/enrichers/principal.enricher.d.ts +1 -7
- package/dist/connectors/postgres/models/enrichers/principal.enricher.d.ts.map +1 -0
- package/dist/connectors/postgres/models/enrichers/principal.enricher.js.map +1 -0
- package/dist/{base → connectors/postgres}/models/enrichers/tz.enricher.d.ts +1 -21
- package/dist/connectors/postgres/models/enrichers/tz.enricher.d.ts.map +1 -0
- package/dist/{base → connectors/postgres}/models/enrichers/tz.enricher.js +0 -2
- package/dist/connectors/postgres/models/enrichers/tz.enricher.js.map +1 -0
- package/dist/{base → connectors/postgres}/models/enrichers/user-audit.enricher.d.ts +1 -11
- package/dist/connectors/postgres/models/enrichers/user-audit.enricher.d.ts.map +1 -0
- package/dist/{base → connectors/postgres}/models/enrichers/user-audit.enricher.js +1 -1
- package/dist/connectors/postgres/models/enrichers/user-audit.enricher.js.map +1 -0
- package/dist/connectors/postgres/models/index.d.ts +6 -0
- package/dist/connectors/postgres/models/index.d.ts.map +1 -0
- package/dist/connectors/postgres/models/index.js +25 -0
- package/dist/connectors/postgres/models/index.js.map +1 -0
- package/dist/connectors/postgres/repositories/common/index.d.ts +3 -0
- package/dist/connectors/postgres/repositories/common/index.d.ts.map +1 -0
- package/dist/connectors/postgres/repositories/common/index.js +19 -0
- package/dist/connectors/postgres/repositories/common/index.js.map +1 -0
- package/dist/connectors/postgres/repositories/common/shared.d.ts +5 -0
- package/dist/connectors/postgres/repositories/common/shared.d.ts.map +1 -0
- package/dist/connectors/postgres/repositories/common/shared.js.map +1 -0
- package/dist/connectors/postgres/repositories/common/types.d.ts +33 -0
- package/dist/connectors/postgres/repositories/common/types.d.ts.map +1 -0
- package/dist/connectors/postgres/repositories/common/types.js +3 -0
- package/dist/connectors/postgres/repositories/common/types.js.map +1 -0
- package/dist/{base/repositories/core/abstract.d.ts → connectors/postgres/repositories/core/base.d.ts} +45 -96
- package/dist/connectors/postgres/repositories/core/base.d.ts.map +1 -0
- package/dist/{base/repositories/core/abstract.js → connectors/postgres/repositories/core/base.js} +85 -74
- package/dist/connectors/postgres/repositories/core/base.js.map +1 -0
- package/dist/connectors/postgres/repositories/core/default-crud.d.ts +9 -0
- package/dist/connectors/postgres/repositories/core/default-crud.d.ts.map +1 -0
- package/dist/{base → connectors/postgres}/repositories/core/default-crud.js +2 -1
- package/dist/connectors/postgres/repositories/core/default-crud.js.map +1 -0
- package/dist/connectors/postgres/repositories/core/index.d.ts +6 -0
- package/dist/connectors/postgres/repositories/core/index.d.ts.map +1 -0
- package/dist/connectors/postgres/repositories/core/index.js +22 -0
- package/dist/connectors/postgres/repositories/core/index.js.map +1 -0
- package/dist/{base → connectors/postgres}/repositories/core/persistable.d.ts +8 -6
- package/dist/connectors/postgres/repositories/core/persistable.d.ts.map +1 -0
- package/dist/{base → connectors/postgres}/repositories/core/persistable.js +13 -6
- package/dist/connectors/postgres/repositories/core/persistable.js.map +1 -0
- package/dist/{base → connectors/postgres}/repositories/core/readable.d.ts +9 -7
- package/dist/connectors/postgres/repositories/core/readable.d.ts.map +1 -0
- package/dist/{base → connectors/postgres}/repositories/core/readable.js +15 -13
- package/dist/connectors/postgres/repositories/core/readable.js.map +1 -0
- package/dist/{base → connectors/postgres}/repositories/core/soft-deletable.d.ts +8 -31
- package/dist/connectors/postgres/repositories/core/soft-deletable.d.ts.map +1 -0
- package/dist/{base → connectors/postgres}/repositories/core/soft-deletable.js +32 -40
- package/dist/connectors/postgres/repositories/core/soft-deletable.js.map +1 -0
- package/dist/connectors/postgres/repositories/index.d.ts +4 -0
- package/dist/connectors/postgres/repositories/index.d.ts.map +1 -0
- package/dist/connectors/postgres/repositories/index.js +20 -0
- package/dist/connectors/postgres/repositories/index.js.map +1 -0
- package/dist/{base → connectors/postgres}/repositories/operators/filter.d.ts +5 -3
- package/dist/connectors/postgres/repositories/operators/filter.d.ts.map +1 -0
- package/dist/{base → connectors/postgres}/repositories/operators/filter.js +16 -14
- package/dist/connectors/postgres/repositories/operators/filter.js.map +1 -0
- package/dist/connectors/postgres/repositories/operators/index.d.ts +6 -0
- package/dist/connectors/postgres/repositories/operators/index.d.ts.map +1 -0
- package/dist/{base → connectors/postgres}/repositories/operators/index.js +4 -1
- package/dist/connectors/postgres/repositories/operators/index.js.map +1 -0
- package/dist/connectors/postgres/repositories/operators/internal/json-utils.d.ts.map +1 -0
- package/dist/connectors/postgres/repositories/operators/internal/json-utils.js.map +1 -0
- package/dist/connectors/postgres/repositories/operators/query.d.ts +31 -0
- package/dist/connectors/postgres/repositories/operators/query.d.ts.map +1 -0
- package/dist/{base → connectors/postgres}/repositories/operators/query.js +12 -114
- package/dist/connectors/postgres/repositories/operators/query.js.map +1 -0
- package/dist/{base → connectors/postgres}/repositories/operators/relation.d.ts +1 -1
- package/dist/connectors/postgres/repositories/operators/relation.d.ts.map +1 -0
- package/dist/connectors/postgres/repositories/operators/relation.js.map +1 -0
- package/dist/{base → connectors/postgres}/repositories/operators/update.d.ts +2 -9
- package/dist/connectors/postgres/repositories/operators/update.d.ts.map +1 -0
- package/dist/{base → connectors/postgres}/repositories/operators/update.js +1 -1
- package/dist/connectors/postgres/repositories/operators/update.js.map +1 -0
- package/dist/connectors/typesense/compiler.d.ts +7 -0
- package/dist/connectors/typesense/compiler.d.ts.map +1 -0
- package/dist/connectors/typesense/compiler.js +114 -0
- package/dist/connectors/typesense/compiler.js.map +1 -0
- package/dist/connectors/typesense/datasources/abstract-datasource.d.ts +16 -0
- package/dist/connectors/typesense/datasources/abstract-datasource.d.ts.map +1 -0
- package/dist/connectors/typesense/datasources/abstract-datasource.js +9 -0
- package/dist/connectors/typesense/datasources/abstract-datasource.js.map +1 -0
- package/dist/connectors/typesense/datasources/base-datasource.d.ts +21 -0
- package/dist/connectors/typesense/datasources/base-datasource.d.ts.map +1 -0
- package/dist/connectors/typesense/datasources/base-datasource.js +66 -0
- package/dist/connectors/typesense/datasources/base-datasource.js.map +1 -0
- package/dist/connectors/typesense/datasources/datasource.d.ts +27 -0
- package/dist/connectors/typesense/datasources/datasource.d.ts.map +1 -0
- package/dist/connectors/typesense/datasources/datasource.js +54 -0
- package/dist/connectors/typesense/datasources/datasource.js.map +1 -0
- package/dist/connectors/typesense/datasources/index.d.ts +4 -0
- package/dist/connectors/typesense/datasources/index.d.ts.map +1 -0
- package/dist/connectors/typesense/datasources/index.js +20 -0
- package/dist/connectors/typesense/datasources/index.js.map +1 -0
- package/dist/connectors/typesense/driver.d.ts +364 -0
- package/dist/connectors/typesense/driver.d.ts.map +1 -0
- package/dist/connectors/typesense/driver.js +624 -0
- package/dist/connectors/typesense/driver.js.map +1 -0
- package/dist/connectors/typesense/index.d.ts +8 -0
- package/dist/connectors/typesense/index.d.ts.map +1 -0
- package/dist/connectors/typesense/index.js +24 -0
- package/dist/connectors/typesense/index.js.map +1 -0
- package/dist/connectors/typesense/internal/driver-internal.d.ts +11 -0
- package/dist/connectors/typesense/internal/driver-internal.d.ts.map +1 -0
- package/dist/connectors/typesense/internal/driver-internal.js +45 -0
- package/dist/connectors/typesense/internal/driver-internal.js.map +1 -0
- package/dist/connectors/typesense/internal/index.d.ts +3 -0
- package/dist/connectors/typesense/internal/index.d.ts.map +1 -0
- package/dist/connectors/typesense/internal/index.js +19 -0
- package/dist/connectors/typesense/internal/index.js.map +1 -0
- package/dist/connectors/typesense/internal/search-driver-internal.d.ts +18 -0
- package/dist/connectors/typesense/internal/search-driver-internal.d.ts.map +1 -0
- package/dist/connectors/typesense/internal/search-driver-internal.js +37 -0
- package/dist/connectors/typesense/internal/search-driver-internal.js.map +1 -0
- package/dist/connectors/typesense/models/base.d.ts +28 -0
- package/dist/connectors/typesense/models/base.d.ts.map +1 -0
- package/dist/connectors/typesense/models/base.js +63 -0
- package/dist/connectors/typesense/models/base.js.map +1 -0
- package/dist/connectors/typesense/models/define-search-collection.d.ts +41 -0
- package/dist/connectors/typesense/models/define-search-collection.d.ts.map +1 -0
- package/dist/connectors/typesense/models/define-search-collection.js +90 -0
- package/dist/connectors/typesense/models/define-search-collection.js.map +1 -0
- package/dist/connectors/typesense/models/index.d.ts +5 -0
- package/dist/connectors/typesense/models/index.d.ts.map +1 -0
- package/dist/connectors/typesense/models/index.js +21 -0
- package/dist/connectors/typesense/models/index.js.map +1 -0
- package/dist/connectors/typesense/models/types.d.ts +57 -0
- package/dist/connectors/typesense/models/types.d.ts.map +1 -0
- package/dist/connectors/typesense/models/types.js +27 -0
- package/dist/connectors/typesense/models/types.js.map +1 -0
- package/dist/connectors/typesense/models/zod-derivation.d.ts +8 -0
- package/dist/connectors/typesense/models/zod-derivation.d.ts.map +1 -0
- package/dist/connectors/typesense/models/zod-derivation.js +67 -0
- package/dist/connectors/typesense/models/zod-derivation.js.map +1 -0
- package/dist/connectors/typesense/query-dialect.d.ts +25 -0
- package/dist/connectors/typesense/query-dialect.d.ts.map +1 -0
- package/dist/connectors/typesense/query-dialect.js +263 -0
- package/dist/connectors/typesense/query-dialect.js.map +1 -0
- package/dist/connectors/typesense/repositories/common/index.d.ts +2 -0
- package/dist/connectors/typesense/repositories/common/index.d.ts.map +1 -0
- package/dist/connectors/typesense/repositories/common/index.js +18 -0
- package/dist/connectors/typesense/repositories/common/index.js.map +1 -0
- package/dist/connectors/typesense/repositories/common/types.d.ts +24 -0
- package/dist/connectors/typesense/repositories/common/types.d.ts.map +1 -0
- package/dist/connectors/typesense/repositories/common/types.js +3 -0
- package/dist/connectors/typesense/repositories/common/types.js.map +1 -0
- package/dist/connectors/typesense/repositories/core/base-repository.d.ts +43 -0
- package/dist/connectors/typesense/repositories/core/base-repository.d.ts.map +1 -0
- package/dist/connectors/typesense/repositories/core/base-repository.js +87 -0
- package/dist/connectors/typesense/repositories/core/base-repository.js.map +1 -0
- package/dist/connectors/typesense/repositories/core/default-search.d.ts +41 -0
- package/dist/connectors/typesense/repositories/core/default-search.d.ts.map +1 -0
- package/dist/connectors/typesense/repositories/core/default-search.js +66 -0
- package/dist/connectors/typesense/repositories/core/default-search.js.map +1 -0
- package/dist/connectors/typesense/repositories/core/index.d.ts +5 -0
- package/dist/connectors/typesense/repositories/core/index.d.ts.map +1 -0
- package/dist/connectors/typesense/repositories/core/index.js +21 -0
- package/dist/connectors/typesense/repositories/core/index.js.map +1 -0
- package/dist/connectors/typesense/repositories/core/persistable.d.ts +100 -0
- package/dist/connectors/typesense/repositories/core/persistable.d.ts.map +1 -0
- package/dist/connectors/typesense/repositories/core/persistable.js +132 -0
- package/dist/connectors/typesense/repositories/core/persistable.js.map +1 -0
- package/dist/connectors/typesense/repositories/core/readable.d.ts +157 -0
- package/dist/connectors/typesense/repositories/core/readable.d.ts.map +1 -0
- package/dist/connectors/typesense/repositories/core/readable.js +114 -0
- package/dist/connectors/typesense/repositories/core/readable.js.map +1 -0
- package/dist/connectors/typesense/repositories/index.d.ts +3 -0
- package/dist/connectors/typesense/repositories/index.d.ts.map +1 -0
- package/dist/connectors/typesense/repositories/index.js +19 -0
- package/dist/connectors/typesense/repositories/index.js.map +1 -0
- package/dist/connectors/typesense/types.d.ts +68 -0
- package/dist/connectors/typesense/types.d.ts.map +1 -0
- package/dist/connectors/typesense/types.js +36 -0
- package/dist/connectors/typesense/types.js.map +1 -0
- package/dist/helpers/inversion/common/types.d.ts +15 -13
- package/dist/helpers/inversion/common/types.d.ts.map +1 -1
- package/dist/helpers/inversion/mixins/model.mixin.d.ts +6 -27
- package/dist/helpers/inversion/mixins/model.mixin.d.ts.map +1 -1
- package/dist/helpers/inversion/mixins/model.mixin.js +9 -28
- package/dist/helpers/inversion/mixins/model.mixin.js.map +1 -1
- package/dist/helpers/inversion/mixins/repository.mixin.d.ts +30 -54
- package/dist/helpers/inversion/mixins/repository.mixin.d.ts.map +1 -1
- package/dist/helpers/inversion/mixins/repository.mixin.js +31 -53
- package/dist/helpers/inversion/mixins/repository.mixin.js.map +1 -1
- package/dist/helpers/inversion/registry.d.ts +20 -14
- package/dist/helpers/inversion/registry.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/utilities/error.utility.d.ts +11 -0
- package/dist/utilities/error.utility.d.ts.map +1 -0
- package/dist/utilities/error.utility.js +19 -0
- package/dist/utilities/error.utility.js.map +1 -0
- package/dist/utilities/index.d.ts +1 -0
- package/dist/utilities/index.d.ts.map +1 -1
- package/dist/utilities/index.js +1 -0
- package/dist/utilities/index.js.map +1 -1
- package/package.json +24 -7
- package/dist/base/datasources/base.d.ts +0 -30
- package/dist/base/datasources/base.d.ts.map +0 -1
- package/dist/base/datasources/base.js.map +0 -1
- package/dist/base/models/common/columns.d.ts +0 -22
- package/dist/base/models/common/columns.d.ts.map +0 -1
- package/dist/base/models/common/columns.js.map +0 -1
- package/dist/base/models/enrichers/data-type.enricher.d.ts.map +0 -1
- package/dist/base/models/enrichers/data-type.enricher.js.map +0 -1
- package/dist/base/models/enrichers/id.enricher.d.ts.map +0 -1
- package/dist/base/models/enrichers/id.enricher.js.map +0 -1
- package/dist/base/models/enrichers/index.d.ts.map +0 -1
- package/dist/base/models/enrichers/index.js.map +0 -1
- package/dist/base/models/enrichers/principal.enricher.d.ts.map +0 -1
- package/dist/base/models/enrichers/principal.enricher.js.map +0 -1
- package/dist/base/models/enrichers/tz.enricher.d.ts.map +0 -1
- package/dist/base/models/enrichers/tz.enricher.js.map +0 -1
- package/dist/base/models/enrichers/user-audit.enricher.d.ts.map +0 -1
- package/dist/base/models/enrichers/user-audit.enricher.js.map +0 -1
- package/dist/base/repositories/common/shared.d.ts +0 -7
- package/dist/base/repositories/common/shared.d.ts.map +0 -1
- package/dist/base/repositories/common/shared.js.map +0 -1
- package/dist/base/repositories/core/abstract.d.ts.map +0 -1
- package/dist/base/repositories/core/abstract.js.map +0 -1
- package/dist/base/repositories/core/default-crud.d.ts +0 -7
- package/dist/base/repositories/core/default-crud.d.ts.map +0 -1
- package/dist/base/repositories/core/default-crud.js.map +0 -1
- package/dist/base/repositories/core/persistable.d.ts.map +0 -1
- package/dist/base/repositories/core/persistable.js.map +0 -1
- package/dist/base/repositories/core/readable.d.ts.map +0 -1
- package/dist/base/repositories/core/readable.js.map +0 -1
- package/dist/base/repositories/core/soft-deletable.d.ts.map +0 -1
- package/dist/base/repositories/core/soft-deletable.js.map +0 -1
- package/dist/base/repositories/mixins/default-filter.d.ts +0 -22
- package/dist/base/repositories/mixins/default-filter.d.ts.map +0 -1
- package/dist/base/repositories/mixins/default-filter.js +0 -54
- package/dist/base/repositories/mixins/default-filter.js.map +0 -1
- package/dist/base/repositories/mixins/fields-visibility.d.ts +0 -18
- package/dist/base/repositories/mixins/fields-visibility.d.ts.map +0 -1
- package/dist/base/repositories/mixins/fields-visibility.js +0 -65
- package/dist/base/repositories/mixins/fields-visibility.js.map +0 -1
- package/dist/base/repositories/mixins/index.d.ts +0 -3
- package/dist/base/repositories/mixins/index.d.ts.map +0 -1
- package/dist/base/repositories/mixins/index.js.map +0 -1
- package/dist/base/repositories/operators/filter.d.ts.map +0 -1
- package/dist/base/repositories/operators/filter.js.map +0 -1
- package/dist/base/repositories/operators/index.d.ts +0 -6
- package/dist/base/repositories/operators/index.d.ts.map +0 -1
- package/dist/base/repositories/operators/index.js.map +0 -1
- package/dist/base/repositories/operators/json-utils.d.ts.map +0 -1
- package/dist/base/repositories/operators/json-utils.js.map +0 -1
- package/dist/base/repositories/operators/query.d.ts +0 -75
- package/dist/base/repositories/operators/query.d.ts.map +0 -1
- package/dist/base/repositories/operators/query.js.map +0 -1
- package/dist/base/repositories/operators/relation.d.ts.map +0 -1
- package/dist/base/repositories/operators/relation.js.map +0 -1
- package/dist/base/repositories/operators/update.d.ts.map +0 -1
- package/dist/base/repositories/operators/update.js.map +0 -1
- /package/dist/{base → connectors/postgres}/models/enrichers/data-type.enricher.js +0 -0
- /package/dist/{base → connectors/postgres}/models/enrichers/id.enricher.js +0 -0
- /package/dist/{base → connectors/postgres}/models/enrichers/index.d.ts +0 -0
- /package/dist/{base → connectors/postgres}/models/enrichers/index.js +0 -0
- /package/dist/{base → connectors/postgres}/models/enrichers/principal.enricher.js +0 -0
- /package/dist/{base → connectors/postgres}/repositories/common/shared.js +0 -0
- /package/dist/{base/repositories/operators → connectors/postgres/repositories/operators/internal}/json-utils.d.ts +0 -0
- /package/dist/{base/repositories/operators → connectors/postgres/repositories/operators/internal}/json-utils.js +0 -0
- /package/dist/{base → connectors/postgres}/repositories/operators/relation.js +0 -0
package/README.md
CHANGED
|
@@ -34,11 +34,13 @@ IGNIS brings together the structured, enterprise development experience of **Loo
|
|
|
34
34
|
```typescript
|
|
35
35
|
import {
|
|
36
36
|
BaseApplication, // Your app extends this
|
|
37
|
-
BaseRestController,
|
|
38
|
-
DefaultCRUDRepository, // Repositories extend this
|
|
39
|
-
BaseEntity, // Models extend this
|
|
40
|
-
BaseDataSource, // DataSources extend this
|
|
37
|
+
BaseRestController, // Controllers extend this
|
|
41
38
|
} from '@venizia/ignis';
|
|
39
|
+
import {
|
|
40
|
+
DefaultCRUDRepository, // Repositories extend this
|
|
41
|
+
BasePostgresEntity, // Models extend this
|
|
42
|
+
BasePostgresDataSource, // DataSources extend this
|
|
43
|
+
} from '@venizia/ignis/postgres';
|
|
42
44
|
```
|
|
43
45
|
|
|
44
46
|
---
|
|
@@ -53,6 +55,8 @@ import {
|
|
|
53
55
|
- [Repositories](#repositories)
|
|
54
56
|
- [Models](#models)
|
|
55
57
|
- [DataSources](#datasources)
|
|
58
|
+
- [Search](#search)
|
|
59
|
+
- [Memory Connector](#memory-connector)
|
|
56
60
|
- [Services](#services)
|
|
57
61
|
- [Components](#components)
|
|
58
62
|
- [Request Context](#request-context)
|
|
@@ -63,6 +67,7 @@ import {
|
|
|
63
67
|
- [Real-World Patterns](#real-world-patterns)
|
|
64
68
|
- [Testing](#testing)
|
|
65
69
|
- [Performance Tips](#performance-tips)
|
|
70
|
+
- [Documentation](#documentation)
|
|
66
71
|
- [License](#license)
|
|
67
72
|
|
|
68
73
|
---
|
|
@@ -104,6 +109,12 @@ bun add casbin
|
|
|
104
109
|
|
|
105
110
|
# Email
|
|
106
111
|
bun add nodemailer mailgun.js
|
|
112
|
+
|
|
113
|
+
# Search (Typesense)
|
|
114
|
+
bun add typesense
|
|
115
|
+
|
|
116
|
+
# gRPC controller transport
|
|
117
|
+
bun add @connectrpc/connect
|
|
107
118
|
```
|
|
108
119
|
|
|
109
120
|
---
|
|
@@ -114,7 +125,8 @@ bun add nodemailer mailgun.js
|
|
|
114
125
|
|
|
115
126
|
```typescript
|
|
116
127
|
// models/user.model.ts
|
|
117
|
-
import {
|
|
128
|
+
import { model } from '@venizia/ignis';
|
|
129
|
+
import { BasePostgresEntity, generateIdColumnDefs, generateTzColumnDefs } from '@venizia/ignis/postgres';
|
|
118
130
|
import { pgTable, text } from 'drizzle-orm/pg-core';
|
|
119
131
|
|
|
120
132
|
@model({
|
|
@@ -123,7 +135,7 @@ import { pgTable, text } from 'drizzle-orm/pg-core';
|
|
|
123
135
|
hiddenProperties: ['password'],
|
|
124
136
|
},
|
|
125
137
|
})
|
|
126
|
-
export class User extends
|
|
138
|
+
export class User extends BasePostgresEntity<typeof User.schema> {
|
|
127
139
|
static override schema = pgTable('User', {
|
|
128
140
|
...generateIdColumnDefs({ id: { dataType: 'string' } }),
|
|
129
141
|
...generateTzColumnDefs(),
|
|
@@ -140,7 +152,8 @@ export class User extends BaseEntity<typeof User.schema> {
|
|
|
140
152
|
|
|
141
153
|
```typescript
|
|
142
154
|
// datasources/postgres.datasource.ts
|
|
143
|
-
import {
|
|
155
|
+
import { datasource, ValueOrPromise } from '@venizia/ignis';
|
|
156
|
+
import { BasePostgresDataSource } from '@venizia/ignis/postgres';
|
|
144
157
|
import { drizzle } from 'drizzle-orm/node-postgres';
|
|
145
158
|
import { Pool } from 'pg';
|
|
146
159
|
|
|
@@ -150,10 +163,14 @@ interface IDSConfigs {
|
|
|
150
163
|
database: string;
|
|
151
164
|
user: string;
|
|
152
165
|
password: string;
|
|
166
|
+
// Optional pg Pool tuning (see "Complete DataSource Configuration" below)
|
|
167
|
+
max?: number;
|
|
168
|
+
idleTimeoutMillis?: number;
|
|
169
|
+
connectionTimeoutMillis?: number;
|
|
153
170
|
}
|
|
154
171
|
|
|
155
172
|
@datasource({ driver: 'node-postgres' })
|
|
156
|
-
export class PostgresDataSource extends
|
|
173
|
+
export class PostgresDataSource extends BasePostgresDataSource<IDSConfigs> {
|
|
157
174
|
constructor() {
|
|
158
175
|
super({
|
|
159
176
|
name: PostgresDataSource.name,
|
|
@@ -185,7 +202,8 @@ export class PostgresDataSource extends BaseDataSource<IDSConfigs> {
|
|
|
185
202
|
|
|
186
203
|
```typescript
|
|
187
204
|
// repositories/user.repository.ts
|
|
188
|
-
import {
|
|
205
|
+
import { repository } from '@venizia/ignis';
|
|
206
|
+
import { PersistableRepository } from '@venizia/ignis/postgres';
|
|
189
207
|
import { User } from '../models/user.model';
|
|
190
208
|
import { PostgresDataSource } from '../datasources/postgres.datasource';
|
|
191
209
|
|
|
@@ -201,7 +219,7 @@ export class UserRepository extends PersistableRepository<typeof User.schema> {
|
|
|
201
219
|
// controllers/user.controller.ts
|
|
202
220
|
import {
|
|
203
221
|
BaseRestController, controller, get, post,
|
|
204
|
-
inject, jsonContent, jsonResponse,
|
|
222
|
+
inject, jsonContent, jsonResponse, TRouteContext,
|
|
205
223
|
} from '@venizia/ignis';
|
|
206
224
|
import { z } from '@hono/zod-openapi';
|
|
207
225
|
import { UserRepository } from '../repositories/user.repository';
|
|
@@ -209,7 +227,7 @@ import { UserRepository } from '../repositories/user.repository';
|
|
|
209
227
|
@controller({ path: '/users' })
|
|
210
228
|
export class UserController extends BaseRestController {
|
|
211
229
|
constructor(
|
|
212
|
-
@inject({ key: 'repositories.UserRepository' }) private
|
|
230
|
+
@inject({ key: 'repositories.UserRepository' }) private userRepository: UserRepository,
|
|
213
231
|
) {
|
|
214
232
|
super({ scope: UserController.name });
|
|
215
233
|
}
|
|
@@ -225,7 +243,7 @@ export class UserController extends BaseRestController {
|
|
|
225
243
|
},
|
|
226
244
|
})
|
|
227
245
|
async listUsers(context: TRouteContext) {
|
|
228
|
-
const users = await this.
|
|
246
|
+
const users = await this.userRepository.find({ filter: {} });
|
|
229
247
|
return context.json(users, 200);
|
|
230
248
|
}
|
|
231
249
|
|
|
@@ -245,7 +263,7 @@ export class UserController extends BaseRestController {
|
|
|
245
263
|
})
|
|
246
264
|
async createUser(context: TRouteContext) {
|
|
247
265
|
const body = context.req.valid<{ username: string; email: string; password: string }>('json');
|
|
248
|
-
const result = await this.
|
|
266
|
+
const result = await this.userRepository.create({ data: body });
|
|
249
267
|
return context.json(result, 200);
|
|
250
268
|
}
|
|
251
269
|
}
|
|
@@ -352,7 +370,7 @@ app.start();
|
|
|
352
370
|
|
|
353
371
|
**`registerComponents()`** -- Iterates all bindings tagged `components`, calls `configure()` on each. Components can register additional datasources during their configuration (the method re-fetches bindings after each component to pick up dynamically added datasources).
|
|
354
372
|
|
|
355
|
-
**`registerControllers()`** --
|
|
373
|
+
**`registerControllers()`** -- Dispatches per configured transport (`configs.transports`, default `['rest']`): a `RestComponent` handles REST controllers (validates that `@controller` metadata has a `path`, calls `configure()` -- which triggers `binding()` and `registerRoutesFromRegistry()` -- then mounts the controller's router at its configured path on the root router), and, when `'grpc'` is included, a `GrpcComponent` handles gRPC controllers. A gRPC controller discovered without `'grpc'` in `transports` is logged as an error and skipped.
|
|
356
374
|
|
|
357
375
|
### Key Application Methods
|
|
358
376
|
|
|
@@ -456,14 +474,16 @@ The runtime is detected via `RuntimeModules.detect()` which checks for the prese
|
|
|
456
474
|
|
|
457
475
|
```typescript
|
|
458
476
|
interface IApplicationConfigs {
|
|
459
|
-
host?: string; // Server host (default: 'localhost' or APP_ENV_SERVER_HOST env)
|
|
477
|
+
host?: string; // Server host (default: 'localhost' or APP_ENV_SERVER_HOST/HOST env)
|
|
460
478
|
port?: number; // Server port (default: 3000 or PORT/APP_ENV_SERVER_PORT env)
|
|
461
479
|
|
|
462
480
|
path: {
|
|
463
481
|
base: string; // Base path for all routes (e.g., '/api')
|
|
464
|
-
isStrict: boolean; //
|
|
482
|
+
isStrict: boolean; // Currently unused -- see strictPath below
|
|
465
483
|
};
|
|
466
484
|
|
|
485
|
+
strictPath?: boolean; // When true (default), '/users' and '/users/' are different routes
|
|
486
|
+
|
|
467
487
|
requestId?: {
|
|
468
488
|
isStrict: boolean; // Enforce request ID on all requests
|
|
469
489
|
};
|
|
@@ -480,12 +500,16 @@ interface IApplicationConfigs {
|
|
|
480
500
|
|
|
481
501
|
bootOptions?: IBootOptions; // Convention-based auto-discovery options
|
|
482
502
|
|
|
503
|
+
transports?: ('rest' | 'grpc')[]; // Controller transports to enable (default: ['rest'])
|
|
504
|
+
|
|
483
505
|
debug?: {
|
|
484
506
|
shouldShowRoutes?: boolean; // Print all registered routes on startup
|
|
485
507
|
};
|
|
486
508
|
}
|
|
487
509
|
```
|
|
488
510
|
|
|
511
|
+
**Note:** the actual Hono strict-routing flag is read from the top-level `strictPath` (defaulting to `true`), not from `path.isStrict` -- `path.isStrict` is declared on the type but not currently read anywhere.
|
|
512
|
+
|
|
489
513
|
```typescript
|
|
490
514
|
interface IApplicationInfo {
|
|
491
515
|
name: string;
|
|
@@ -510,7 +534,7 @@ All controllers extend `BaseRestController`, which provides:
|
|
|
510
534
|
- Zod-based request validation with automatic 422 error responses
|
|
511
535
|
|
|
512
536
|
```typescript
|
|
513
|
-
abstract class BaseRestController extends
|
|
537
|
+
abstract class BaseRestController extends AbstractRestController {
|
|
514
538
|
// Register routes -- override this method
|
|
515
539
|
abstract binding(): ValueOrPromise<void>;
|
|
516
540
|
|
|
@@ -538,7 +562,7 @@ Use `@get`, `@post`, `@put`, `@patch`, `@del`, or the generic `@api` decorators.
|
|
|
538
562
|
@controller({ path: '/products' })
|
|
539
563
|
class ProductController extends BaseRestController {
|
|
540
564
|
constructor(
|
|
541
|
-
@inject({ key: 'repositories.ProductRepository' }) private
|
|
565
|
+
@inject({ key: 'repositories.ProductRepository' }) private productRepository: ProductRepository,
|
|
542
566
|
@inject({ key: 'services.InventoryService' }) private inventoryService: InventoryService,
|
|
543
567
|
) {
|
|
544
568
|
super({ scope: ProductController.name });
|
|
@@ -562,7 +586,7 @@ class ProductController extends BaseRestController {
|
|
|
562
586
|
},
|
|
563
587
|
})
|
|
564
588
|
async list(context: TRouteContext) {
|
|
565
|
-
const products = await this.
|
|
589
|
+
const products = await this.productRepository.find({
|
|
566
590
|
filter: { order: ['createdAt DESC'], limit: 20 },
|
|
567
591
|
});
|
|
568
592
|
return context.json(products, 200);
|
|
@@ -586,7 +610,7 @@ class ProductController extends BaseRestController {
|
|
|
586
610
|
})
|
|
587
611
|
async getById(context: TRouteContext) {
|
|
588
612
|
const { id } = context.req.valid<{ id: number }>('param');
|
|
589
|
-
const product = await this.
|
|
613
|
+
const product = await this.productRepository.findById({ id });
|
|
590
614
|
if (!product) {
|
|
591
615
|
return context.json({ message: 'Product not found' }, 404);
|
|
592
616
|
}
|
|
@@ -620,7 +644,7 @@ class ProductController extends BaseRestController {
|
|
|
620
644
|
category: string;
|
|
621
645
|
description?: string;
|
|
622
646
|
}>('json');
|
|
623
|
-
const result = await this.
|
|
647
|
+
const result = await this.productRepository.create({ data });
|
|
624
648
|
return context.json(result, 200);
|
|
625
649
|
}
|
|
626
650
|
}
|
|
@@ -640,7 +664,7 @@ override binding() {
|
|
|
640
664
|
responses: jsonResponse({ schema: z.array(ProductSchema) }),
|
|
641
665
|
},
|
|
642
666
|
handler: async (context) => {
|
|
643
|
-
const products = await this.
|
|
667
|
+
const products = await this.productRepository.find({ filter: {} });
|
|
644
668
|
return context.json(products, 200);
|
|
645
669
|
},
|
|
646
670
|
});
|
|
@@ -656,7 +680,7 @@ override binding() {
|
|
|
656
680
|
},
|
|
657
681
|
handler: async (context) => {
|
|
658
682
|
const { id } = context.req.valid<{ id: number }>('param');
|
|
659
|
-
const result = await this.
|
|
683
|
+
const result = await this.productRepository.deleteById({ id });
|
|
660
684
|
return context.json(result, 200);
|
|
661
685
|
},
|
|
662
686
|
});
|
|
@@ -679,7 +703,7 @@ override binding() {
|
|
|
679
703
|
}).to({
|
|
680
704
|
handler: async (context) => {
|
|
681
705
|
const { id } = context.req.valid<{ id: number }>('param');
|
|
682
|
-
const product = await this.
|
|
706
|
+
const product = await this.productRepository.findById({ id });
|
|
683
707
|
return context.json(product, 200);
|
|
684
708
|
},
|
|
685
709
|
});
|
|
@@ -703,7 +727,7 @@ This means you never manually wire auth middleware -- it is all declarative.
|
|
|
703
727
|
You can pass additional Hono middleware to any route:
|
|
704
728
|
|
|
705
729
|
```typescript
|
|
706
|
-
import { rateLimiter } from 'hono
|
|
730
|
+
import { rateLimiter } from 'hono-rate-limiter'; // separate package -- Hono ships no built-in rate limiter
|
|
707
731
|
import { cors } from 'hono/cors';
|
|
708
732
|
|
|
709
733
|
@post({
|
|
@@ -737,6 +761,7 @@ Routes automatically validate request parameters, query strings, headers, and bo
|
|
|
737
761
|
age: z.number().int().min(18, 'Must be at least 18'),
|
|
738
762
|
role: z.enum(['admin', 'user', 'moderator']),
|
|
739
763
|
}),
|
|
764
|
+
description: 'New user data',
|
|
740
765
|
}),
|
|
741
766
|
query: z.object({
|
|
742
767
|
dryRun: z.string().optional().transform(v => v === 'true'),
|
|
@@ -760,18 +785,23 @@ On validation failure, the error handler returns:
|
|
|
760
785
|
|
|
761
786
|
```json
|
|
762
787
|
{
|
|
763
|
-
"message": "
|
|
788
|
+
"message": "Invalid email format",
|
|
789
|
+
"messageCode": "invalid_format",
|
|
764
790
|
"statusCode": 422,
|
|
765
791
|
"requestId": "abc-123",
|
|
766
792
|
"details": {
|
|
793
|
+
"url": "http://localhost:3000/api/users",
|
|
794
|
+
"path": "/api/users",
|
|
767
795
|
"cause": [
|
|
768
|
-
{ "path": "email", "message": "Invalid email format", "code": "
|
|
796
|
+
{ "path": "email", "message": "Invalid email format", "code": "invalid_format" },
|
|
769
797
|
{ "path": "age", "message": "Must be at least 18", "code": "too_small" }
|
|
770
798
|
]
|
|
771
799
|
}
|
|
772
800
|
}
|
|
773
801
|
```
|
|
774
802
|
|
|
803
|
+
Top-level `message`/`messageCode` come from the first Zod issue (or a schema-supplied `params.code`, if present); `"ValidationError"` is only used as a fallback when no issue list can be parsed at all. `details.stack` is included alongside `cause` outside production.
|
|
804
|
+
|
|
775
805
|
### Accessing Hono Context
|
|
776
806
|
|
|
777
807
|
The `context` parameter (`TRouteContext`) provides full access to the Hono request/response:
|
|
@@ -866,6 +896,7 @@ this.defineJSXRoute({
|
|
|
866
896
|
method: 'get',
|
|
867
897
|
description: 'User profile page',
|
|
868
898
|
authenticate: { strategies: ['jwt'] },
|
|
899
|
+
responses: htmlResponse({ description: 'Rendered profile page' }),
|
|
869
900
|
},
|
|
870
901
|
handler: (context) => {
|
|
871
902
|
const user = context.get('auth.current.user');
|
|
@@ -878,7 +909,7 @@ this.defineJSXRoute({
|
|
|
878
909
|
|
|
879
910
|
| Decorator | Description |
|
|
880
911
|
| --- | --- |
|
|
881
|
-
| `@controller({ path
|
|
912
|
+
| `@controller({ path })` | Class decorator -- registers controller base path |
|
|
882
913
|
| `@get({ configs })` | GET route -- method is set automatically |
|
|
883
914
|
| `@post({ configs })` | POST route |
|
|
884
915
|
| `@put({ configs })` | PUT route |
|
|
@@ -974,7 +1005,7 @@ Each generated endpoint includes:
|
|
|
974
1005
|
- Conditional count response via `x-request-count` header -- send `x-request-count: false` to get data only without the wrapping `{ count, data }` object.
|
|
975
1006
|
- Content-Range header for paginated find results (e.g., `records 0-19/150`).
|
|
976
1007
|
- Authentication and authorization middleware from controller-level or route-level config.
|
|
977
|
-
- `X-Response-Count
|
|
1008
|
+
- `X-Response-Count` response header with the count of returned records.
|
|
978
1009
|
|
|
979
1010
|
#### Customizing Controller Factory Routes
|
|
980
1011
|
|
|
@@ -1007,17 +1038,22 @@ routes: {
|
|
|
1007
1038
|
### Hierarchy
|
|
1008
1039
|
|
|
1009
1040
|
```
|
|
1010
|
-
AbstractRepository
|
|
1011
|
-
extends
|
|
1041
|
+
AbstractRepository<TDataObject, TPersistObject, TOptions>
|
|
1042
|
+
extends BaseHelper (engine-neutral: lazy dataSource/entity resolution, class-keyed
|
|
1043
|
+
| @model settings getters -- hiddenFields/defaultWhere/defaultLimit --
|
|
1044
|
+
| all CRUD verbs abstract)
|
|
1012
1045
|
|
|
|
1013
|
-
+--
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1046
|
+
+-- PostgresBaseRepository (+ FilterBuilder, hidden-column query/RETURNING plumbing, drizzle wiring)
|
|
1047
|
+
|
|
|
1048
|
+
+-- ReadableRepository (read operations only -- write operations throw errors)
|
|
1049
|
+
|
|
|
1050
|
+
+-- PersistableRepository (+ create, update, delete operations)
|
|
1051
|
+
|
|
|
1052
|
+
+-- DefaultCRUDRepository (alias -- identical to PersistableRepository)
|
|
1053
|
+
+-- SoftDeletableRepository (extends DefaultCRUDRepository -- soft-delete semantics)
|
|
1018
1054
|
```
|
|
1019
1055
|
|
|
1020
|
-
`PersistableRepository` is the recommended base class for most use cases. `DefaultCRUDRepository` is a convenience alias. Use `ReadableRepository` when you need a repository that should only read data (e.g., reporting views, read replicas).
|
|
1056
|
+
`AbstractRepository` is the single engine-neutral base every connector's hierarchy extends directly -- the search branch (Typesense) re-parents its own `TypesenseBaseRepository` onto it the same way. `PersistableRepository` is the recommended base class for most use cases. `DefaultCRUDRepository` is a convenience alias. Use `ReadableRepository` when you need a repository that should only read data (e.g., reporting views, read replicas).
|
|
1021
1057
|
|
|
1022
1058
|
### Defining a Repository
|
|
1023
1059
|
|
|
@@ -1061,10 +1097,10 @@ export class UserRepository extends PersistableRepository<typeof User.schema> {
|
|
|
1061
1097
|
|
|
1062
1098
|
```typescript
|
|
1063
1099
|
// Simple count
|
|
1064
|
-
const { count } = await
|
|
1100
|
+
const { count } = await repository.count({ where: { status: 'active' } });
|
|
1065
1101
|
|
|
1066
1102
|
// Count with complex conditions
|
|
1067
|
-
const { count } = await
|
|
1103
|
+
const { count } = await repository.count({
|
|
1068
1104
|
where: {
|
|
1069
1105
|
and: [
|
|
1070
1106
|
{ role: { inq: ['admin', 'moderator'] } },
|
|
@@ -1075,7 +1111,7 @@ const { count } = await repo.count({
|
|
|
1075
1111
|
});
|
|
1076
1112
|
|
|
1077
1113
|
// Count within a transaction
|
|
1078
|
-
const { count } = await
|
|
1114
|
+
const { count } = await repository.count({
|
|
1079
1115
|
where: { status: 'pending' },
|
|
1080
1116
|
options: { transaction: tx },
|
|
1081
1117
|
});
|
|
@@ -1084,7 +1120,7 @@ const { count } = await repo.count({
|
|
|
1084
1120
|
#### `existsWith()` -- Check Existence
|
|
1085
1121
|
|
|
1086
1122
|
```typescript
|
|
1087
|
-
const emailTaken = await
|
|
1123
|
+
const emailTaken = await repository.existsWith({
|
|
1088
1124
|
where: { email: 'john@example.com' },
|
|
1089
1125
|
});
|
|
1090
1126
|
|
|
@@ -1097,7 +1133,7 @@ if (emailTaken) {
|
|
|
1097
1133
|
|
|
1098
1134
|
```typescript
|
|
1099
1135
|
// Basic find with filter
|
|
1100
|
-
const users = await
|
|
1136
|
+
const users = await repository.find({
|
|
1101
1137
|
filter: {
|
|
1102
1138
|
where: { status: 'active' },
|
|
1103
1139
|
fields: ['id', 'name', 'email'],
|
|
@@ -1108,7 +1144,7 @@ const users = await repo.find({
|
|
|
1108
1144
|
});
|
|
1109
1145
|
|
|
1110
1146
|
// Find with pagination range info
|
|
1111
|
-
const { data, range } = await
|
|
1147
|
+
const { data, range } = await repository.find({
|
|
1112
1148
|
filter: { where: { status: 'active' }, limit: 20, skip: 40 },
|
|
1113
1149
|
options: { shouldQueryRange: true },
|
|
1114
1150
|
});
|
|
@@ -1116,7 +1152,7 @@ const { data, range } = await repo.find({
|
|
|
1116
1152
|
// range = { start: 40, end: 59, total: 150 }
|
|
1117
1153
|
|
|
1118
1154
|
// Find with relation inclusion (uses Query API)
|
|
1119
|
-
const usersWithPosts = await
|
|
1155
|
+
const usersWithPosts = await repository.find({
|
|
1120
1156
|
filter: {
|
|
1121
1157
|
where: { isActive: true },
|
|
1122
1158
|
include: [
|
|
@@ -1126,21 +1162,21 @@ const usersWithPosts = await repo.find({
|
|
|
1126
1162
|
});
|
|
1127
1163
|
|
|
1128
1164
|
// Find all (bypass default filter for admin views)
|
|
1129
|
-
const allUsers = await
|
|
1165
|
+
const allUsers = await repository.find({
|
|
1130
1166
|
filter: {},
|
|
1131
1167
|
options: { shouldSkipDefaultFilter: true },
|
|
1132
1168
|
});
|
|
1133
1169
|
|
|
1134
1170
|
// Find with transaction
|
|
1135
|
-
const users = await
|
|
1171
|
+
const users = await repository.find({
|
|
1136
1172
|
filter: { where: { batchId: currentBatch } },
|
|
1137
1173
|
options: { transaction: tx },
|
|
1138
1174
|
});
|
|
1139
1175
|
|
|
1140
|
-
// Find with
|
|
1141
|
-
const users = await
|
|
1176
|
+
// Find with row-level locking (requires a transaction; incompatible with include/fields)
|
|
1177
|
+
const users = await repository.find({
|
|
1142
1178
|
filter: { where: { status: 'active' } },
|
|
1143
|
-
options: {
|
|
1179
|
+
options: { transaction: tx, lock: { strength: 'update' } },
|
|
1144
1180
|
});
|
|
1145
1181
|
```
|
|
1146
1182
|
|
|
@@ -1149,7 +1185,7 @@ const users = await repo.find({
|
|
|
1149
1185
|
`findOne()` accepts a full filter with `where`, `fields`, `include`, and `order`. It returns the first matching record:
|
|
1150
1186
|
|
|
1151
1187
|
```typescript
|
|
1152
|
-
const user = await
|
|
1188
|
+
const user = await repository.findOne({
|
|
1153
1189
|
filter: {
|
|
1154
1190
|
where: { email: 'john@example.com' },
|
|
1155
1191
|
fields: ['id', 'name', 'email'],
|
|
@@ -1162,7 +1198,7 @@ const user = await repo.findOne({
|
|
|
1162
1198
|
`findById()` is a convenience wrapper around `findOne()` that automatically sets `where: { id }`. It accepts an optional filter **without** the `where` clause:
|
|
1163
1199
|
|
|
1164
1200
|
```typescript
|
|
1165
|
-
const user = await
|
|
1201
|
+
const user = await repository.findById({
|
|
1166
1202
|
id: 42,
|
|
1167
1203
|
filter: {
|
|
1168
1204
|
fields: ['id', 'name', 'email'],
|
|
@@ -1179,20 +1215,20 @@ const user = await repo.findById({
|
|
|
1179
1215
|
|
|
1180
1216
|
```typescript
|
|
1181
1217
|
// Create and return the created record (default: shouldReturn = true)
|
|
1182
|
-
const { count, data } = await
|
|
1218
|
+
const { count, data } = await repository.create({
|
|
1183
1219
|
data: { username: 'john', email: 'john@example.com', role: 'user' },
|
|
1184
1220
|
});
|
|
1185
1221
|
// count = 1, data = { id: 1, username: 'john', ... }
|
|
1186
1222
|
|
|
1187
1223
|
// Create without returning data (faster -- skips RETURNING clause)
|
|
1188
|
-
const { count } = await
|
|
1224
|
+
const { count } = await repository.create({
|
|
1189
1225
|
data: { username: 'john', email: 'john@example.com' },
|
|
1190
1226
|
options: { shouldReturn: false },
|
|
1191
1227
|
});
|
|
1192
1228
|
// count = 1, data = null
|
|
1193
1229
|
|
|
1194
1230
|
// Create within a transaction
|
|
1195
|
-
const { data: user } = await
|
|
1231
|
+
const { data: user } = await repository.create({
|
|
1196
1232
|
data: { username: 'john', email: 'john@example.com' },
|
|
1197
1233
|
options: { transaction: tx },
|
|
1198
1234
|
});
|
|
@@ -1202,7 +1238,7 @@ const { data: user } = await repo.create({
|
|
|
1202
1238
|
|
|
1203
1239
|
```typescript
|
|
1204
1240
|
// Bulk create and return all records
|
|
1205
|
-
const { count, data } = await
|
|
1241
|
+
const { count, data } = await repository.createAll({
|
|
1206
1242
|
data: [
|
|
1207
1243
|
{ username: 'john', email: 'john@example.com' },
|
|
1208
1244
|
{ username: 'jane', email: 'jane@example.com' },
|
|
@@ -1212,7 +1248,7 @@ const { count, data } = await repo.createAll({
|
|
|
1212
1248
|
// count = 3, data = [{ id: 1, ... }, { id: 2, ... }, { id: 3, ... }]
|
|
1213
1249
|
|
|
1214
1250
|
// Bulk create without returning (faster for large inserts)
|
|
1215
|
-
const { count } = await
|
|
1251
|
+
const { count } = await repository.createAll({
|
|
1216
1252
|
data: largeDataArray,
|
|
1217
1253
|
options: { shouldReturn: false },
|
|
1218
1254
|
});
|
|
@@ -1222,14 +1258,14 @@ const { count } = await repo.createAll({
|
|
|
1222
1258
|
|
|
1223
1259
|
```typescript
|
|
1224
1260
|
// Update by ID and return the updated record
|
|
1225
|
-
const { count, data } = await
|
|
1261
|
+
const { count, data } = await repository.updateById({
|
|
1226
1262
|
id: 42,
|
|
1227
1263
|
data: { email: 'new@example.com', status: 'verified' },
|
|
1228
1264
|
});
|
|
1229
1265
|
// count = 1, data = { id: 42, email: 'new@example.com', ... }
|
|
1230
1266
|
|
|
1231
1267
|
// Update JSON fields using dot notation
|
|
1232
|
-
const { data } = await
|
|
1268
|
+
const { data } = await repository.updateById({
|
|
1233
1269
|
id: 42,
|
|
1234
1270
|
data: {
|
|
1235
1271
|
'metadata.theme': 'dark',
|
|
@@ -1242,14 +1278,14 @@ const { data } = await repo.updateById({
|
|
|
1242
1278
|
|
|
1243
1279
|
```typescript
|
|
1244
1280
|
// Update all matching records
|
|
1245
|
-
const { count, data } = await
|
|
1281
|
+
const { count, data } = await repository.updateAll({
|
|
1246
1282
|
data: { status: 'inactive' },
|
|
1247
1283
|
where: { lastLoginAt: { lt: new Date('2024-01-01') } },
|
|
1248
1284
|
});
|
|
1249
1285
|
// count = 25, data = [...25 updated records...]
|
|
1250
1286
|
|
|
1251
1287
|
// updateBy is an alias for updateAll
|
|
1252
|
-
const { count } = await
|
|
1288
|
+
const { count } = await repository.updateBy({
|
|
1253
1289
|
data: { isNotified: true },
|
|
1254
1290
|
where: { role: 'subscriber' },
|
|
1255
1291
|
options: { shouldReturn: false },
|
|
@@ -1257,7 +1293,7 @@ const { count } = await repo.updateBy({
|
|
|
1257
1293
|
|
|
1258
1294
|
// SAFETY: Empty where throws an error to prevent accidental mass updates
|
|
1259
1295
|
// Use force: true to explicitly allow it
|
|
1260
|
-
const { count } = await
|
|
1296
|
+
const { count } = await repository.updateAll({
|
|
1261
1297
|
data: { version: 2 },
|
|
1262
1298
|
where: {},
|
|
1263
1299
|
options: { force: true },
|
|
@@ -1268,22 +1304,22 @@ const { count } = await repo.updateAll({
|
|
|
1268
1304
|
|
|
1269
1305
|
```typescript
|
|
1270
1306
|
// Delete by ID (returns deleted record)
|
|
1271
|
-
const { count, data } = await
|
|
1307
|
+
const { count, data } = await repository.deleteById({ id: 42 });
|
|
1272
1308
|
// count = 1, data = { id: 42, username: 'john', ... }
|
|
1273
1309
|
|
|
1274
1310
|
// Delete all matching records
|
|
1275
|
-
const { count, data } = await
|
|
1311
|
+
const { count, data } = await repository.deleteAll({
|
|
1276
1312
|
where: { status: 'inactive' },
|
|
1277
1313
|
});
|
|
1278
1314
|
|
|
1279
1315
|
// deleteBy is an alias for deleteAll
|
|
1280
|
-
const { count } = await
|
|
1316
|
+
const { count } = await repository.deleteBy({
|
|
1281
1317
|
where: { expiresAt: { lt: new Date() } },
|
|
1282
1318
|
options: { shouldReturn: false },
|
|
1283
1319
|
});
|
|
1284
1320
|
|
|
1285
1321
|
// SAFETY: Empty where throws an error. Use force: true to allow.
|
|
1286
|
-
const { count } = await
|
|
1322
|
+
const { count } = await repository.deleteAll({
|
|
1287
1323
|
where: {},
|
|
1288
1324
|
options: { force: true, shouldReturn: false },
|
|
1289
1325
|
});
|
|
@@ -1299,95 +1335,28 @@ interface TFilter<T> {
|
|
|
1299
1335
|
order?: string[]; // Sorting (e.g., ['createdAt DESC', 'name ASC'])
|
|
1300
1336
|
limit?: number; // Max results (default: 10)
|
|
1301
1337
|
skip?: number; // Offset
|
|
1302
|
-
offset?: number; //
|
|
1338
|
+
offset?: number; // Fallback for skip -- if both are set, skip wins
|
|
1303
1339
|
}
|
|
1304
1340
|
```
|
|
1305
1341
|
|
|
1306
|
-
#### Where Operators
|
|
1307
|
-
|
|
1308
|
-
**Comparison operators:**
|
|
1342
|
+
#### Where Operators
|
|
1309
1343
|
|
|
1310
|
-
|
|
1311
|
-
| --- | --- | --- |
|
|
1312
|
-
| _(equality)_ | Exact match | `{ status: 'active' }` |
|
|
1313
|
-
| `eq` | Equal | `{ age: { eq: 25 } }` |
|
|
1314
|
-
| `ne` / `neq` | Not equal | `{ role: { neq: 'guest' } }` |
|
|
1315
|
-
| `gt` | Greater than | `{ score: { gt: 90 } }` |
|
|
1316
|
-
| `gte` | Greater than or equal | `{ age: { gte: 18 } }` |
|
|
1317
|
-
| `lt` | Less than | `{ price: { lt: 100 } }` |
|
|
1318
|
-
| `lte` | Less than or equal | `{ priority: { lte: 5 } }` |
|
|
1319
|
-
|
|
1320
|
-
**Pattern matching operators:**
|
|
1321
|
-
|
|
1322
|
-
| Operator | Description | Example |
|
|
1323
|
-
| --- | --- | --- |
|
|
1324
|
-
| `like` | SQL LIKE (case-sensitive) | `{ name: { like: '%john%' } }` |
|
|
1325
|
-
| `ilike` | Case-insensitive LIKE | `{ email: { ilike: '%@GMAIL.COM' } }` |
|
|
1326
|
-
| `nlike` | NOT LIKE | `{ name: { nlike: '%test%' } }` |
|
|
1327
|
-
| `nilike` | NOT ILIKE | `{ email: { nilike: '%spam%' } }` |
|
|
1328
|
-
| `regexp` | POSIX regex (case-sensitive) | `{ code: { regexp: '^[A-Z]{3}' } }` |
|
|
1329
|
-
| `iregexp` | POSIX regex (case-insensitive) | `{ name: { iregexp: '^john' } }` |
|
|
1330
|
-
|
|
1331
|
-
**Array/set operators:**
|
|
1332
|
-
|
|
1333
|
-
| Operator | Description | Example |
|
|
1334
|
-
| --- | --- | --- |
|
|
1335
|
-
| `inq` / `in` | IN array | `{ status: { inq: ['active', 'pending'] } }` |
|
|
1336
|
-
| `nin` | NOT IN array | `{ role: { nin: ['banned', 'deleted'] } }` |
|
|
1337
|
-
| `between` | BETWEEN two values | `{ age: { between: [18, 65] } }` |
|
|
1338
|
-
| `notBetween` | NOT BETWEEN | `{ score: { notBetween: [0, 10] } }` |
|
|
1339
|
-
|
|
1340
|
-
**Null check operators:**
|
|
1341
|
-
|
|
1342
|
-
| Operator | Description | Example |
|
|
1343
|
-
| --- | --- | --- |
|
|
1344
|
-
| `is` | IS NULL (when value is null) | `{ deletedAt: { is: null } }` |
|
|
1345
|
-
| `isn` | IS NOT NULL (when value is null) | `{ email: { isn: null } }` |
|
|
1346
|
-
|
|
1347
|
-
**Logical operators:**
|
|
1348
|
-
|
|
1349
|
-
| Operator | Description | Example |
|
|
1350
|
-
| --- | --- | --- |
|
|
1351
|
-
| `and` | Logical AND | `{ and: [{ status: 'active' }, { role: 'admin' }] }` |
|
|
1352
|
-
| `or` | Logical OR | `{ or: [{ role: 'admin' }, { role: 'moderator' }] }` |
|
|
1353
|
-
|
|
1354
|
-
**PostgreSQL array column operators** (for columns defined as `text[]`, `integer[]`, etc.):
|
|
1355
|
-
|
|
1356
|
-
| Operator | SQL | Description | Example |
|
|
1357
|
-
| --- | --- | --- | --- |
|
|
1358
|
-
| `contains` | `@>` | Array contains all elements | `{ tags: { contains: ['urgent', 'bug'] } }` |
|
|
1359
|
-
| `containedBy` | `<@` | Array is contained by | `{ tags: { containedBy: ['a', 'b', 'c'] } }` |
|
|
1360
|
-
| `overlaps` | `&&` | Arrays have common elements | `{ categories: { overlaps: ['tech', 'science'] } }` |
|
|
1361
|
-
|
|
1362
|
-
**JSON path queries** (for `json`/`jsonb` columns):
|
|
1344
|
+
Comparison (`eq`/`neq`/`gt`/`gte`/`lt`/`lte`), pattern matching (`like`/`ilike`/`regexp` and negations), array/set (`inq`/`nin`/`between`/`notBetween`), null checks (`is`/`isn`), logical (`and`/`or`), and PostgreSQL array-column operators (`contains`/`containedBy`/`overlaps`) are all supported. JSON path queries (dot notation into `json`/`jsonb` columns, with automatic numeric casting for comparison operators) and JSON path sorting work the same way. Full operator tables: [Filter System Quick Reference](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/references/base/filter-system/quick-reference.md).
|
|
1363
1345
|
|
|
1364
1346
|
```typescript
|
|
1365
|
-
//
|
|
1366
|
-
const users = await
|
|
1367
|
-
filter: {
|
|
1368
|
-
where: {
|
|
1369
|
-
'metadata.theme': 'dark',
|
|
1370
|
-
'settings.notifications.email': true,
|
|
1371
|
-
'preferences.items[0].enabled': { eq: true },
|
|
1372
|
-
'metadata.score': { gt: 50, lte: 100 },
|
|
1373
|
-
},
|
|
1374
|
-
},
|
|
1347
|
+
// Comparison + pattern matching
|
|
1348
|
+
const users = await repository.find({
|
|
1349
|
+
filter: { where: { age: { gte: 18 }, email: { ilike: '%@gmail.com' } } },
|
|
1375
1350
|
});
|
|
1376
|
-
```
|
|
1377
|
-
|
|
1378
|
-
JSON path queries automatically handle numeric casting: when a numeric comparison operator (`gt`, `gte`, `lt`, `lte`, `between`) is used with a numeric value, the extracted text is safely cast to `numeric` via a CASE expression.
|
|
1379
1351
|
|
|
1380
|
-
|
|
1352
|
+
// Array/set + logical
|
|
1353
|
+
const users = await repository.find({
|
|
1354
|
+
filter: { where: { and: [{ status: { inq: ['active', 'pending'] } }, { role: { neq: 'guest' } }] } },
|
|
1355
|
+
});
|
|
1381
1356
|
|
|
1382
|
-
|
|
1383
|
-
const
|
|
1384
|
-
filter: {
|
|
1385
|
-
order: [
|
|
1386
|
-
'createdAt DESC',
|
|
1387
|
-
'metadata.priority DESC',
|
|
1388
|
-
'data.nested.score ASC',
|
|
1389
|
-
],
|
|
1390
|
-
},
|
|
1357
|
+
// JSON path query (dot notation into a jsonb column)
|
|
1358
|
+
const users = await repository.find({
|
|
1359
|
+
filter: { where: { 'metadata.score': { gt: 50, lte: 100 } } },
|
|
1391
1360
|
});
|
|
1392
1361
|
```
|
|
1393
1362
|
|
|
@@ -1395,12 +1364,12 @@ const products = await repo.find({
|
|
|
1395
1364
|
|
|
1396
1365
|
```typescript
|
|
1397
1366
|
// Array format -- include only these columns
|
|
1398
|
-
const users = await
|
|
1367
|
+
const users = await repository.find({
|
|
1399
1368
|
filter: { fields: ['id', 'name', 'email'] },
|
|
1400
1369
|
});
|
|
1401
1370
|
|
|
1402
1371
|
// Object format -- include/exclude
|
|
1403
|
-
const users = await
|
|
1372
|
+
const users = await repository.find({
|
|
1404
1373
|
filter: { fields: { id: true, name: true, password: false } },
|
|
1405
1374
|
});
|
|
1406
1375
|
```
|
|
@@ -1409,14 +1378,14 @@ const users = await repo.find({
|
|
|
1409
1378
|
|
|
1410
1379
|
```typescript
|
|
1411
1380
|
// Simple inclusion
|
|
1412
|
-
const users = await
|
|
1381
|
+
const users = await repository.find({
|
|
1413
1382
|
filter: {
|
|
1414
1383
|
include: [{ relation: 'posts' }],
|
|
1415
1384
|
},
|
|
1416
1385
|
});
|
|
1417
1386
|
|
|
1418
1387
|
// With nested filter (scope)
|
|
1419
|
-
const users = await
|
|
1388
|
+
const users = await repository.find({
|
|
1420
1389
|
filter: {
|
|
1421
1390
|
include: [{
|
|
1422
1391
|
relation: 'posts',
|
|
@@ -1431,7 +1400,7 @@ const users = await repo.find({
|
|
|
1431
1400
|
});
|
|
1432
1401
|
|
|
1433
1402
|
// Skip default filter on a specific relation
|
|
1434
|
-
const users = await
|
|
1403
|
+
const users = await repository.find({
|
|
1435
1404
|
filter: {
|
|
1436
1405
|
include: [{
|
|
1437
1406
|
relation: 'archivedPosts',
|
|
@@ -1448,7 +1417,7 @@ Note: Relations are defined on the model via `static relations`. The FilterBuild
|
|
|
1448
1417
|
When `shouldQueryRange: true` is passed to `find()`, the method runs both the data fetch and a count query in parallel, then returns:
|
|
1449
1418
|
|
|
1450
1419
|
```typescript
|
|
1451
|
-
const result = await
|
|
1420
|
+
const result = await repository.find({
|
|
1452
1421
|
filter: { where: { status: 'active' }, limit: 10, skip: 20 },
|
|
1453
1422
|
options: { shouldQueryRange: true },
|
|
1454
1423
|
});
|
|
@@ -1465,19 +1434,19 @@ The `shouldSkipDefaultFilter` option bypasses the model's `defaultFilter`. Commo
|
|
|
1465
1434
|
|
|
1466
1435
|
```typescript
|
|
1467
1436
|
// Admin panel showing all records (including soft-deleted)
|
|
1468
|
-
const allUsers = await
|
|
1437
|
+
const allUsers = await repository.find({
|
|
1469
1438
|
filter: {},
|
|
1470
1439
|
options: { shouldSkipDefaultFilter: true },
|
|
1471
1440
|
});
|
|
1472
1441
|
|
|
1473
1442
|
// Data migration or cleanup script
|
|
1474
|
-
const deletedUsers = await
|
|
1443
|
+
const deletedUsers = await repository.find({
|
|
1475
1444
|
filter: { where: { isDeleted: true } },
|
|
1476
1445
|
options: { shouldSkipDefaultFilter: true },
|
|
1477
1446
|
});
|
|
1478
1447
|
|
|
1479
1448
|
// Export all data for backup
|
|
1480
|
-
const everything = await
|
|
1449
|
+
const everything = await repository.find({
|
|
1481
1450
|
filter: {},
|
|
1482
1451
|
options: { shouldSkipDefaultFilter: true, shouldQueryRange: true },
|
|
1483
1452
|
});
|
|
@@ -1485,32 +1454,41 @@ const everything = await repo.find({
|
|
|
1485
1454
|
|
|
1486
1455
|
### ExtraOptions
|
|
1487
1456
|
|
|
1488
|
-
All repository operations accept an `options` parameter
|
|
1457
|
+
All repository operations accept an `options` parameter. The base shape is `IExtraOptions`:
|
|
1489
1458
|
|
|
1490
1459
|
```typescript
|
|
1491
1460
|
interface IExtraOptions {
|
|
1492
1461
|
transaction?: ITransaction; // Use within a transaction
|
|
1493
|
-
|
|
1494
|
-
shouldQueryRange?: boolean; // Return { data, range: { total, start, end } }
|
|
1462
|
+
log?: { use: boolean; level?: TLogLevel }; // Enable operation logging (write operations only)
|
|
1495
1463
|
shouldSkipDefaultFilter?: boolean; // Bypass model's default filter
|
|
1496
|
-
|
|
1464
|
+
lock?: { // Row-level locking. Requires a transaction; incompatible
|
|
1465
|
+
strength: 'update' | 'no key update' | 'share' | 'key share'; // with include/fields (Query API)
|
|
1466
|
+
config?: { noWait?: true } | { skipLocked?: true };
|
|
1467
|
+
};
|
|
1497
1468
|
}
|
|
1498
1469
|
```
|
|
1499
1470
|
|
|
1500
|
-
|
|
1471
|
+
`shouldReturn` and `shouldQueryRange` are **not** part of `IExtraOptions` -- they only exist as ad-hoc intersections on the specific methods that support them: `shouldReturn` on `create`/`createAll`/`updateById`/`updateAll`/`deleteById`/`deleteAll` (`options: IExtraOptions & { shouldReturn?: boolean }`), and `shouldQueryRange` only on `find` (`options: IExtraOptions & { shouldQueryRange?: boolean }`). `count`/`existsWith` accept neither.
|
|
1472
|
+
|
|
1473
|
+
`log: { use: true }` is only honored by write operations (`create`/`updateById`/`deleteById`/etc.) -- read operations (`find`/`findOne`/`count`/`existsWith`) never inspect it.
|
|
1474
|
+
|
|
1475
|
+
### Hidden Fields and Default Filter
|
|
1476
|
+
|
|
1477
|
+
This behavior lives directly on the repository hierarchy now, not in separate mixins: `AbstractRepository` (`src/base/repositories/core/`) exposes the class-keyed `@model` settings as protected getters -- `hiddenFields`, `defaultWhere`, `defaultLimit` -- resolved once per entity class and memoized; `PostgresBaseRepository` (`src/connectors/postgres/repositories/core/`) is where those getters actually get applied to SQL, via its own `getHiddenProperties()`/`getVisibleProperties()`/`buildQuery()`/`applyDefaultFilter()`. The two mixins this section used to describe (`FieldsVisibilityMixin`, `DefaultFilterMixin`, under `src/connectors/postgres/repositories/mixins/`) are legacy -- no longer composed onto any repository class -- and are not exported from any barrel.
|
|
1501
1478
|
|
|
1502
|
-
####
|
|
1479
|
+
#### Hidden fields
|
|
1503
1480
|
|
|
1504
1481
|
Automatically excludes properties listed in `@model({ settings: { hiddenProperties } })` from all query results at the SQL level -- not post-processing:
|
|
1505
1482
|
|
|
1506
1483
|
```typescript
|
|
1507
1484
|
@model({
|
|
1485
|
+
type: 'entity',
|
|
1508
1486
|
settings: { hiddenProperties: ['password', 'secretToken'] },
|
|
1509
1487
|
})
|
|
1510
|
-
export class User extends
|
|
1488
|
+
export class User extends BasePostgresEntity<typeof User.schema> { ... }
|
|
1511
1489
|
|
|
1512
1490
|
// All repository queries automatically exclude 'password' and 'secretToken'
|
|
1513
|
-
const user = await
|
|
1491
|
+
const user = await userRepository.findById({ id: 1 });
|
|
1514
1492
|
// user.password === undefined (never selected from DB)
|
|
1515
1493
|
|
|
1516
1494
|
// Hidden fields are excluded from:
|
|
@@ -1521,29 +1499,30 @@ const user = await userRepo.findById({ id: 1 });
|
|
|
1521
1499
|
// - Included relation queries (applied recursively)
|
|
1522
1500
|
```
|
|
1523
1501
|
|
|
1524
|
-
|
|
1502
|
+
`PostgresBaseRepository.getVisibleProperties()` caches the visible property set for performance. It computes it once from the schema columns minus hidden properties.
|
|
1525
1503
|
|
|
1526
|
-
####
|
|
1504
|
+
#### Default filter
|
|
1527
1505
|
|
|
1528
1506
|
Automatically applies a default filter to all queries. Common use case -- soft delete:
|
|
1529
1507
|
|
|
1530
1508
|
```typescript
|
|
1531
1509
|
@model({
|
|
1510
|
+
type: 'entity',
|
|
1532
1511
|
settings: { defaultFilter: { where: { isDeleted: false } } },
|
|
1533
1512
|
})
|
|
1534
|
-
export class User extends
|
|
1513
|
+
export class User extends BasePostgresEntity<typeof User.schema> { ... }
|
|
1535
1514
|
|
|
1536
1515
|
// All queries automatically add WHERE is_deleted = false
|
|
1537
|
-
const users = await
|
|
1516
|
+
const users = await userRepository.find({ filter: {} });
|
|
1538
1517
|
|
|
1539
1518
|
// The default filter merges with user-provided filters:
|
|
1540
|
-
const activeAdmins = await
|
|
1519
|
+
const activeAdmins = await userRepository.find({
|
|
1541
1520
|
filter: { where: { role: 'admin' } },
|
|
1542
1521
|
});
|
|
1543
1522
|
// SQL: WHERE is_deleted = false AND role = 'admin'
|
|
1544
1523
|
|
|
1545
1524
|
// Bypass when needed (e.g., admin panel showing all records)
|
|
1546
|
-
const allUsers = await
|
|
1525
|
+
const allUsers = await userRepository.find({
|
|
1547
1526
|
filter: {},
|
|
1548
1527
|
options: { shouldSkipDefaultFilter: true },
|
|
1549
1528
|
});
|
|
@@ -1581,13 +1560,14 @@ Multiple path updates to the same JSON column are chained into a single expressi
|
|
|
1581
1560
|
|
|
1582
1561
|
## Models
|
|
1583
1562
|
|
|
1584
|
-
###
|
|
1563
|
+
### BasePostgresEntity
|
|
1585
1564
|
|
|
1586
|
-
All entities extend `
|
|
1565
|
+
All entities extend `BasePostgresEntity` and define a static `schema` using Drizzle's `pgTable`:
|
|
1587
1566
|
|
|
1588
1567
|
```typescript
|
|
1589
|
-
import {
|
|
1590
|
-
import {
|
|
1568
|
+
import { model } from '@venizia/ignis';
|
|
1569
|
+
import { BasePostgresEntity, TRelationConfig } from '@venizia/ignis/postgres';
|
|
1570
|
+
import { pgTable, text, jsonb, boolean } from 'drizzle-orm/pg-core';
|
|
1591
1571
|
|
|
1592
1572
|
@model({
|
|
1593
1573
|
type: 'entity',
|
|
@@ -1596,7 +1576,7 @@ import { pgTable, text, integer, jsonb, boolean } from 'drizzle-orm/pg-core';
|
|
|
1596
1576
|
defaultFilter: { where: { isDeleted: false } },
|
|
1597
1577
|
},
|
|
1598
1578
|
})
|
|
1599
|
-
export class User extends
|
|
1579
|
+
export class User extends BasePostgresEntity<typeof User.schema> {
|
|
1600
1580
|
static override schema = pgTable('User', {
|
|
1601
1581
|
...generateIdColumnDefs({ id: { dataType: 'string' } }),
|
|
1602
1582
|
...generateTzColumnDefs({
|
|
@@ -1612,12 +1592,14 @@ export class User extends BaseEntity<typeof User.schema> {
|
|
|
1612
1592
|
metadata: jsonb('metadata').$type<{ theme?: string; score?: number }>(),
|
|
1613
1593
|
});
|
|
1614
1594
|
|
|
1615
|
-
static override relations = () => [
|
|
1616
|
-
|
|
1595
|
+
static override relations = (): TRelationConfig[] => [
|
|
1596
|
+
// 'many' side: no fields/references -- relationName points at the 'one' relation declared on Post
|
|
1597
|
+
{ name: 'posts', type: 'many', schema: Post.schema, metadata: { relationName: 'author' } },
|
|
1598
|
+
// 'one' side: fields/references establish the FK, since it's declared on this table
|
|
1617
1599
|
{ name: 'profile', type: 'one', schema: Profile.schema, metadata: { fields: [Profile.schema.userId], references: [User.schema.id] } },
|
|
1618
1600
|
];
|
|
1619
1601
|
|
|
1620
|
-
static TABLE_NAME = 'User';
|
|
1602
|
+
static override TABLE_NAME = 'User';
|
|
1621
1603
|
}
|
|
1622
1604
|
```
|
|
1623
1605
|
|
|
@@ -1641,7 +1623,7 @@ When `@model` is applied, it registers the class with the `MetadataRegistry`, ex
|
|
|
1641
1623
|
|
|
1642
1624
|
### Schema Generation
|
|
1643
1625
|
|
|
1644
|
-
`
|
|
1626
|
+
`BasePostgresEntity` provides `getSchema()` for Zod schema generation from the Drizzle table using `drizzle-zod`:
|
|
1645
1627
|
|
|
1646
1628
|
```typescript
|
|
1647
1629
|
const entity = new User();
|
|
@@ -1651,26 +1633,25 @@ entity.getSchema({ type: 'create' }); // Zod schema for INSERT data -- required
|
|
|
1651
1633
|
entity.getSchema({ type: 'update' }); // Zod schema for UPDATE data -- all fields optional (partial)
|
|
1652
1634
|
```
|
|
1653
1635
|
|
|
1654
|
-
These schemas are used by `ControllerFactory` to auto-generate OpenAPI documentation. The schema factory is lazily initialized and shared across all
|
|
1636
|
+
These schemas are used by `ControllerFactory` to auto-generate OpenAPI documentation. The schema factory is lazily initialized and shared across all BasePostgresEntity instances for performance.
|
|
1655
1637
|
|
|
1656
1638
|
### Static `relations` Definition
|
|
1657
1639
|
|
|
1658
1640
|
Relations are defined as a static property or function returning an array of `TRelationConfig`:
|
|
1659
1641
|
|
|
1660
1642
|
```typescript
|
|
1661
|
-
static override relations = () => [
|
|
1643
|
+
static override relations = (): TRelationConfig[] => [
|
|
1662
1644
|
{
|
|
1663
1645
|
name: 'posts',
|
|
1664
|
-
type: 'many', // RelationTypes.MANY
|
|
1665
|
-
schema: Post.schema,
|
|
1646
|
+
type: 'many', // RelationTypes.MANY -- no fields/references; relationName points
|
|
1647
|
+
schema: Post.schema, // at the 'one' relation declared on the other side (Post)
|
|
1666
1648
|
metadata: {
|
|
1667
|
-
|
|
1668
|
-
references: [User.schema.id],
|
|
1649
|
+
relationName: 'author',
|
|
1669
1650
|
},
|
|
1670
1651
|
},
|
|
1671
1652
|
{
|
|
1672
1653
|
name: 'profile',
|
|
1673
|
-
type: 'one', // RelationTypes.ONE
|
|
1654
|
+
type: 'one', // RelationTypes.ONE -- the FK-owning side declares fields/references
|
|
1674
1655
|
schema: Profile.schema,
|
|
1675
1656
|
metadata: {
|
|
1676
1657
|
fields: [Profile.schema.userId],
|
|
@@ -1682,134 +1663,30 @@ static override relations = () => [
|
|
|
1682
1663
|
|
|
1683
1664
|
These relations are used by the `FilterBuilder` when processing `include` in filters, and by the DataSource's `discoverSchema()` to build Drizzle relation definitions.
|
|
1684
1665
|
|
|
1685
|
-
### Enrichers
|
|
1666
|
+
### Enrichers
|
|
1686
1667
|
|
|
1687
|
-
Column definition helpers that
|
|
1688
|
-
|
|
1689
|
-
#### `generateIdColumnDefs()` -- ID Column
|
|
1690
|
-
|
|
1691
|
-
```typescript
|
|
1692
|
-
// Auto-incrementing integer ID (default)
|
|
1693
|
-
...generateIdColumnDefs()
|
|
1694
|
-
// Column: id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY
|
|
1668
|
+
Column definition helpers that spread common column patterns into a `pgTable` schema:
|
|
1695
1669
|
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
...generateIdColumnDefs({
|
|
1704
|
-
id: { dataType: 'string', generator: () => mySnowflakeGenerator() },
|
|
1705
|
-
})
|
|
1706
|
-
|
|
1707
|
-
// BigInt ID (as JavaScript number)
|
|
1708
|
-
...generateIdColumnDefs({ id: { dataType: 'big-number', numberMode: 'number' } })
|
|
1709
|
-
|
|
1710
|
-
// BigInt ID (as JavaScript bigint)
|
|
1711
|
-
...generateIdColumnDefs({ id: { dataType: 'big-number', numberMode: 'bigint' } })
|
|
1712
|
-
|
|
1713
|
-
// With custom sequence options
|
|
1714
|
-
...generateIdColumnDefs({
|
|
1715
|
-
id: { dataType: 'number', sequenceOptions: { startWith: 1000, increment: 1 } },
|
|
1716
|
-
})
|
|
1717
|
-
```
|
|
1718
|
-
|
|
1719
|
-
#### `generateTzColumnDefs()` -- Timestamps
|
|
1720
|
-
|
|
1721
|
-
```typescript
|
|
1722
|
-
// Default: createdAt + modifiedAt (with timezone, defaultNow)
|
|
1723
|
-
...generateTzColumnDefs()
|
|
1724
|
-
// Columns: created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
|
|
1725
|
-
// modified_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL (auto-updates via $onUpdate)
|
|
1726
|
-
|
|
1727
|
-
// With deletedAt for soft delete
|
|
1728
|
-
...generateTzColumnDefs({
|
|
1729
|
-
deleted: { enable: true, columnName: 'deleted_at', withTimezone: true },
|
|
1730
|
-
})
|
|
1731
|
-
// Adds: deleted_at TIMESTAMP WITH TIME ZONE (nullable)
|
|
1732
|
-
|
|
1733
|
-
// Disable modifiedAt
|
|
1734
|
-
...generateTzColumnDefs({
|
|
1735
|
-
modified: { enable: false },
|
|
1736
|
-
})
|
|
1737
|
-
// Only creates: created_at
|
|
1738
|
-
|
|
1739
|
-
// Custom column names
|
|
1740
|
-
...generateTzColumnDefs({
|
|
1741
|
-
created: { columnName: 'date_created', withTimezone: false },
|
|
1742
|
-
modified: { enable: true, columnName: 'date_modified', withTimezone: false },
|
|
1743
|
-
})
|
|
1744
|
-
```
|
|
1745
|
-
|
|
1746
|
-
#### `generateUserAuditColumnDefs()` -- Audit Trail
|
|
1747
|
-
|
|
1748
|
-
Automatically populates `createdBy` and `modifiedBy` from the authenticated user in the request context:
|
|
1749
|
-
|
|
1750
|
-
```typescript
|
|
1751
|
-
// Default: integer columns
|
|
1752
|
-
...generateUserAuditColumnDefs()
|
|
1753
|
-
// Columns: created_by integer, modified_by integer
|
|
1754
|
-
// Auto-populated from context.get('audit.user.id') on create/update
|
|
1755
|
-
|
|
1756
|
-
// String IDs
|
|
1757
|
-
...generateUserAuditColumnDefs({
|
|
1758
|
-
created: { dataType: 'string', columnName: 'created_by', allowAnonymous: true },
|
|
1759
|
-
modified: { dataType: 'string', columnName: 'modified_by', allowAnonymous: true },
|
|
1760
|
-
})
|
|
1761
|
-
```
|
|
1762
|
-
|
|
1763
|
-
When `allowAnonymous: false` (default is `true`), an error is thrown if there is no authenticated user in the request context. This is useful for columns that must always have an audit trail.
|
|
1764
|
-
|
|
1765
|
-
**Important:** `createdBy` is only set on insert (`$default`). `modifiedBy` is set on both insert and update (`$default` + `$onUpdate`).
|
|
1766
|
-
|
|
1767
|
-
#### `generatePrincipalColumnDefs()` -- Polymorphic Relations
|
|
1768
|
-
|
|
1769
|
-
```typescript
|
|
1770
|
-
// Default discriminator name ('principal')
|
|
1771
|
-
...generatePrincipalColumnDefs({ polymorphicIdType: 'number' })
|
|
1772
|
-
// Columns: principal_id integer NOT NULL, principal_type text
|
|
1773
|
-
|
|
1774
|
-
// Custom discriminator
|
|
1775
|
-
...generatePrincipalColumnDefs({
|
|
1776
|
-
discriminator: 'owner',
|
|
1777
|
-
polymorphicIdType: 'string',
|
|
1778
|
-
defaultPolymorphic: 'user',
|
|
1779
|
-
})
|
|
1780
|
-
// Columns: owner_id text NOT NULL, owner_type text DEFAULT 'user'
|
|
1781
|
-
```
|
|
1782
|
-
|
|
1783
|
-
#### `generateDataTypeColumnDefs()` -- Multi-type Value Columns
|
|
1784
|
-
|
|
1785
|
-
For storing heterogeneous values (useful for key-value stores, settings tables):
|
|
1786
|
-
|
|
1787
|
-
```typescript
|
|
1788
|
-
...generateDataTypeColumnDefs()
|
|
1789
|
-
// Columns:
|
|
1790
|
-
// data_type text -- type discriminator
|
|
1791
|
-
// n_value double precision -- numeric values
|
|
1792
|
-
// t_value text -- text values
|
|
1793
|
-
// b_value bytea -- binary values
|
|
1794
|
-
// j_value jsonb -- JSON values
|
|
1795
|
-
// bo_value boolean -- boolean values
|
|
1670
|
+
| Enricher | Adds |
|
|
1671
|
+
| --- | --- |
|
|
1672
|
+
| `generateIdColumnDefs()` | Primary key column -- auto-increment integer (default), UUID string, big-number, or custom string generator |
|
|
1673
|
+
| `generateTzColumnDefs()` | `created_at`/`modified_at` timestamps (with timezone, auto `$onUpdate`), optional `deleted_at` for soft delete |
|
|
1674
|
+
| `generateUserAuditColumnDefs()` | `created_by`/`modified_by`, auto-populated from `context.get('audit.user.id')` on create/update |
|
|
1675
|
+
| `generatePrincipalColumnDefs()` | Polymorphic discriminator pair (`principal_id`/`principal_type` by default) |
|
|
1676
|
+
| `generateDataTypeColumnDefs()` | Multi-type value columns (`data_type` + `n_value`/`t_value`/`b_value`/`j_value`/`bo_value`) for key-value/settings tables |
|
|
1796
1677
|
|
|
1797
|
-
|
|
1798
|
-
...generateDataTypeColumnDefs({
|
|
1799
|
-
defaultValue: { dataType: 'text', tValue: 'default' },
|
|
1800
|
-
})
|
|
1801
|
-
```
|
|
1678
|
+
Full options and generated-column tables: [Models reference](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/references/base/models.md).
|
|
1802
1679
|
|
|
1803
1680
|
---
|
|
1804
1681
|
|
|
1805
1682
|
## DataSources
|
|
1806
1683
|
|
|
1807
|
-
###
|
|
1684
|
+
### BasePostgresDataSource
|
|
1808
1685
|
|
|
1809
1686
|
DataSources manage database connections and provide Drizzle connectors:
|
|
1810
1687
|
|
|
1811
1688
|
```typescript
|
|
1812
|
-
abstract class
|
|
1689
|
+
abstract class BasePostgresDataSource<Settings, Schema> extends AbstractPostgresDataSource {
|
|
1813
1690
|
// Implemented by subclass
|
|
1814
1691
|
abstract configure(): ValueOrPromise<void>;
|
|
1815
1692
|
abstract getConnectionString(): ValueOrPromise<string>;
|
|
@@ -1820,16 +1697,21 @@ abstract class BaseDataSource<Settings, Schema> extends AbstractDataSource {
|
|
|
1820
1697
|
// Check if any repositories reference this datasource
|
|
1821
1698
|
hasDiscoverableModels(): boolean;
|
|
1822
1699
|
|
|
1823
|
-
// Transaction support
|
|
1824
|
-
|
|
1700
|
+
// Transaction support -- postgres's own IDatabaseTransactionOptions/IDatabaseTransaction<Schema>,
|
|
1701
|
+
// narrowing the neutral ITransactionOptions/ITransaction from AbstractDataSource
|
|
1702
|
+
beginTransaction(opts?: IDatabaseTransactionOptions): Promise<IDatabaseTransaction<Schema>>;
|
|
1825
1703
|
}
|
|
1826
1704
|
```
|
|
1827
1705
|
|
|
1706
|
+
#### Capability Probe
|
|
1707
|
+
|
|
1708
|
+
Every datasource -- regardless of connector -- exposes `getCapabilities(): { transactions: boolean }`. `AbstractDataSource` (the engine-neutral root every connector extends) defaults it to `{ transactions: false }`, paired with a `beginTransaction()` default that throws the standardized NotSupported error (`core.not_supported`, HTTP 501). `BasePostgresDataSource` (postgres) is the one connector that overrides both: `getCapabilities()` reports `{ transactions: true }`, and `beginTransaction()` is a real implementation (below) instead of the inherited throw. `TypesenseDataSource` adds no override -- it inherits the neutral NotSupported default from `AbstractDataSource` directly. Check `getCapabilities().transactions` before calling `beginTransaction()` on a datasource whose connector isn't known ahead of time.
|
|
1709
|
+
|
|
1828
1710
|
### Complete DataSource Configuration
|
|
1829
1711
|
|
|
1830
1712
|
```typescript
|
|
1831
1713
|
@datasource({ driver: 'node-postgres' })
|
|
1832
|
-
export class PostgresDataSource extends
|
|
1714
|
+
export class PostgresDataSource extends BasePostgresDataSource<IDSConfigs> {
|
|
1833
1715
|
constructor() {
|
|
1834
1716
|
super({
|
|
1835
1717
|
name: PostgresDataSource.name,
|
|
@@ -1879,12 +1761,12 @@ const tx = await dataSource.beginTransaction({
|
|
|
1879
1761
|
});
|
|
1880
1762
|
|
|
1881
1763
|
try {
|
|
1882
|
-
await
|
|
1764
|
+
await userRepository.create({
|
|
1883
1765
|
data: { username: 'john', email: 'john@example.com' },
|
|
1884
1766
|
options: { transaction: tx },
|
|
1885
1767
|
});
|
|
1886
1768
|
|
|
1887
|
-
await
|
|
1769
|
+
await auditRepository.create({
|
|
1888
1770
|
data: { action: 'user_created', userId: newUser.id },
|
|
1889
1771
|
options: { transaction: tx },
|
|
1890
1772
|
});
|
|
@@ -1896,16 +1778,7 @@ try {
|
|
|
1896
1778
|
}
|
|
1897
1779
|
```
|
|
1898
1780
|
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
1. `beginTransaction()` acquires a `PoolClient` from the `pg` Pool.
|
|
1902
|
-
2. Executes `BEGIN TRANSACTION ISOLATION LEVEL <level>` on that client.
|
|
1903
|
-
3. Creates a new Drizzle connector using that dedicated client.
|
|
1904
|
-
4. Returns an `ITransaction` object with `connector`, `commit()`, `rollback()`, and `isActive`.
|
|
1905
|
-
5. When `{ transaction: tx }` is passed to a repository method, `resolveConnector()` returns the transaction's connector instead of the default DataSource connector.
|
|
1906
|
-
6. `commit()` runs `COMMIT`, sets `isActive = false`, and releases the client back to the pool.
|
|
1907
|
-
7. `rollback()` runs `ROLLBACK`, sets `isActive = false`, and releases the client.
|
|
1908
|
-
8. Attempting to use a committed/rolled-back transaction throws an error.
|
|
1781
|
+
`beginTransaction()` acquires a dedicated `PoolClient` and returns an `IDatabaseTransaction<Schema>` (`connector`, `commit()`, `rollback()`, `isolationLevel`, `isActive`); passing `{ transaction: tx }` to a repository method routes it through that dedicated connector instead of the DataSource's shared pool, and `commit()`/`rollback()` always release the client back to the pool. Full internals: [Transactions guide](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/guides/core-concepts/persistent/transactions.md).
|
|
1909
1782
|
|
|
1910
1783
|
#### Isolation Levels
|
|
1911
1784
|
|
|
@@ -1921,6 +1794,191 @@ Connections are always released back to the pool in the `finally` block of both
|
|
|
1921
1794
|
|
|
1922
1795
|
---
|
|
1923
1796
|
|
|
1797
|
+
## Search
|
|
1798
|
+
|
|
1799
|
+
IGNIS has a second, engine-neutral branch for search-only entities backed by Typesense instead of Postgres. It mirrors the SQL branch -- same `@model`/`@repository` decorators, same `TFilter`/where-operator vocabulary, same hidden-field/default-filter/default-limit behavior -- but returns a slightly different envelope in a few places (see below).
|
|
1800
|
+
|
|
1801
|
+
### Search Entities
|
|
1802
|
+
|
|
1803
|
+
Search entities extend `BaseSearchEntity` and define a static `definition` via `defineSearchCollection` + the `field` DSL, instead of Drizzle's `pgTable`. Same as `BasePostgresEntity`, apply `@model` so `hiddenProperties`/`defaultFilter`/`defaultLimit` are registered:
|
|
1804
|
+
|
|
1805
|
+
```typescript
|
|
1806
|
+
import { model } from '@venizia/ignis';
|
|
1807
|
+
import { BaseSearchEntity, defineSearchCollection, field } from '@venizia/ignis/typesense';
|
|
1808
|
+
|
|
1809
|
+
@model({
|
|
1810
|
+
type: 'entity',
|
|
1811
|
+
settings: { hiddenProperties: ['internalNotes'], defaultLimit: 20 },
|
|
1812
|
+
})
|
|
1813
|
+
export class Product extends BaseSearchEntity {
|
|
1814
|
+
static override definition = defineSearchCollection({
|
|
1815
|
+
name: 'products',
|
|
1816
|
+
fields: [
|
|
1817
|
+
field.id(),
|
|
1818
|
+
field.string('name', { searchable: true, sortable: true }),
|
|
1819
|
+
field.number('price', { filterable: true, sortable: true }),
|
|
1820
|
+
field.string('internalNotes', { optional: true }),
|
|
1821
|
+
],
|
|
1822
|
+
defaultSort: 'price',
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1825
|
+
```
|
|
1826
|
+
|
|
1827
|
+
`field.id()` is optional -- `defineSearchCollection` prepends it automatically when missing. `defaultSort` must reference a `number` field; Typesense's `default_sorting_field` only accepts a scalar numeric field, never a string or array.
|
|
1828
|
+
|
|
1829
|
+
### Search DataSources
|
|
1830
|
+
|
|
1831
|
+
`TypesenseDataSource` is a `BaseSearchDataSource` subclass, imported from a dedicated sub-path so the root `@venizia/ignis` barrel never pulls in the `typesense` client:
|
|
1832
|
+
|
|
1833
|
+
```typescript
|
|
1834
|
+
import { DataSourceDrivers, datasource } from '@venizia/ignis';
|
|
1835
|
+
import { TypesenseDataSource } from '@venizia/ignis/typesense';
|
|
1836
|
+
|
|
1837
|
+
@datasource({ driver: DataSourceDrivers.TYPESENSE })
|
|
1838
|
+
export class ProductSearchDataSource extends TypesenseDataSource {
|
|
1839
|
+
constructor() {
|
|
1840
|
+
super({
|
|
1841
|
+
name: ProductSearchDataSource.name,
|
|
1842
|
+
config: {
|
|
1843
|
+
nodes: [{ host: process.env.TYPESENSE_HOST!, port: +(process.env.TYPESENSE_PORT ?? 8108) }],
|
|
1844
|
+
apiKey: process.env.TYPESENSE_API_KEY!,
|
|
1845
|
+
},
|
|
1846
|
+
});
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
```
|
|
1850
|
+
|
|
1851
|
+
`typesense` is an optional peer dependency -- install it yourself (`bun add typesense`) if you use the search branch.
|
|
1852
|
+
|
|
1853
|
+
Same as the postgres branch, collections are discovered from `@repository` bindings and auto-provisioned (via `ensureCollection()`, additive-only) when the DataSource's `configure()` runs during boot. Opt out with:
|
|
1854
|
+
|
|
1855
|
+
- `autoProvision: false` in the `super({ ... })` call -- skips provisioning, discovery still runs.
|
|
1856
|
+
- `@datasource({ autoDiscovery: false })` -- skips discovery entirely (`getSchema()` returns `{}`).
|
|
1857
|
+
|
|
1858
|
+
`getClient()` returns the raw typesense `Client`, an escape hatch symmetric to the SQL branch's `pg.Pool` access:
|
|
1859
|
+
|
|
1860
|
+
```typescript
|
|
1861
|
+
const client = productSearchDataSource.getClient();
|
|
1862
|
+
```
|
|
1863
|
+
|
|
1864
|
+
### Search Repositories
|
|
1865
|
+
|
|
1866
|
+
Same tiered hierarchy as the SQL branch, over Typesense instead of Postgres:
|
|
1867
|
+
|
|
1868
|
+
```
|
|
1869
|
+
ReadableSearchRepository (read operations only)
|
|
1870
|
+
|
|
|
1871
|
+
+-- PersistableSearchRepository (+ create, update)
|
|
1872
|
+
|
|
|
1873
|
+
+-- DefaultSearchRepository (+ delete)
|
|
1874
|
+
```
|
|
1875
|
+
|
|
1876
|
+
`TInferSearchDocument<typeof X.schema>` derives the document type straight from the collection definition -- same idea as postgres's `typeof User.schema` inference, no hand-written interface needed:
|
|
1877
|
+
|
|
1878
|
+
```typescript
|
|
1879
|
+
import { repository } from '@venizia/ignis';
|
|
1880
|
+
import { DefaultSearchRepository, TInferSearchDocument } from '@venizia/ignis/typesense';
|
|
1881
|
+
|
|
1882
|
+
type ProductDocument = TInferSearchDocument<typeof Product.schema>;
|
|
1883
|
+
// -> { id: string; name: string; price: number; internalNotes?: string }
|
|
1884
|
+
|
|
1885
|
+
@repository({ model: Product, dataSource: ProductSearchDataSource })
|
|
1886
|
+
export class ProductSearchRepository extends DefaultSearchRepository<ProductDocument> {
|
|
1887
|
+
// No constructor needed -- dataSource auto-injected from @repository metadata
|
|
1888
|
+
}
|
|
1889
|
+
```
|
|
1890
|
+
|
|
1891
|
+
`TInferSearchDocument` only resolves field-level types when the collection is captured through `defineSearchCollection`'s `const` type param -- true as long as you call it directly on a `static schema = defineSearchCollection({ ... })` assignment, as above. `id` is always inferred as a required `string`; fields marked `{ optional: true }` become optional properties, everything else is required.
|
|
1892
|
+
|
|
1893
|
+
`@repository`, `TFilter`, and the where operators (`eq`, `gt`, `inq`, `and`/`or`, ...) work the same as the SQL branch -- `TFilter`/`TWhere` is translated into Typesense search params by `TypesenseQueryDialect`. Operators that can't be expressed as a Typesense `filter_by` throw instead of silently degrading:
|
|
1894
|
+
|
|
1895
|
+
- `like` / `ilike` (and their negations)
|
|
1896
|
+
- JSON-path fields (e.g. `'metadata.score'`)
|
|
1897
|
+
- `include` (relations)
|
|
1898
|
+
|
|
1899
|
+
Reach for the repository's `search()` method when you need one of these -- it's a raw passthrough to the driver (no dialect translation, no default filter applied).
|
|
1900
|
+
|
|
1901
|
+
`hiddenProperties`, `defaultFilter`, and `defaultLimit` from `@model` settings are honored the same way they are for the SQL branch: hidden fields become Typesense `exclude_fields`, `defaultFilter` is AND-merged into every query, and an omitted `limit` falls back to `defaultLimit` then `DEFAULT_LIMIT` (10).
|
|
1902
|
+
|
|
1903
|
+
Note "the same way" covers hidden-fields/limit fallback, not the `where` merge policy: the search branch (like memory) always AND-merges `defaultFilter.where` with the user's `where`, while Postgres deep-merges with the user filter winning key-by-key (`merge({}, defaultWhere, userWhere)`, `filter.ts`) -- see the memory connector section below for the concrete difference.
|
|
1904
|
+
|
|
1905
|
+
### Envelope Differences vs SQL
|
|
1906
|
+
|
|
1907
|
+
`create()`/`createAll()` are overloaded by `shouldReturn` the same way on both branches, and every write verb now returns `{ count, data }` on both branches too -- but Typesense has no `RETURNING` equivalent, so a few return shapes and constraints still differ underneath that shared envelope:
|
|
1908
|
+
|
|
1909
|
+
| Aspect | SQL | Search |
|
|
1910
|
+
| --- | --- | --- |
|
|
1911
|
+
| `deleteById()` `data` | The deleted row, straight from `RETURNING` | Populated only when the model has a `defaultFilter` -- its guard read (a `findById()` done to check the row isn't excluded) is the only document Typesense ever hands back. No `defaultFilter` means no guard read, so `data` is `null` even with `shouldReturn: true` |
|
|
1912
|
+
| `deleteAll()` / `deleteBy()` `data` | The deleted rows via `RETURNING`, always in lockstep with `count` | A `find()` snapshot taken *before* the delete, capped at `defaultLimit`/`DEFAULT_LIMIT` (10) like any other read -- `count` (the engine's real deleted-row count) can exceed `data.length` when more rows matched than the snapshot's page size. Truncating (no effective `where`) reports `{ count: 0, data: [] }` -- Typesense's truncate reports no per-document count at all |
|
|
1913
|
+
| `updateAll()` / `updateBy()` `data` | The updated rows via `RETURNING`, always in lockstep with `count` | A `find()` snapshot taken *after* the update, subject to the same pagination cap as `deleteAll()` -- can likewise diverge from `count` |
|
|
1914
|
+
| `skip` | Any offset | Must be a multiple of `limit` -- Typesense paginates by page, not row offset |
|
|
1915
|
+
| Transactions / row-level locks | `beginTransaction()` / `{ transaction }`, `{ lock }` in options | Neither is supported -- both throw the standardized NotSupported error |
|
|
1916
|
+
|
|
1917
|
+
### Adding a Search Engine
|
|
1918
|
+
|
|
1919
|
+
A new engine is one folder under `src/connectors/<engine>/` -- `DataSource`, `Driver`, a paired `QueryDialect` (never mixed with another engine's driver), a compiler (neutral DSL -> engine schema), a repository tier re-parented onto the shared `AbstractRepository`, and an internal error classifier -- sub-path exported (`@venizia/ignis/<engine>`) with the client as an optional peer dependency, the same convention `typesense/` follows today. Full contract (fixed `ISearchDriver` verb set, escape-hatch extras, transaction posture): [Connectors reference](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/references/base/connectors.md).
|
|
1920
|
+
|
|
1921
|
+
---
|
|
1922
|
+
|
|
1923
|
+
## Memory Connector
|
|
1924
|
+
|
|
1925
|
+
`MemoryDataSource` + `MemoryRepository` (`src/connectors/memory/`) are a zero-dependency, Map-backed engine for prototyping and tests -- the same role LoopBack 4's memory connector plays. No client library, no network, no peer dependency. Import it from the root (`@venizia/ignis`) or from its explicit sub-path (`@venizia/ignis/memory`) - both doors resolve to the same modules. Only optional-peer connectors (typesense) are sub-path-only:
|
|
1926
|
+
|
|
1927
|
+
```typescript
|
|
1928
|
+
import {
|
|
1929
|
+
AbstractEntity,
|
|
1930
|
+
SchemaTypes,
|
|
1931
|
+
TSchemaType,
|
|
1932
|
+
model,
|
|
1933
|
+
repository,
|
|
1934
|
+
} from '@venizia/ignis';
|
|
1935
|
+
import { MemoryDataSource, MemoryRepository } from '@venizia/ignis/memory';
|
|
1936
|
+
import { z } from '@hono/zod-openapi';
|
|
1937
|
+
|
|
1938
|
+
const ProductSelectSchema = z.object({ id: z.string(), title: z.string(), price: z.number() });
|
|
1939
|
+
type ProductDocument = z.infer<typeof ProductSelectSchema>;
|
|
1940
|
+
|
|
1941
|
+
@model({ type: 'entity', settings: { defaultLimit: 20 } })
|
|
1942
|
+
class Product extends AbstractEntity {
|
|
1943
|
+
static readonly COLLECTION_NAME = 'products';
|
|
1944
|
+
|
|
1945
|
+
constructor() {
|
|
1946
|
+
super({ name: Product.COLLECTION_NAME });
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
getSchema(opts: { type: TSchemaType }) {
|
|
1950
|
+
switch (opts.type) {
|
|
1951
|
+
case SchemaTypes.SELECT:
|
|
1952
|
+
return ProductSelectSchema;
|
|
1953
|
+
default:
|
|
1954
|
+
return ProductSelectSchema;
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
class ProductDataSource extends MemoryDataSource {
|
|
1960
|
+
constructor() {
|
|
1961
|
+
super({ name: ProductDataSource.name });
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
@repository({ model: Product, dataSource: ProductDataSource })
|
|
1966
|
+
class ProductRepository extends MemoryRepository<ProductDocument> {
|
|
1967
|
+
// No constructor needed -- dataSource auto-injected from @repository metadata
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
const productRepository = new ProductRepository(new ProductDataSource());
|
|
1971
|
+
const products = await productRepository.find({ filter: { where: { price: { gt: 10 } }, order: ['price DESC'] } });
|
|
1972
|
+
```
|
|
1973
|
+
|
|
1974
|
+
A static `COLLECTION_NAME` on the model is this connector's discoverable-model convention -- the equivalent of postgres's static `schema` or the search branch's static `definition`. `MemoryRepository` is a single, untiered class implementing every `AbstractRepository` verb (no Readable/Persistable/DefaultCRUD split -- there's no connection/dialect plumbing to layer progressively), supporting equality, comparison (`gt`/`gte`/`lt`/`lte`/`between`), pattern (`like`/`ilike` via anchored `RegExp`), array (`inq`/`nin`), and logical (`and`/`or`) operators, plus `order`/`skip`/`limit`/`fields`. Operators with no faithful plain-JS meaning (`is`/`isn`, `regexp`/`iregexp`, the PostgreSQL-only array operators, `include`) throw instead of guessing.
|
|
1975
|
+
|
|
1976
|
+
`hiddenProperties`, `defaultFilter`, and `defaultLimit` from `@model` settings are honored the same way every connector honors them, with the same AND-merge default-filter policy as the search branch (narrows rather than overrides, unlike postgres's key-by-key deep merge). No transactions or row locks -- both reject with the standardized NotSupported error. Ids default to `crypto.randomUUID()` when a document is created without one.
|
|
1977
|
+
|
|
1978
|
+
Full operator matrix and internals: [Memory Connector guide](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/guides/core-concepts/persistent/memory-connector.md).
|
|
1979
|
+
|
|
1980
|
+
---
|
|
1981
|
+
|
|
1924
1982
|
## Services
|
|
1925
1983
|
|
|
1926
1984
|
Services encapsulate business logic and are registered in the DI container:
|
|
@@ -1930,21 +1988,21 @@ import { BaseService, inject } from '@venizia/ignis';
|
|
|
1930
1988
|
|
|
1931
1989
|
export class UserService extends BaseService {
|
|
1932
1990
|
constructor(
|
|
1933
|
-
@inject({ key: 'repositories.UserRepository' }) private
|
|
1934
|
-
@inject({ key: 'repositories.AuditRepository' }) private
|
|
1991
|
+
@inject({ key: 'repositories.UserRepository' }) private userRepository: UserRepository,
|
|
1992
|
+
@inject({ key: 'repositories.AuditRepository' }) private auditRepository: AuditRepository,
|
|
1935
1993
|
) {
|
|
1936
1994
|
super({ scope: UserService.name });
|
|
1937
1995
|
}
|
|
1938
1996
|
|
|
1939
1997
|
async createUser(data: CreateUserInput) {
|
|
1940
|
-
const tx = await this.
|
|
1998
|
+
const tx = await this.userRepository.dataSource.beginTransaction();
|
|
1941
1999
|
try {
|
|
1942
|
-
const { data: user } = await this.
|
|
2000
|
+
const { data: user } = await this.userRepository.create({
|
|
1943
2001
|
data,
|
|
1944
2002
|
options: { transaction: tx },
|
|
1945
2003
|
});
|
|
1946
2004
|
|
|
1947
|
-
await this.
|
|
2005
|
+
await this.auditRepository.create({
|
|
1948
2006
|
data: { action: 'user_created', userId: user.id },
|
|
1949
2007
|
options: { transaction: tx },
|
|
1950
2008
|
});
|
|
@@ -1959,7 +2017,7 @@ export class UserService extends BaseService {
|
|
|
1959
2017
|
|
|
1960
2018
|
async deactivateInactiveUsers() {
|
|
1961
2019
|
const cutoff = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000); // 90 days
|
|
1962
|
-
const { count } = await this.
|
|
2020
|
+
const { count } = await this.userRepository.updateBy({
|
|
1963
2021
|
data: { status: 'inactive' },
|
|
1964
2022
|
where: { lastLoginAt: { lt: cutoff }, status: 'active' },
|
|
1965
2023
|
});
|
|
@@ -1987,35 +2045,29 @@ Components are self-contained modules that register controllers, services, bindi
|
|
|
1987
2045
|
|
|
1988
2046
|
| Component | Import | Description |
|
|
1989
2047
|
| --- | --- | --- |
|
|
1990
|
-
| **HealthCheckComponent** | `@venizia/ignis` | Health check endpoints (`GET /health`,
|
|
2048
|
+
| **HealthCheckComponent** | `@venizia/ignis` | Health check endpoints (`GET /health`, `POST /health/ping`) |
|
|
1991
2049
|
| **SwaggerComponent** | `@venizia/ignis` | OpenAPI documentation with Swagger UI or Scalar UI |
|
|
1992
|
-
| **AuthenticateComponent** | `@venizia/ignis` |
|
|
2050
|
+
| **AuthenticateComponent** | `@venizia/ignis` | JWS/JWKS + Basic authentication strategies, token services, auth middleware |
|
|
1993
2051
|
| **AuthorizeComponent** | `@venizia/ignis` | Casbin-based RBAC authorization with enforcers |
|
|
1994
2052
|
| **RequestTrackerComponent** | `@venizia/ignis` | `x-request-id` header injection, request body parsing |
|
|
1995
|
-
| **StaticAssetComponent** | `@venizia/ignis` | File upload/download CRUD with MinIO or
|
|
2053
|
+
| **StaticAssetComponent** | `@venizia/ignis/static-asset` | File upload/download CRUD with MinIO, Disk, or Bun S3 storage |
|
|
1996
2054
|
| **MailComponent** | `@venizia/ignis/mail` | Email sending via Nodemailer/Mailgun with Direct/BullMQ/InternalQueue executors |
|
|
1997
|
-
| **SocketIOComponent** | `@venizia/ignis/socket-io` | Socket.IO server with Redis adapter for horizontal scaling |
|
|
1998
|
-
| **WebSocketComponent** | `@venizia/ignis` | Native WebSocket support (Bun runtime) |
|
|
2055
|
+
| **SocketIOComponent** | `@venizia/ignis/socket-io` | Socket.IO server with a mandatory Redis adapter for horizontal scaling |
|
|
2056
|
+
| **WebSocketComponent** | `@venizia/ignis/websocket` | Native WebSocket support (Bun runtime) |
|
|
2057
|
+
| **GrpcComponent** | `@venizia/ignis/grpc` | Auto-registered by `BaseApplication` (like `RestComponent`) -- mounts controllers whose `@controller` metadata has `transport: 'grpc'` via ConnectRPC; never registered manually with `this.component()` |
|
|
1999
2058
|
|
|
2000
2059
|
### Health Check Component
|
|
2001
2060
|
|
|
2002
2061
|
```typescript
|
|
2003
2062
|
import { HealthCheckComponent, HealthCheckBindingKeys, IHealthCheckOptions } from '@venizia/ignis';
|
|
2004
2063
|
|
|
2005
|
-
// Optional: customize path (default: /health)
|
|
2006
2064
|
this.bind<IHealthCheckOptions>({ key: HealthCheckBindingKeys.HEALTH_CHECK_OPTIONS }).toValue({
|
|
2007
|
-
restOptions: { path: '/health-check' },
|
|
2065
|
+
restOptions: { path: '/health-check' }, // optional, default: /health
|
|
2008
2066
|
});
|
|
2009
2067
|
this.component(HealthCheckComponent);
|
|
2010
2068
|
```
|
|
2011
2069
|
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
| Endpoint | Description |
|
|
2015
|
-
| --- | --- |
|
|
2016
|
-
| `GET /health` | Basic health check -- returns `{ status: 'ok', uptime, timestamp }` |
|
|
2017
|
-
| `GET /health/live` | Liveness probe -- returns 200 if server is running |
|
|
2018
|
-
| `GET /health/ready` | Readiness probe -- returns 200 if server is ready to accept traffic |
|
|
2070
|
+
`GET /health` (basic status) and `POST /health/ping` (ping/pong echo) -- no separate liveness/readiness probe pair. Details: [Health Check](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/extensions/components/health-check.md).
|
|
2019
2071
|
|
|
2020
2072
|
### Swagger / OpenAPI Component
|
|
2021
2073
|
|
|
@@ -2023,288 +2075,82 @@ this.component(HealthCheckComponent);
|
|
|
2023
2075
|
import { SwaggerComponent, SwaggerBindingKeys, ISwaggerOptions } from '@venizia/ignis';
|
|
2024
2076
|
|
|
2025
2077
|
this.bind<ISwaggerOptions>({ key: SwaggerBindingKeys.SWAGGER_OPTIONS }).toValue({
|
|
2026
|
-
restOptions: {
|
|
2027
|
-
base: { path: '/doc' },
|
|
2028
|
-
doc: { path: '/openapi.json' },
|
|
2029
|
-
ui: { path: '/explorer', type: 'scalar' }, // 'scalar' or 'swagger'
|
|
2030
|
-
},
|
|
2031
|
-
explorer: { openapi: '3.0.0' },
|
|
2078
|
+
restOptions: { base: { path: '/doc' }, doc: { path: '/openapi.json' }, ui: { path: '/explorer', type: 'scalar' } },
|
|
2032
2079
|
});
|
|
2033
2080
|
this.component(SwaggerComponent);
|
|
2034
2081
|
```
|
|
2035
2082
|
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
**Endpoints:**
|
|
2039
|
-
|
|
2040
|
-
| Endpoint | Description |
|
|
2041
|
-
| --- | --- |
|
|
2042
|
-
| `GET /doc/openapi.json` | Raw OpenAPI spec in JSON |
|
|
2043
|
-
| `GET /doc/explorer` | Interactive API explorer (Scalar or Swagger UI) |
|
|
2083
|
+
Auto-populates `info` from `getAppInfo()`, registers JWT/Basic security schemes, and serves Scalar or Swagger UI depending on `ui.type`. Details: [Swagger](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/extensions/components/swagger.md).
|
|
2044
2084
|
|
|
2045
2085
|
### Authentication Component
|
|
2046
2086
|
|
|
2047
|
-
|
|
2087
|
+
Basic auth plus Bearer tokens under two JOSE standards -- **JWS** (HMAC-signed, single shared secret) and **JWKS** (asymmetric, issuer/verifier split, `/certs` publishing) -- with optional AES-encrypted payloads:
|
|
2048
2088
|
|
|
2049
2089
|
```typescript
|
|
2050
|
-
import {
|
|
2051
|
-
AuthenticateComponent, AuthenticateBindingKeys,
|
|
2052
|
-
Authentication, AuthenticationStrategyRegistry,
|
|
2053
|
-
JWTAuthenticationStrategy, BasicAuthenticationStrategy,
|
|
2054
|
-
IJWTTokenServiceOptions, IBasicTokenServiceOptions,
|
|
2055
|
-
TAuthenticationRestOptions,
|
|
2056
|
-
} from '@venizia/ignis';
|
|
2090
|
+
import { AuthenticateComponent, AuthenticateBindingKeys, JOSEStandards, TJWTTokenServiceOptions } from '@venizia/ignis';
|
|
2057
2091
|
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
jwtSecret: process.env.JWT_SECRET!,
|
|
2061
|
-
applicationSecret: process.env.APP_SECRET!, // Secret for AES encrypting payload fields
|
|
2062
|
-
headerAlgorithm: 'HS256', // JWT signing algorithm (default: HS256)
|
|
2063
|
-
aesAlgorithm: 'aes-256-cbc', // Payload encryption algorithm (default: aes-256-cbc)
|
|
2064
|
-
getTokenExpiresFn: () => 86400, // Token expiration in seconds (24 hours)
|
|
2092
|
+
this.bind<TJWTTokenServiceOptions>({ key: AuthenticateBindingKeys.JWT_OPTIONS }).toValue({
|
|
2093
|
+
standard: JOSEStandards.JWS,
|
|
2094
|
+
options: { jwtSecret: process.env.JWT_SECRET!, headerAlgorithm: 'HS256', getTokenExpiresFn: () => 86400 },
|
|
2065
2095
|
});
|
|
2066
|
-
|
|
2067
|
-
// 2. Configure Basic auth options (optional)
|
|
2068
|
-
this.bind<IBasicTokenServiceOptions>({ key: AuthenticateBindingKeys.BASIC_OPTIONS }).toValue({
|
|
2069
|
-
verifyCredentials: async ({ credentials, context }) => {
|
|
2070
|
-
const user = await userRepo.findOne({
|
|
2071
|
-
filter: { where: { username: credentials.username } },
|
|
2072
|
-
});
|
|
2073
|
-
if (user && await verifyPassword(credentials.password, user.password)) {
|
|
2074
|
-
return { userId: user.id, roles: user.roles };
|
|
2075
|
-
}
|
|
2076
|
-
return null;
|
|
2077
|
-
},
|
|
2078
|
-
});
|
|
2079
|
-
|
|
2080
|
-
// 3. Optionally enable auth controller (sign-in, sign-up, change-password)
|
|
2081
|
-
this.bind<TAuthenticationRestOptions>({ key: AuthenticateBindingKeys.REST_OPTIONS }).toValue({
|
|
2082
|
-
useAuthController: true,
|
|
2083
|
-
controllerOpts: {
|
|
2084
|
-
restPath: '/auth',
|
|
2085
|
-
payload: {
|
|
2086
|
-
signIn: { request: { schema: SignInSchema }, response: { schema: TokenSchema } },
|
|
2087
|
-
signUp: { request: { schema: SignUpSchema }, response: { schema: UserSchema } },
|
|
2088
|
-
},
|
|
2089
|
-
},
|
|
2090
|
-
});
|
|
2091
|
-
|
|
2092
|
-
// 4. Register the component
|
|
2093
2096
|
this.component(AuthenticateComponent);
|
|
2094
|
-
|
|
2095
|
-
// 5. Register strategies
|
|
2096
|
-
AuthenticationStrategyRegistry.getInstance().register({
|
|
2097
|
-
container: this,
|
|
2098
|
-
strategies: [
|
|
2099
|
-
{ name: Authentication.STRATEGY_JWT, strategy: JWTAuthenticationStrategy },
|
|
2100
|
-
{ name: Authentication.STRATEGY_BASIC, strategy: BasicAuthenticationStrategy },
|
|
2101
|
-
],
|
|
2102
|
-
});
|
|
2103
|
-
```
|
|
2104
|
-
|
|
2105
|
-
#### JWT Token Service API
|
|
2106
|
-
|
|
2107
|
-
```typescript
|
|
2108
|
-
// Generate a token
|
|
2109
|
-
const token = await jwtTokenService.generate({
|
|
2110
|
-
payload: {
|
|
2111
|
-
userId: user.id,
|
|
2112
|
-
roles: [{ id: 1, identifier: 'admin', priority: 1 }],
|
|
2113
|
-
email: user.email,
|
|
2114
|
-
},
|
|
2115
|
-
});
|
|
2116
|
-
|
|
2117
|
-
// Verify a token
|
|
2118
|
-
const payload = await jwtTokenService.verify({
|
|
2119
|
-
type: 'Bearer',
|
|
2120
|
-
token: 'eyJhbGciOiJ...',
|
|
2121
|
-
});
|
|
2122
|
-
// payload = { userId: '...', roles: [...], email: '...' }
|
|
2123
|
-
```
|
|
2124
|
-
|
|
2125
|
-
JWT payloads are AES-encrypted: all non-standard JWT fields (userId, roles, email, etc.) are encrypted with the `applicationSecret` before signing. This prevents payload inspection without the application secret.
|
|
2126
|
-
|
|
2127
|
-
#### Getting Current User from Context
|
|
2128
|
-
|
|
2129
|
-
After authentication middleware runs, the current user is available via context variables:
|
|
2130
|
-
|
|
2131
|
-
```typescript
|
|
2132
|
-
@get({
|
|
2133
|
-
configs: {
|
|
2134
|
-
path: '/profile',
|
|
2135
|
-
authenticate: { strategies: ['jwt'], mode: 'any' },
|
|
2136
|
-
responses: jsonResponse({ schema: UserProfileSchema }),
|
|
2137
|
-
},
|
|
2138
|
-
})
|
|
2139
|
-
async getProfile(context: TRouteContext) {
|
|
2140
|
-
const user = context.get('auth.current.user');
|
|
2141
|
-
// user = { userId: '...', roles: [...], ... }
|
|
2142
|
-
|
|
2143
|
-
const auditId = context.get('audit.user.id');
|
|
2144
|
-
// auditId = user.userId (set automatically for UserAuditEnricher)
|
|
2145
|
-
|
|
2146
|
-
// ...
|
|
2147
|
-
}
|
|
2148
2097
|
```
|
|
2149
2098
|
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
| Mode | Behavior |
|
|
2153
|
-
| --- | --- |
|
|
2154
|
-
| `any` (default) | At least one strategy must succeed. Tries each strategy in order; first success wins. |
|
|
2155
|
-
| `all` | All specified strategies must succeed. All are tried; all must pass. |
|
|
2156
|
-
|
|
2157
|
-
#### Route-Level vs Controller-Level Authentication
|
|
2158
|
-
|
|
2159
|
-
```typescript
|
|
2160
|
-
// Controller-level (via ControllerFactory)
|
|
2161
|
-
const UserController = ControllerFactory.defineCrudController({
|
|
2162
|
-
authenticate: { strategies: ['jwt'] }, // Applied to ALL routes
|
|
2163
|
-
routes: {
|
|
2164
|
-
find: { authenticate: { skip: true } }, // Override: public
|
|
2165
|
-
},
|
|
2166
|
-
});
|
|
2167
|
-
|
|
2168
|
-
// Route-level (via decorators)
|
|
2169
|
-
@controller({ path: '/products' })
|
|
2170
|
-
class ProductController extends BaseRestController {
|
|
2171
|
-
@get({
|
|
2172
|
-
configs: {
|
|
2173
|
-
path: '/',
|
|
2174
|
-
// No authenticate -- public route
|
|
2175
|
-
responses: jsonResponse({ schema: z.array(ProductSchema) }),
|
|
2176
|
-
},
|
|
2177
|
-
})
|
|
2178
|
-
async list(c: TRouteContext) { /* ... */ }
|
|
2179
|
-
|
|
2180
|
-
@post({
|
|
2181
|
-
configs: {
|
|
2182
|
-
path: '/',
|
|
2183
|
-
authenticate: { strategies: ['jwt'] }, // Protected route
|
|
2184
|
-
responses: jsonResponse({ schema: ProductSchema }),
|
|
2185
|
-
},
|
|
2186
|
-
})
|
|
2187
|
-
async create(c: TRouteContext) { /* ... */ }
|
|
2188
|
-
}
|
|
2189
|
-
```
|
|
2099
|
+
Strategies (`jwt`/`basic`) register via `AuthenticationStrategyRegistry`; the current user is available via `context.get('auth.current.user')` after the middleware runs. Authentication mode is `any` (first success wins) or `all` (every strategy must pass). Full setup (JWKS issuer/verifier mode, token service API, route vs controller-level auth): [Authentication](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/extensions/components/authentication/index.md).
|
|
2190
2100
|
|
|
2191
2101
|
### Authorization Component
|
|
2192
2102
|
|
|
2193
|
-
Casbin-based RBAC
|
|
2103
|
+
Casbin-based RBAC. `IAuthorizeOptions` carries the component-wide decision policy; the enforcer is registered separately through `AuthorizationEnforcerRegistry`:
|
|
2194
2104
|
|
|
2195
2105
|
```typescript
|
|
2196
|
-
import {
|
|
2197
|
-
AuthorizeComponent, AuthorizeBindingKeys, IAuthorizeOptions,
|
|
2198
|
-
CasbinAuthorizationEnforcer,
|
|
2199
|
-
} from '@venizia/ignis';
|
|
2106
|
+
import { AuthorizeComponent, AuthorizeBindingKeys, AuthorizationDecisions } from '@venizia/ignis';
|
|
2200
2107
|
|
|
2201
|
-
this.bind
|
|
2202
|
-
|
|
2203
|
-
alwaysAllowRoles: ['superadmin'],
|
|
2204
|
-
casbinOptions: {
|
|
2205
|
-
model: '/path/to/model.conf', // Casbin model file
|
|
2206
|
-
adapter: myAdapter, // e.g., FileAdapter, PostgresAdapter
|
|
2207
|
-
},
|
|
2108
|
+
this.bind({ key: AuthorizeBindingKeys.OPTIONS }).toValue({
|
|
2109
|
+
defaultDecision: AuthorizationDecisions.DENY,
|
|
2110
|
+
alwaysAllowRoles: ['superadmin'],
|
|
2208
2111
|
});
|
|
2209
2112
|
this.component(AuthorizeComponent);
|
|
2210
2113
|
```
|
|
2211
2114
|
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
```typescript
|
|
2215
|
-
@get({
|
|
2216
|
-
configs: {
|
|
2217
|
-
path: '/admin/users',
|
|
2218
|
-
authenticate: { strategies: ['jwt'] },
|
|
2219
|
-
authorize: { action: 'read', resource: 'User' },
|
|
2220
|
-
responses: jsonResponse({ schema: z.array(UserSchema) }),
|
|
2221
|
-
},
|
|
2222
|
-
})
|
|
2223
|
-
async listUsers(context: TRouteContext) { /* ... */ }
|
|
2224
|
-
|
|
2225
|
-
// Multiple authorization specs (all must pass)
|
|
2226
|
-
@del({
|
|
2227
|
-
configs: {
|
|
2228
|
-
path: '/admin/users/{id}',
|
|
2229
|
-
authenticate: { strategies: ['jwt'] },
|
|
2230
|
-
authorize: [
|
|
2231
|
-
{ action: 'delete', resource: 'User' },
|
|
2232
|
-
{ action: 'manage', resource: 'Admin' },
|
|
2233
|
-
],
|
|
2234
|
-
// ...
|
|
2235
|
-
},
|
|
2236
|
-
})
|
|
2237
|
-
async deleteUser(context: TRouteContext) { /* ... */ }
|
|
2238
|
-
```
|
|
2115
|
+
Apply with `authorize: { action, resource }` (or an array, all must pass) in a route's `configs`. Full enforcer registration and Casbin model setup: [Authorization](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/extensions/components/authorization/index.md).
|
|
2239
2116
|
|
|
2240
2117
|
### Static Asset Component
|
|
2241
2118
|
|
|
2242
|
-
File upload/download with MinIO or
|
|
2119
|
+
File upload/download with MinIO, Disk, or Bun S3 storage. Sub-path only, like `mail`/`socket-io`/`websocket`:
|
|
2243
2120
|
|
|
2244
2121
|
```typescript
|
|
2245
|
-
import {
|
|
2246
|
-
|
|
2247
|
-
StaticAssetStorageTypes, TStaticAssetsComponentOptions,
|
|
2248
|
-
} from '@venizia/ignis';
|
|
2249
|
-
import { MinioHelper, DiskHelper } from '@venizia/ignis-helpers';
|
|
2250
|
-
|
|
2251
|
-
// MinIO backend
|
|
2252
|
-
this.bind<TStaticAssetsComponentOptions>({
|
|
2253
|
-
key: StaticAssetComponentBindingKeys.STATIC_ASSET_COMPONENT_OPTIONS,
|
|
2254
|
-
}).toValue({
|
|
2255
|
-
staticAsset: {
|
|
2256
|
-
controller: { name: 'AssetController', basePath: '/assets' },
|
|
2257
|
-
storage: StaticAssetStorageTypes.MINIO,
|
|
2258
|
-
helper: new MinioHelper({
|
|
2259
|
-
endPoint: 'localhost',
|
|
2260
|
-
port: 9000,
|
|
2261
|
-
accessKey: 'minioadmin',
|
|
2262
|
-
secretKey: 'minioadmin',
|
|
2263
|
-
useSSL: false,
|
|
2264
|
-
}),
|
|
2265
|
-
},
|
|
2266
|
-
});
|
|
2267
|
-
this.component(StaticAssetComponent);
|
|
2122
|
+
import { StaticAssetComponent, StaticAssetComponentBindingKeys, StaticAssetStorageTypes } from '@venizia/ignis/static-asset';
|
|
2123
|
+
import { DiskHelper } from '@venizia/ignis-helpers';
|
|
2268
2124
|
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
key: StaticAssetComponentBindingKeys.STATIC_ASSET_COMPONENT_OPTIONS,
|
|
2272
|
-
}).toValue({
|
|
2273
|
-
staticAsset: {
|
|
2274
|
-
controller: { name: 'AssetController', basePath: '/assets' },
|
|
2275
|
-
storage: StaticAssetStorageTypes.DISK,
|
|
2276
|
-
helper: new DiskHelper({ basePath: './uploads' }),
|
|
2277
|
-
},
|
|
2125
|
+
this.bind({ key: StaticAssetComponentBindingKeys.STATIC_ASSET_COMPONENT_OPTIONS }).toValue({
|
|
2126
|
+
staticAsset: { controller: { name: 'AssetController', basePath: '/assets' }, storage: StaticAssetStorageTypes.DISK, helper: new DiskHelper({ basePath: './uploads' }) },
|
|
2278
2127
|
});
|
|
2128
|
+
this.component(StaticAssetComponent);
|
|
2279
2129
|
```
|
|
2280
2130
|
|
|
2131
|
+
MinIO and Bun S3 backends follow the same shape, swapping `storage` and `helper`. Details: [Static Asset](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/extensions/components/static-asset/index.md).
|
|
2132
|
+
|
|
2281
2133
|
### Mail Component
|
|
2282
2134
|
|
|
2283
2135
|
```typescript
|
|
2284
2136
|
import { MailComponent } from '@venizia/ignis/mail';
|
|
2285
2137
|
```
|
|
2286
2138
|
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
- **Transporters**: Nodemailer (SMTP), Mailgun (API)
|
|
2290
|
-
- **Executors**: Direct (synchronous send), BullMQ (background queue with Redis), InternalQueue (in-memory queue)
|
|
2139
|
+
Transporters: Nodemailer (SMTP), Mailgun (API). Executors: Direct (synchronous), BullMQ (Redis-backed queue), InternalQueue (in-memory). Details: [Mail](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/extensions/components/mail/index.md).
|
|
2291
2140
|
|
|
2292
2141
|
### Socket.IO Component
|
|
2293
2142
|
|
|
2294
2143
|
```typescript
|
|
2295
2144
|
import { SocketIOComponent, SocketIOBindingKeys } from '@venizia/ignis/socket-io';
|
|
2296
2145
|
|
|
2297
|
-
this.bind({ key: SocketIOBindingKeys.
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
});
|
|
2146
|
+
this.bind({ key: SocketIOBindingKeys.SERVER_OPTIONS }).toValue({ cors: { origin: '*' } });
|
|
2147
|
+
// A Redis connection and an authenticate handler are both mandatory (unlike most other components).
|
|
2148
|
+
this.bind({ key: SocketIOBindingKeys.REDIS_CONNECTION }).toValue(redisHelper);
|
|
2149
|
+
this.bind({ key: SocketIOBindingKeys.AUTHENTICATE_HANDLER }).toValue(authenticateSocket);
|
|
2301
2150
|
this.component(SocketIOComponent);
|
|
2302
2151
|
```
|
|
2303
2152
|
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
- Bun and Node.js runtime handlers (auto-detected)
|
|
2307
|
-
- Redis adapter support for horizontal scaling across multiple server instances
|
|
2153
|
+
Bun and Node.js runtime handlers (auto-detected), Redis adapter for horizontal scaling. Details: [Socket.IO](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/extensions/components/socket-io/index.md).
|
|
2308
2154
|
|
|
2309
2155
|
---
|
|
2310
2156
|
|
|
@@ -2313,7 +2159,7 @@ Provides Socket.IO server integration with:
|
|
|
2313
2159
|
Access the Hono request context from anywhere using `useRequestContext()`:
|
|
2314
2160
|
|
|
2315
2161
|
```typescript
|
|
2316
|
-
import { useRequestContext } from '@venizia/ignis';
|
|
2162
|
+
import { useRequestContext, IAuthUser } from '@venizia/ignis';
|
|
2317
2163
|
|
|
2318
2164
|
function getCurrentRequestId(): string | undefined {
|
|
2319
2165
|
const context = useRequestContext();
|
|
@@ -2383,7 +2229,14 @@ setupMiddlewares(): ValueOrPromise<void> {
|
|
|
2383
2229
|
|
|
2384
2230
|
### Error Propagation
|
|
2385
2231
|
|
|
2386
|
-
Errors propagate through the layer stack and are caught by the global `appErrorHandler
|
|
2232
|
+
Errors propagate through the layer stack and are caught by the global `appErrorHandler` (Hono's `onError`), which routes each error to one of four shapes:
|
|
2233
|
+
|
|
2234
|
+
```
|
|
2235
|
+
ZodError -> 422, via formatZodError()
|
|
2236
|
+
DB client error (SQLSTATE class 22/23/44) -> 400, via isDatabaseClientError()
|
|
2237
|
+
Transient DB conflict (40001/40P01) -> 409, via isRetryableDatabaseError()
|
|
2238
|
+
Everything else (incl. getError()) -> its own statusCode, default 500
|
|
2239
|
+
```
|
|
2387
2240
|
|
|
2388
2241
|
```
|
|
2389
2242
|
Controller throws -> appErrorHandler catches -> JSON error response
|
|
@@ -2396,8 +2249,10 @@ Repository throws -> Service doesn't catch -> Controller doesn't catch -> appErr
|
|
|
2396
2249
|
```json
|
|
2397
2250
|
{
|
|
2398
2251
|
"message": "Error description",
|
|
2252
|
+
"messageCode": "some.message.code",
|
|
2399
2253
|
"statusCode": 500,
|
|
2400
2254
|
"requestId": "abc-123-def",
|
|
2255
|
+
"extra": { "any": "extra context attached via getError({ extra })" },
|
|
2401
2256
|
"details": {
|
|
2402
2257
|
"url": "http://localhost:3000/api/users",
|
|
2403
2258
|
"path": "/api/users",
|
|
@@ -2407,22 +2262,20 @@ Repository throws -> Service doesn't catch -> Controller doesn't catch -> appErr
|
|
|
2407
2262
|
}
|
|
2408
2263
|
```
|
|
2409
2264
|
|
|
2410
|
-
In production (`NODE_ENV=production`), `stack` and `cause` are stripped from responses.
|
|
2265
|
+
In production (`NODE_ENV=production`), `stack` and `cause` are stripped from responses, and an unexpected (non-`getError`) 500 has its `message` replaced with a generic `"Internal Server Error"` so raw driver/connection errors never leak.
|
|
2411
2266
|
|
|
2412
2267
|
### PostgreSQL Constraint Errors
|
|
2413
2268
|
|
|
2414
|
-
The error handler
|
|
2269
|
+
The error handler classifies by SQLSTATE **class** (the code's first two characters), not an exhaustive list of individual codes:
|
|
2415
2270
|
|
|
2416
|
-
|
|
|
2417
|
-
| --- | --- |
|
|
2418
|
-
| `
|
|
2419
|
-
| `
|
|
2420
|
-
| `
|
|
2421
|
-
| `
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
| `22003` | Numeric value out of range |
|
|
2425
|
-
| `22001` | String data too long |
|
|
2271
|
+
| SQLSTATE class | Meaning | Resolved status |
|
|
2272
|
+
| --- | --- | --- |
|
|
2273
|
+
| `22` | Data exception (e.g. `22001` string too long, `22003` numeric out of range, `22P02` invalid text representation) | 400 |
|
|
2274
|
+
| `23` | Integrity constraint violation (e.g. `23505` unique, `23503` foreign key, `23502` not null, `23514` check, `23P01` exclusion) | 400 |
|
|
2275
|
+
| `44` | WITH CHECK OPTION violation | 400 |
|
|
2276
|
+
| `40001` / `40P01` (exact codes, not a class) | Serialization failure / deadlock detected -- transient, safe to retry | 409, `messageCode: 'database.conflict'` |
|
|
2277
|
+
|
|
2278
|
+
In production, only the generic per-code message is returned; outside production, `detail`/`table`/`constraint` from the driver are appended for debugging.
|
|
2426
2279
|
|
|
2427
2280
|
### Throwing Application Errors
|
|
2428
2281
|
|
|
@@ -2442,16 +2295,18 @@ throw getError({
|
|
|
2442
2295
|
});
|
|
2443
2296
|
```
|
|
2444
2297
|
|
|
2298
|
+
`throwNotSupported()` (`src/utilities/error.utility.ts`) is the standardized way connectors reject a capability they deliberately don't implement (e.g. Typesense/Memory transactions and locks): it logs a warning, then throws via `getError()` with `messageCode: 'core.not_supported'` and HTTP 501.
|
|
2299
|
+
|
|
2445
2300
|
---
|
|
2446
2301
|
|
|
2447
2302
|
## Decorators Reference
|
|
2448
2303
|
|
|
2449
2304
|
| Decorator | Target | Parameters | Description |
|
|
2450
2305
|
| --- | --- | --- | --- |
|
|
2451
|
-
| `@model({ type?,
|
|
2452
|
-
| `@datasource({ driver
|
|
2306
|
+
| `@model({ type, settings?, tableName?, skipMigrate? })` | Class | `type`: `'entity' \| 'view'` (required); `settings.hiddenProperties`: string[]; `settings.defaultFilter`: TFilter | Register entity model with hidden properties and default filters |
|
|
2307
|
+
| `@datasource({ driver, autoDiscovery? })` | Class | `driver`: e.g. `'node-postgres'`, `'typesense'` (required); `autoDiscovery`: boolean (default true) | Register datasource with driver configuration |
|
|
2453
2308
|
| `@repository({ model, dataSource })` | Class | `model`: entity class; `dataSource`: datasource class | Bind repository to model and datasource; auto-injects datasource at param[0] |
|
|
2454
|
-
| `@controller({ path
|
|
2309
|
+
| `@controller({ path })` | Class | `path`: base path string | Register controller with base path. Default authentication/authorization is set at the `ControllerFactory` level, not on this decorator |
|
|
2455
2310
|
| `@get({ configs })` | Method | Full route config (path, request, responses, authenticate, authorize, middleware) | Define GET route |
|
|
2456
2311
|
| `@post({ configs })` | Method | Same as `@get` | Define POST route |
|
|
2457
2312
|
| `@put({ configs })` | Method | Same as `@get` | Define PUT route |
|
|
@@ -2469,6 +2324,7 @@ Utility functions for building OpenAPI-compliant response and request schemas:
|
|
|
2469
2324
|
|
|
2470
2325
|
```typescript
|
|
2471
2326
|
import { jsonContent, jsonResponse, htmlResponse, idParamsSchema } from '@venizia/ignis';
|
|
2327
|
+
import { z } from '@hono/zod-openapi';
|
|
2472
2328
|
|
|
2473
2329
|
// JSON request body
|
|
2474
2330
|
jsonContent({
|
|
@@ -2505,255 +2361,84 @@ idParamsSchema({ idType: 'string' });
|
|
|
2505
2361
|
|
|
2506
2362
|
### Complete User CRUD with Auth, Validation, Soft Delete, Pagination
|
|
2507
2363
|
|
|
2364
|
+
A model with hidden fields + soft delete, a repository, a service enforcing a uniqueness rule, and a controller wiring auth/authorization onto routes -- the full layered pattern in one file group:
|
|
2365
|
+
|
|
2508
2366
|
```typescript
|
|
2509
2367
|
// models/user.model.ts
|
|
2510
|
-
@model({
|
|
2511
|
-
|
|
2512
|
-
settings: {
|
|
2513
|
-
hiddenProperties: ['password'],
|
|
2514
|
-
defaultFilter: { where: { isDeleted: false } },
|
|
2515
|
-
},
|
|
2516
|
-
})
|
|
2517
|
-
export class User extends BaseEntity<typeof User.schema> {
|
|
2368
|
+
@model({ type: 'entity', settings: { hiddenProperties: ['password'], defaultFilter: { where: { isDeleted: false } } } })
|
|
2369
|
+
export class User extends BasePostgresEntity<typeof User.schema> {
|
|
2518
2370
|
static override schema = pgTable('User', {
|
|
2519
2371
|
...generateIdColumnDefs({ id: { dataType: 'string' } }),
|
|
2520
|
-
...generateTzColumnDefs({
|
|
2521
|
-
deleted: { enable: true, columnName: 'deleted_at', withTimezone: true },
|
|
2522
|
-
}),
|
|
2523
|
-
...generateUserAuditColumnDefs({
|
|
2524
|
-
created: { dataType: 'string', columnName: 'created_by', allowAnonymous: true },
|
|
2525
|
-
modified: { dataType: 'string', columnName: 'modified_by', allowAnonymous: true },
|
|
2526
|
-
}),
|
|
2372
|
+
...generateTzColumnDefs({ deleted: { enable: true, columnName: 'deleted_at', withTimezone: true } }),
|
|
2527
2373
|
username: text('username').notNull().unique(),
|
|
2528
2374
|
email: text('email').notNull().unique(),
|
|
2529
2375
|
password: text('password'),
|
|
2530
|
-
role: text('role').default('user').notNull(),
|
|
2531
2376
|
isDeleted: boolean('is_deleted').default(false).notNull(),
|
|
2532
|
-
metadata: jsonb('metadata').$type<Record<string, any>>(),
|
|
2533
2377
|
});
|
|
2534
|
-
|
|
2535
2378
|
static override relations = () => [];
|
|
2536
|
-
static TABLE_NAME = 'User';
|
|
2379
|
+
static override TABLE_NAME = 'User';
|
|
2537
2380
|
}
|
|
2538
2381
|
|
|
2539
2382
|
// repositories/user.repository.ts
|
|
2540
2383
|
@repository({ model: User, dataSource: PostgresDataSource })
|
|
2541
2384
|
export class UserRepository extends DefaultCRUDRepository<typeof User.schema> {}
|
|
2542
2385
|
|
|
2543
|
-
// services/user.service.ts
|
|
2386
|
+
// services/user.service.ts -- enforces a uniqueness rule before delegating to the repository
|
|
2544
2387
|
export class UserService extends BaseService {
|
|
2545
|
-
constructor(
|
|
2546
|
-
@inject({ key: 'repositories.UserRepository' }) private userRepo: UserRepository,
|
|
2547
|
-
) {
|
|
2388
|
+
constructor(@inject({ key: 'repositories.UserRepository' }) private userRepository: UserRepository) {
|
|
2548
2389
|
super({ scope: UserService.name });
|
|
2549
2390
|
}
|
|
2550
2391
|
|
|
2551
2392
|
async createUser(data: { username: string; email: string; password: string }) {
|
|
2552
|
-
const exists = await this.
|
|
2393
|
+
const exists = await this.userRepository.existsWith({ where: { email: data.email } });
|
|
2553
2394
|
if (exists) {
|
|
2554
|
-
throw getError({
|
|
2555
|
-
statusCode: HTTP.ResultCodes.RS_4.Conflict,
|
|
2556
|
-
message: 'Email already in use',
|
|
2557
|
-
});
|
|
2395
|
+
throw getError({ statusCode: HTTP.ResultCodes.RS_4.Conflict, message: 'Email already in use' });
|
|
2558
2396
|
}
|
|
2559
|
-
|
|
2560
|
-
const hashedPassword = await Bun.password.hash(data.password);
|
|
2561
|
-
return this.userRepo.create({
|
|
2562
|
-
data: { ...data, password: hashedPassword },
|
|
2563
|
-
});
|
|
2564
|
-
}
|
|
2565
|
-
|
|
2566
|
-
async softDeleteUser(id: string) {
|
|
2567
|
-
return this.userRepo.updateById({
|
|
2568
|
-
id,
|
|
2569
|
-
data: { isDeleted: true, deletedAt: new Date() },
|
|
2570
|
-
});
|
|
2397
|
+
return this.userRepository.create({ data: { ...data, password: await Bun.password.hash(data.password) } });
|
|
2571
2398
|
}
|
|
2572
2399
|
}
|
|
2573
2400
|
|
|
2574
|
-
// controllers/user.controller.ts
|
|
2401
|
+
// controllers/user.controller.ts -- @get is public, @post requires jwt + authorization
|
|
2575
2402
|
@controller({ path: '/users' })
|
|
2576
2403
|
export class UserController extends BaseRestController {
|
|
2577
|
-
constructor(
|
|
2578
|
-
@inject({ key: 'services.UserService' }) private userService: UserService,
|
|
2579
|
-
@inject({ key: 'repositories.UserRepository' }) private userRepo: UserRepository,
|
|
2580
|
-
) {
|
|
2404
|
+
constructor(@inject({ key: 'services.UserService' }) private userService: UserService) {
|
|
2581
2405
|
super({ scope: UserController.name });
|
|
2582
2406
|
}
|
|
2583
|
-
|
|
2584
2407
|
override binding() {}
|
|
2585
|
-
|
|
2586
|
-
@get({
|
|
2587
|
-
configs: {
|
|
2588
|
-
path: '/',
|
|
2589
|
-
description: 'List users with pagination',
|
|
2590
|
-
request: {
|
|
2591
|
-
query: z.object({
|
|
2592
|
-
filter: FilterSchema,
|
|
2593
|
-
}),
|
|
2594
|
-
},
|
|
2595
|
-
responses: jsonResponse({
|
|
2596
|
-
schema: z.object({
|
|
2597
|
-
data: z.array(z.object({
|
|
2598
|
-
id: z.string(),
|
|
2599
|
-
username: z.string(),
|
|
2600
|
-
email: z.string(),
|
|
2601
|
-
role: z.string(),
|
|
2602
|
-
})),
|
|
2603
|
-
range: z.object({ start: z.number(), end: z.number(), total: z.number() }),
|
|
2604
|
-
}),
|
|
2605
|
-
}),
|
|
2606
|
-
},
|
|
2607
|
-
})
|
|
2608
|
-
async list(context: TRouteContext) {
|
|
2609
|
-
const { filter = {} } = context.req.valid<{ filter?: any }>('query');
|
|
2610
|
-
const result = await this.userRepo.find({
|
|
2611
|
-
filter: { ...filter, fields: ['id', 'username', 'email', 'role'] },
|
|
2612
|
-
options: { shouldQueryRange: true },
|
|
2613
|
-
});
|
|
2614
|
-
return context.json(result, 200);
|
|
2615
|
-
}
|
|
2616
|
-
|
|
2617
|
-
@post({
|
|
2618
|
-
configs: {
|
|
2619
|
-
path: '/',
|
|
2620
|
-
authenticate: { strategies: ['jwt'] },
|
|
2621
|
-
authorize: { action: 'create', resource: 'User' },
|
|
2622
|
-
request: {
|
|
2623
|
-
body: jsonContent({
|
|
2624
|
-
schema: z.object({
|
|
2625
|
-
username: z.string().min(3).max(50),
|
|
2626
|
-
email: z.string().email(),
|
|
2627
|
-
password: z.string().min(8),
|
|
2628
|
-
}),
|
|
2629
|
-
}),
|
|
2630
|
-
},
|
|
2631
|
-
responses: jsonResponse({
|
|
2632
|
-
schema: z.object({ count: z.number(), data: z.any() }),
|
|
2633
|
-
}),
|
|
2634
|
-
},
|
|
2635
|
-
})
|
|
2636
|
-
async create(context: TRouteContext) {
|
|
2637
|
-
const data = context.req.valid<{ username: string; email: string; password: string }>('json');
|
|
2638
|
-
const result = await this.userService.createUser(data);
|
|
2639
|
-
return context.json(result, 200);
|
|
2640
|
-
}
|
|
2641
|
-
|
|
2642
|
-
@del({
|
|
2643
|
-
configs: {
|
|
2644
|
-
path: '/{id}',
|
|
2645
|
-
authenticate: { strategies: ['jwt'] },
|
|
2646
|
-
authorize: { action: 'delete', resource: 'User' },
|
|
2647
|
-
request: { params: idParamsSchema({ idType: 'string' }) },
|
|
2648
|
-
responses: jsonResponse({ schema: z.object({ count: z.number() }) }),
|
|
2649
|
-
},
|
|
2650
|
-
})
|
|
2651
|
-
async softDelete(context: TRouteContext) {
|
|
2652
|
-
const { id } = context.req.valid<{ id: string }>('param');
|
|
2653
|
-
const result = await this.userService.softDeleteUser(id);
|
|
2654
|
-
return context.json({ count: result.count }, 200);
|
|
2655
|
-
}
|
|
2656
2408
|
}
|
|
2657
2409
|
```
|
|
2658
2410
|
|
|
2411
|
+
The full walkthrough (pagination, controller-level find/create/soft-delete routes, OpenAPI schemas) is the [Building a CRUD API tutorial](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/guides/tutorials/building-a-crud-api.md).
|
|
2412
|
+
|
|
2659
2413
|
---
|
|
2660
2414
|
|
|
2661
2415
|
## Testing
|
|
2662
2416
|
|
|
2663
|
-
|
|
2417
|
+
IGNIS uses the Bun test runner throughout. A repository test needs only a configured DataSource:
|
|
2664
2418
|
|
|
2665
2419
|
```typescript
|
|
2666
|
-
import { describe, test, expect } from 'bun:test';
|
|
2420
|
+
import { describe, test, expect, beforeAll } from 'bun:test';
|
|
2667
2421
|
|
|
2668
2422
|
describe('UserRepository', () => {
|
|
2669
|
-
let
|
|
2670
|
-
let dataSource: PostgresDataSource;
|
|
2423
|
+
let repository: UserRepository;
|
|
2671
2424
|
|
|
2672
2425
|
beforeAll(async () => {
|
|
2673
|
-
dataSource = new PostgresDataSource();
|
|
2426
|
+
const dataSource = new PostgresDataSource();
|
|
2674
2427
|
await dataSource.configure();
|
|
2675
|
-
|
|
2428
|
+
repository = new UserRepository(dataSource, { entityClass: User });
|
|
2676
2429
|
});
|
|
2677
2430
|
|
|
2678
2431
|
test('create and find user', async () => {
|
|
2679
|
-
const { data: user } = await
|
|
2680
|
-
data: { username: 'test', email: 'test@example.com' },
|
|
2681
|
-
});
|
|
2682
|
-
|
|
2432
|
+
const { data: user } = await repository.create({ data: { username: 'test', email: 'test@example.com' } });
|
|
2683
2433
|
expect(user.id).toBeDefined();
|
|
2684
|
-
expect(user.username).toBe('test');
|
|
2685
2434
|
|
|
2686
|
-
const found = await
|
|
2687
|
-
expect(found).not.toBeNull();
|
|
2435
|
+
const found = await repository.findById({ id: user.id });
|
|
2688
2436
|
expect(found!.email).toBe('test@example.com');
|
|
2689
2437
|
});
|
|
2690
|
-
|
|
2691
|
-
test('hidden fields are excluded', async () => {
|
|
2692
|
-
const { data: user } = await repo.create({
|
|
2693
|
-
data: { username: 'secret', email: 'secret@example.com', password: 'hash123' },
|
|
2694
|
-
});
|
|
2695
|
-
|
|
2696
|
-
// password should not be in the returned data
|
|
2697
|
-
expect(user.password).toBeUndefined();
|
|
2698
|
-
});
|
|
2699
|
-
|
|
2700
|
-
test('default filter excludes soft-deleted records', async () => {
|
|
2701
|
-
const { data: user } = await repo.create({
|
|
2702
|
-
data: { username: 'deleted', email: 'deleted@example.com', isDeleted: true },
|
|
2703
|
-
});
|
|
2704
|
-
|
|
2705
|
-
// Default filter: { where: { isDeleted: false } }
|
|
2706
|
-
const found = await repo.findById({ id: user.id });
|
|
2707
|
-
expect(found).toBeNull();
|
|
2708
|
-
|
|
2709
|
-
// Bypass default filter
|
|
2710
|
-
const foundAll = await repo.findById({
|
|
2711
|
-
id: user.id,
|
|
2712
|
-
options: { shouldSkipDefaultFilter: true },
|
|
2713
|
-
});
|
|
2714
|
-
expect(foundAll).not.toBeNull();
|
|
2715
|
-
});
|
|
2716
2438
|
});
|
|
2717
2439
|
```
|
|
2718
2440
|
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
```typescript
|
|
2722
|
-
import { describe, test, expect } from 'bun:test';
|
|
2723
|
-
|
|
2724
|
-
describe('UserController', () => {
|
|
2725
|
-
let app: Application;
|
|
2726
|
-
|
|
2727
|
-
beforeAll(async () => {
|
|
2728
|
-
app = new Application();
|
|
2729
|
-
await app.initialize();
|
|
2730
|
-
});
|
|
2731
|
-
|
|
2732
|
-
test('GET /api/users returns 200', async () => {
|
|
2733
|
-
const server = app.getServer();
|
|
2734
|
-
const res = await server.fetch(
|
|
2735
|
-
new Request('http://localhost/api/users'),
|
|
2736
|
-
);
|
|
2737
|
-
expect(res.status).toBe(200);
|
|
2738
|
-
const body = await res.json();
|
|
2739
|
-
expect(Array.isArray(body)).toBe(true);
|
|
2740
|
-
});
|
|
2741
|
-
|
|
2742
|
-
test('POST /api/users validates request body', async () => {
|
|
2743
|
-
const server = app.getServer();
|
|
2744
|
-
const res = await server.fetch(
|
|
2745
|
-
new Request('http://localhost/api/users', {
|
|
2746
|
-
method: 'POST',
|
|
2747
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2748
|
-
body: JSON.stringify({ username: '' }), // Invalid: missing email, empty username
|
|
2749
|
-
}),
|
|
2750
|
-
);
|
|
2751
|
-
expect(res.status).toBe(422);
|
|
2752
|
-
const body = await res.json();
|
|
2753
|
-
expect(body.message).toBe('ValidationError');
|
|
2754
|
-
});
|
|
2755
|
-
});
|
|
2756
|
-
```
|
|
2441
|
+
Controller tests drive `app.getServer().fetch()` against a real running `Application` -- routes are only mounted inside `start()`. Full patterns (hidden-field/default-filter assertions, request validation, auth): [Testing tutorial](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/guides/tutorials/testing.md).
|
|
2757
2442
|
|
|
2758
2443
|
---
|
|
2759
2444
|
|
|
@@ -2767,11 +2452,11 @@ describe('UserController', () => {
|
|
|
2767
2452
|
|
|
2768
2453
|
4. **Core API vs Query API** -- The repository automatically uses the faster Core API (15--20% faster) when your filter does not include relations or explicit field selection. No manual optimization needed.
|
|
2769
2454
|
|
|
2770
|
-
5. **Visible Property Caching** --
|
|
2455
|
+
5. **Visible Property Caching** -- `PostgresBaseRepository.getVisibleProperties()` computes the visible property set once and caches it. Subsequent queries reuse the cached column selection.
|
|
2771
2456
|
|
|
2772
2457
|
6. **Parallel Count + Data** -- When `shouldQueryRange: true`, the data fetch and count query run in parallel via `Promise.all`, not sequentially.
|
|
2773
2458
|
|
|
2774
|
-
7. **Schema Factory Sharing** -- `
|
|
2459
|
+
7. **Schema Factory Sharing** -- `BasePostgresEntity` uses a lazy singleton for the Drizzle-Zod schema factory, shared across all entity instances. Schema generation does not create redundant factory objects.
|
|
2775
2460
|
|
|
2776
2461
|
8. **Avoid `shouldReturn: true` for Bulk Inserts** -- When inserting large batches, pass `shouldReturn: false` to skip the `RETURNING` clause, which significantly reduces response payload size and memory usage.
|
|
2777
2462
|
|
|
@@ -2784,8 +2469,8 @@ describe('UserController', () => {
|
|
|
2784
2469
|
## Documentation
|
|
2785
2470
|
|
|
2786
2471
|
- [IGNIS Repository](https://github.com/VENIZIA-AI/ignis)
|
|
2787
|
-
- [Getting Started](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/
|
|
2788
|
-
- [Core Concepts](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/
|
|
2472
|
+
- [Getting Started](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/guides/index.md)
|
|
2473
|
+
- [Core Concepts](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/guides/core-concepts/application/index.md)
|
|
2789
2474
|
- [Examples](https://github.com/VENIZIA-AI/ignis/tree/main/examples/vert)
|
|
2790
2475
|
|
|
2791
2476
|
---
|