sovrium 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3497 -0
- package/LICENSE.md +147 -0
- package/LICENSE_EE.md +297 -0
- package/README.md +321 -0
- package/drizzle/0000_melted_kabuki.sql +163 -0
- package/drizzle/meta/0000_snapshot.json +1216 -0
- package/drizzle/meta/_journal.json +13 -0
- package/package.json +167 -0
- package/schemas/0.0.1/app.openapi.json +70 -0
- package/schemas/0.0.1/app.schema.json +7961 -0
- package/schemas/0.0.2/app.openapi.json +80 -0
- package/schemas/0.0.2/app.schema.json +8829 -0
- package/schemas/development/app.openapi.json +70 -0
- package/schemas/development/app.schema.json +7456 -0
- package/src/application/errors/app-validation-error.ts +14 -0
- package/src/application/errors/static-generation-error.ts +16 -0
- package/src/application/metadata/favicon-transformer.ts +127 -0
- package/src/application/models/server.ts +27 -0
- package/src/application/ports/models/user-metadata.ts +36 -0
- package/src/application/ports/models/user-session.ts +34 -0
- package/src/application/ports/repositories/activity-log-repository.ts +68 -0
- package/src/application/ports/repositories/activity-repository.ts +49 -0
- package/src/application/ports/repositories/analytics-repository.ts +164 -0
- package/src/application/ports/repositories/auth-repository.ts +33 -0
- package/src/application/ports/repositories/batch-repository.ts +86 -0
- package/src/application/ports/repositories/comment-repository.ts +150 -0
- package/src/application/ports/repositories/index.ts +41 -0
- package/src/application/ports/repositories/table-repository.ts +139 -0
- package/src/application/ports/services/css-compiler.ts +55 -0
- package/src/application/ports/services/index.ts +16 -0
- package/src/application/ports/services/page-renderer.ts +79 -0
- package/src/application/ports/services/server-factory.ts +80 -0
- package/src/application/ports/services/static-site-generator.ts +82 -0
- package/src/application/use-cases/activity/programs.ts +66 -0
- package/src/application/use-cases/analytics/collect-page-view.ts +114 -0
- package/src/application/use-cases/analytics/purge-old-data.ts +40 -0
- package/src/application/use-cases/analytics/query-campaigns.ts +43 -0
- package/src/application/use-cases/analytics/query-devices.ts +36 -0
- package/src/application/use-cases/analytics/query-overview.ts +50 -0
- package/src/application/use-cases/analytics/query-pages.ts +40 -0
- package/src/application/use-cases/analytics/query-referrers.ts +43 -0
- package/src/application/use-cases/analytics/ua-parser.ts +89 -0
- package/src/application/use-cases/analytics/visitor-hash.ts +77 -0
- package/src/application/use-cases/auth/bootstrap-admin.ts +270 -0
- package/src/application/use-cases/list-activity-logs.ts +123 -0
- package/src/application/use-cases/server/generate-static-helpers.ts +374 -0
- package/src/application/use-cases/server/generate-static.ts +287 -0
- package/src/application/use-cases/server/start-server.ts +118 -0
- package/src/application/use-cases/server/startup-error-handler.ts +69 -0
- package/src/application/use-cases/server/static-content-generators.ts +182 -0
- package/src/application/use-cases/server/static-language-generators.ts +181 -0
- package/src/application/use-cases/server/static-url-rewriter.ts +237 -0
- package/src/application/use-cases/server/translation-replacer.ts +164 -0
- package/src/application/use-cases/tables/activity-programs.ts +93 -0
- package/src/application/use-cases/tables/batch-operations.ts +156 -0
- package/src/application/use-cases/tables/comment-programs.ts +436 -0
- package/src/application/use-cases/tables/permissions/permissions.ts +25 -0
- package/src/application/use-cases/tables/programs.ts +435 -0
- package/src/application/use-cases/tables/table-operations.ts +412 -0
- package/src/application/use-cases/tables/user-role.ts +52 -0
- package/src/application/use-cases/tables/utils/display-formatter.ts +471 -0
- package/src/application/use-cases/tables/utils/field-read-filter.ts +189 -0
- package/src/application/use-cases/tables/utils/list-helpers.ts +122 -0
- package/src/application/use-cases/tables/utils/record-transformer.ts +319 -0
- package/src/cli.ts +370 -0
- package/src/domain/errors/create-tagged-error.ts +36 -0
- package/src/domain/errors/index.ts +78 -0
- package/src/domain/models/api/analytics.ts +179 -0
- package/src/domain/models/api/auth.ts +231 -0
- package/src/domain/models/api/common.ts +60 -0
- package/src/domain/models/api/error.ts +89 -0
- package/src/domain/models/api/health.ts +38 -0
- package/src/domain/models/api/index.ts +42 -0
- package/src/domain/models/api/request.ts +132 -0
- package/src/domain/models/api/tables.ts +444 -0
- package/src/domain/models/app/analytics/index.ts +129 -0
- package/src/domain/models/app/auth/config.ts +116 -0
- package/src/domain/models/app/auth/index.ts +230 -0
- package/src/domain/models/app/auth/methods/email-and-password.ts +67 -0
- package/src/domain/models/app/auth/methods/index.ts +11 -0
- package/src/domain/models/app/auth/methods/magic-link.ts +54 -0
- package/src/domain/models/app/auth/oauth/index.ts +8 -0
- package/src/domain/models/app/auth/oauth/providers.ts +105 -0
- package/src/domain/models/app/auth/plugins/admin.ts +130 -0
- package/src/domain/models/app/auth/plugins/index.ts +74 -0
- package/src/domain/models/app/auth/plugins/two-factor.ts +63 -0
- package/src/domain/models/app/auth/roles.ts +179 -0
- package/src/domain/models/app/auth/strategies.ts +191 -0
- package/src/domain/models/app/auth/validation.ts +127 -0
- package/src/domain/models/app/common/branded-ids.ts +200 -0
- package/src/domain/models/app/common/definitions.ts +187 -0
- package/src/domain/models/app/component/common/component-children.ts +119 -0
- package/src/domain/models/app/component/common/component-props.ts +89 -0
- package/src/domain/models/app/component/common/component-reference.ts +170 -0
- package/src/domain/models/app/component/component.ts +81 -0
- package/src/domain/models/app/components.ts +65 -0
- package/src/domain/models/app/description.ts +83 -0
- package/src/domain/models/app/index.ts +258 -0
- package/src/domain/models/app/language/language-config.ts +200 -0
- package/src/domain/models/app/languages.ts +205 -0
- package/src/domain/models/app/name.ts +66 -0
- package/src/domain/models/app/page/common/interactions/click-interaction.ts +116 -0
- package/src/domain/models/app/page/common/interactions/entrance-animation.ts +84 -0
- package/src/domain/models/app/page/common/interactions/hover-interaction.ts +144 -0
- package/src/domain/models/app/page/common/interactions/interactions.ts +64 -0
- package/src/domain/models/app/page/common/interactions/scroll-interaction.ts +93 -0
- package/src/domain/models/app/page/common/responsive.ts +114 -0
- package/src/domain/models/app/page/common/url.ts +35 -0
- package/src/domain/models/app/page/common/variable-reference.ts +53 -0
- package/src/domain/models/app/page/id.ts +44 -0
- package/src/domain/models/app/page/index.ts +270 -0
- package/src/domain/models/app/page/meta/analytics.ts +248 -0
- package/src/domain/models/app/page/meta/custom-elements.ts +180 -0
- package/src/domain/models/app/page/meta/dns-prefetch.ts +77 -0
- package/src/domain/models/app/page/meta/favicon-set.ts +203 -0
- package/src/domain/models/app/page/meta/favicon.ts +50 -0
- package/src/domain/models/app/page/meta/favicons-config.ts +73 -0
- package/src/domain/models/app/page/meta/index.ts +278 -0
- package/src/domain/models/app/page/meta/open-graph.ts +166 -0
- package/src/domain/models/app/page/meta/preload.ts +190 -0
- package/src/domain/models/app/page/meta/structured-data/article.ts +211 -0
- package/src/domain/models/app/page/meta/structured-data/breadcrumb.ts +115 -0
- package/src/domain/models/app/page/meta/structured-data/common-fields.ts +201 -0
- package/src/domain/models/app/page/meta/structured-data/education-event.ts +256 -0
- package/src/domain/models/app/page/meta/structured-data/faq-page.ts +127 -0
- package/src/domain/models/app/page/meta/structured-data/index.ts +95 -0
- package/src/domain/models/app/page/meta/structured-data/local-business.ts +247 -0
- package/src/domain/models/app/page/meta/structured-data/organization.ts +171 -0
- package/src/domain/models/app/page/meta/structured-data/person.ts +138 -0
- package/src/domain/models/app/page/meta/structured-data/postal-address.ts +106 -0
- package/src/domain/models/app/page/meta/structured-data/product.ts +214 -0
- package/src/domain/models/app/page/meta/twitter-card.ts +217 -0
- package/src/domain/models/app/page/name.ts +38 -0
- package/src/domain/models/app/page/path.ts +21 -0
- package/src/domain/models/app/page/scripts/external-scripts.ts +163 -0
- package/src/domain/models/app/page/scripts/features.ts +135 -0
- package/src/domain/models/app/page/scripts/inline-scripts.ts +114 -0
- package/src/domain/models/app/page/scripts/scripts.ts +102 -0
- package/src/domain/models/app/page/sections.ts +298 -0
- package/src/domain/models/app/pages.ts +61 -0
- package/src/domain/models/app/permissions/index.ts +61 -0
- package/src/domain/models/app/permissions/resource-action.ts +114 -0
- package/src/domain/models/app/permissions/roles.ts +120 -0
- package/src/domain/models/app/table/check-constraints.ts +105 -0
- package/src/domain/models/app/table/cycle-detection.ts +124 -0
- package/src/domain/models/app/table/database-identifier.ts +153 -0
- package/src/domain/models/app/table/field-name.ts +36 -0
- package/src/domain/models/app/table/field-types/advanced/array-field.ts +33 -0
- package/src/domain/models/app/table/field-types/advanced/autonumber-field.ts +54 -0
- package/src/domain/models/app/table/field-types/advanced/button-field.ts +56 -0
- package/src/domain/models/app/table/field-types/advanced/color-field.ts +57 -0
- package/src/domain/models/app/table/field-types/advanced/count-field.ts +54 -0
- package/src/domain/models/app/table/field-types/advanced/formula-field.ts +58 -0
- package/src/domain/models/app/table/field-types/advanced/geolocation-field.ts +49 -0
- package/src/domain/models/app/table/field-types/advanced/index.ts +16 -0
- package/src/domain/models/app/table/field-types/advanced/json-field.ts +25 -0
- package/src/domain/models/app/table/field-types/advanced/unknown-field.ts +85 -0
- package/src/domain/models/app/table/field-types/base-field.ts +42 -0
- package/src/domain/models/app/table/field-types/date-time/created-at-field.ts +49 -0
- package/src/domain/models/app/table/field-types/date-time/date-field.ts +95 -0
- package/src/domain/models/app/table/field-types/date-time/deleted-at-field.ts +56 -0
- package/src/domain/models/app/table/field-types/date-time/duration-field.ts +73 -0
- package/src/domain/models/app/table/field-types/date-time/index.ts +12 -0
- package/src/domain/models/app/table/field-types/date-time/updated-at-field.ts +50 -0
- package/src/domain/models/app/table/field-types/index.ts +19 -0
- package/src/domain/models/app/table/field-types/media/barcode-field.ts +58 -0
- package/src/domain/models/app/table/field-types/media/index.ts +10 -0
- package/src/domain/models/app/table/field-types/media/multiple-attachments-field.ts +80 -0
- package/src/domain/models/app/table/field-types/media/single-attachment-field.ts +81 -0
- package/src/domain/models/app/table/field-types/numeric/currency-field.ts +144 -0
- package/src/domain/models/app/table/field-types/numeric/decimal-field.ts +113 -0
- package/src/domain/models/app/table/field-types/numeric/index.ts +13 -0
- package/src/domain/models/app/table/field-types/numeric/integer-field.ts +98 -0
- package/src/domain/models/app/table/field-types/numeric/percentage-field.ts +115 -0
- package/src/domain/models/app/table/field-types/numeric/progress-field.ts +71 -0
- package/src/domain/models/app/table/field-types/numeric/rating-field.ts +74 -0
- package/src/domain/models/app/table/field-types/relational/index.ts +10 -0
- package/src/domain/models/app/table/field-types/relational/lookup-field.ts +46 -0
- package/src/domain/models/app/table/field-types/relational/relationship-field.ts +112 -0
- package/src/domain/models/app/table/field-types/relational/rollup-field.ts +58 -0
- package/src/domain/models/app/table/field-types/selection/checkbox-field.ts +51 -0
- package/src/domain/models/app/table/field-types/selection/index.ts +11 -0
- package/src/domain/models/app/table/field-types/selection/multi-select-field.ts +68 -0
- package/src/domain/models/app/table/field-types/selection/single-select-field.ts +54 -0
- package/src/domain/models/app/table/field-types/selection/status-field.ts +37 -0
- package/src/domain/models/app/table/field-types/text/email-field.ts +80 -0
- package/src/domain/models/app/table/field-types/text/index.ts +13 -0
- package/src/domain/models/app/table/field-types/text/long-text-field.ts +77 -0
- package/src/domain/models/app/table/field-types/text/phone-number-field.ts +82 -0
- package/src/domain/models/app/table/field-types/text/rich-text-field.ts +66 -0
- package/src/domain/models/app/table/field-types/text/single-line-text-field.ts +79 -0
- package/src/domain/models/app/table/field-types/text/url-field.ts +81 -0
- package/src/domain/models/app/table/field-types/user/created-by-field.ts +50 -0
- package/src/domain/models/app/table/field-types/user/deleted-by-field.ts +57 -0
- package/src/domain/models/app/table/field-types/user/index.ts +11 -0
- package/src/domain/models/app/table/field-types/user/updated-by-field.ts +51 -0
- package/src/domain/models/app/table/field-types/user/user-field.ts +52 -0
- package/src/domain/models/app/table/field-types/validation-utils.ts +166 -0
- package/src/domain/models/app/table/fields.ts +216 -0
- package/src/domain/models/app/table/foreign-keys.ts +111 -0
- package/src/domain/models/app/table/formula-keywords.ts +326 -0
- package/src/domain/models/app/table/id.ts +31 -0
- package/src/domain/models/app/table/index.ts +290 -0
- package/src/domain/models/app/table/indexes.ts +80 -0
- package/src/domain/models/app/table/name.ts +37 -0
- package/src/domain/models/app/table/permissions/field-permission.ts +83 -0
- package/src/domain/models/app/table/permissions/index.ts +167 -0
- package/src/domain/models/app/table/permissions/permission-evaluator.ts +372 -0
- package/src/domain/models/app/table/permissions/permission.ts +49 -0
- package/src/domain/models/app/table/primary-key.ts +62 -0
- package/src/domain/models/app/table/table-formula-validation.ts +168 -0
- package/src/domain/models/app/table/table-indexes-validation.ts +38 -0
- package/src/domain/models/app/table/table-permissions-validation.ts +77 -0
- package/src/domain/models/app/table/table-primary-key-validation.ts +49 -0
- package/src/domain/models/app/table/table-views-validation.ts +408 -0
- package/src/domain/models/app/table/unique-constraints.ts +79 -0
- package/src/domain/models/app/table/views/fields.ts +28 -0
- package/src/domain/models/app/table/views/filters.ts +162 -0
- package/src/domain/models/app/table/views/group-by.ts +32 -0
- package/src/domain/models/app/table/views/id.ts +50 -0
- package/src/domain/models/app/table/views/index.ts +177 -0
- package/src/domain/models/app/table/views/name.ts +32 -0
- package/src/domain/models/app/table/views/permissions.ts +98 -0
- package/src/domain/models/app/table/views/sorts.ts +31 -0
- package/src/domain/models/app/tables.ts +695 -0
- package/src/domain/models/app/theme/animations.ts +208 -0
- package/src/domain/models/app/theme/border-radius.ts +58 -0
- package/src/domain/models/app/theme/breakpoints.ts +62 -0
- package/src/domain/models/app/theme/colors.ts +110 -0
- package/src/domain/models/app/theme/fonts.ts +164 -0
- package/src/domain/models/app/theme/shadows.ts +61 -0
- package/src/domain/models/app/theme/spacing.ts +115 -0
- package/src/domain/models/app/theme.ts +66 -0
- package/src/domain/models/app/version.ts +87 -0
- package/src/domain/models/record-comment.ts +91 -0
- package/src/domain/utils/content-parsing.ts +49 -0
- package/src/domain/utils/format-detection.ts +69 -0
- package/src/domain/utils/index.ts +9 -0
- package/src/domain/utils/route-matcher.ts +184 -0
- package/src/domain/utils/translation-resolver.ts +170 -0
- package/src/index.ts +208 -0
- package/src/infrastructure/analytics/tracking-script.ts +48 -0
- package/src/infrastructure/auth/better-auth/auth.ts +216 -0
- package/src/infrastructure/auth/better-auth/email-handlers.ts +162 -0
- package/src/infrastructure/auth/better-auth/index.ts +16 -0
- package/src/infrastructure/auth/better-auth/layer.ts +97 -0
- package/src/infrastructure/auth/better-auth/plugins/admin.ts +56 -0
- package/src/infrastructure/auth/better-auth/plugins/magic-link.ts +31 -0
- package/src/infrastructure/auth/better-auth/plugins/two-factor.ts +19 -0
- package/src/infrastructure/auth/better-auth/schema.ts +152 -0
- package/src/infrastructure/auth/index.ts +27 -0
- package/src/infrastructure/css/cache/css-cache-service.ts +130 -0
- package/src/infrastructure/css/compiler.ts +210 -0
- package/src/infrastructure/css/css-compiler-live.ts +20 -0
- package/src/infrastructure/css/index.ts +25 -0
- package/src/infrastructure/css/styles/animation-styles-generator.ts +177 -0
- package/src/infrastructure/css/styles/click-animations.ts +147 -0
- package/src/infrastructure/css/styles/component-layer-generators.ts +147 -0
- package/src/infrastructure/css/theme/theme-generators.ts +130 -0
- package/src/infrastructure/css/theme/theme-layer-generators.ts +219 -0
- package/src/infrastructure/css/theme/theme-token-resolver.ts +76 -0
- package/src/infrastructure/database/activity-queries.ts +111 -0
- package/src/infrastructure/database/auth/auth-validation.ts +101 -0
- package/src/infrastructure/database/drizzle/db-bun.ts +17 -0
- package/src/infrastructure/database/drizzle/db.ts +17 -0
- package/src/infrastructure/database/drizzle/index.ts +16 -0
- package/src/infrastructure/database/drizzle/layer.ts +34 -0
- package/src/infrastructure/database/drizzle/migrate.ts +77 -0
- package/src/infrastructure/database/drizzle/schema/activity-log.ts +111 -0
- package/src/infrastructure/database/drizzle/schema/analytics-page-views.ts +116 -0
- package/src/infrastructure/database/drizzle/schema/migration-audit.ts +68 -0
- package/src/infrastructure/database/drizzle/schema/record-comments.ts +79 -0
- package/src/infrastructure/database/drizzle/schema.ts +12 -0
- package/src/infrastructure/database/field-utils.ts +87 -0
- package/src/infrastructure/database/filter-operators.ts +136 -0
- package/src/infrastructure/database/formula/formula-trigger-generators.ts +114 -0
- package/src/infrastructure/database/formula/formula-utils.ts +440 -0
- package/src/infrastructure/database/generators/index-generators.ts +152 -0
- package/src/infrastructure/database/generators/trigger-generators.ts +154 -0
- package/src/infrastructure/database/index.ts +35 -0
- package/src/infrastructure/database/lookup/lookup-expression-generators.ts +356 -0
- package/src/infrastructure/database/lookup/lookup-expressions.ts +116 -0
- package/src/infrastructure/database/lookup/lookup-view-generators.ts +403 -0
- package/src/infrastructure/database/lookup/lookup-view-helpers.ts +65 -0
- package/src/infrastructure/database/lookup/lookup-view-triggers.ts +121 -0
- package/src/infrastructure/database/migration-audit-trail.ts +375 -0
- package/src/infrastructure/database/repositories/activity-log-repository-live.ts +99 -0
- package/src/infrastructure/database/repositories/activity-repository-live.ts +21 -0
- package/src/infrastructure/database/repositories/analytics-repository-live.ts +316 -0
- package/src/infrastructure/database/repositories/auth-repository-live.ts +42 -0
- package/src/infrastructure/database/repositories/batch-repository-live.ts +29 -0
- package/src/infrastructure/database/repositories/comment-repository-live.ts +39 -0
- package/src/infrastructure/database/repositories/table-repository-live.ts +38 -0
- package/src/infrastructure/database/schema/schema-dependency-sorting.ts +142 -0
- package/src/infrastructure/database/schema/schema-initializer.ts +598 -0
- package/src/infrastructure/database/schema-migration/column-detection.ts +286 -0
- package/src/infrastructure/database/schema-migration/constants.ts +31 -0
- package/src/infrastructure/database/schema-migration/constraint-sync.ts +288 -0
- package/src/infrastructure/database/schema-migration/index-sync.ts +108 -0
- package/src/infrastructure/database/schema-migration/index.ts +66 -0
- package/src/infrastructure/database/schema-migration/migration-statements.ts +106 -0
- package/src/infrastructure/database/schema-migration/rename-detection.ts +87 -0
- package/src/infrastructure/database/schema-migration/table-operations.ts +65 -0
- package/src/infrastructure/database/schema-migration/type-utils.ts +98 -0
- package/src/infrastructure/database/schema-migration/types.ts +14 -0
- package/src/infrastructure/database/schema-migration-helpers.ts +53 -0
- package/src/infrastructure/database/session-context.ts +20 -0
- package/src/infrastructure/database/sql/sql-check-constraints.ts +252 -0
- package/src/infrastructure/database/sql/sql-column-generators.ts +174 -0
- package/src/infrastructure/database/sql/sql-execution.ts +245 -0
- package/src/infrastructure/database/sql/sql-field-predicates.ts +81 -0
- package/src/infrastructure/database/sql/sql-generators.ts +91 -0
- package/src/infrastructure/database/sql/sql-junction-tables.ts +79 -0
- package/src/infrastructure/database/sql/sql-key-constraints.ts +210 -0
- package/src/infrastructure/database/sql/sql-type-mappings.ts +106 -0
- package/src/infrastructure/database/sql/sql-utils.ts +53 -0
- package/src/infrastructure/database/table-live-layers.ts +30 -0
- package/src/infrastructure/database/table-operations/column-generators.ts +82 -0
- package/src/infrastructure/database/table-operations/create-table-sql.ts +81 -0
- package/src/infrastructure/database/table-operations/index.ts +55 -0
- package/src/infrastructure/database/table-operations/migration-utils.ts +157 -0
- package/src/infrastructure/database/table-operations/table-effects.ts +234 -0
- package/src/infrastructure/database/table-operations/table-features.ts +96 -0
- package/src/infrastructure/database/table-operations/type-compatibility.ts +58 -0
- package/src/infrastructure/database/table-operations.ts +47 -0
- package/src/infrastructure/database/table-queries/batch/batch-create.ts +80 -0
- package/src/infrastructure/database/table-queries/batch/batch-delete.ts +212 -0
- package/src/infrastructure/database/table-queries/batch/batch-helpers.ts +124 -0
- package/src/infrastructure/database/table-queries/batch/batch-restore.ts +161 -0
- package/src/infrastructure/database/table-queries/batch/batch-update.ts +146 -0
- package/src/infrastructure/database/table-queries/batch/batch-upsert.ts +357 -0
- package/src/infrastructure/database/table-queries/batch/batch.ts +14 -0
- package/src/infrastructure/database/table-queries/crud/crud-read.ts +351 -0
- package/src/infrastructure/database/table-queries/crud/crud-write.ts +399 -0
- package/src/infrastructure/database/table-queries/crud/crud.ts +16 -0
- package/src/infrastructure/database/table-queries/index.ts +11 -0
- package/src/infrastructure/database/table-queries/mutation-helpers/authorship-helpers.ts +152 -0
- package/src/infrastructure/database/table-queries/mutation-helpers/create-record-helpers.ts +90 -0
- package/src/infrastructure/database/table-queries/mutation-helpers/delete-helpers.ts +163 -0
- package/src/infrastructure/database/table-queries/mutation-helpers/record-fetch-helpers.ts +79 -0
- package/src/infrastructure/database/table-queries/mutation-helpers/update-helpers.ts +74 -0
- package/src/infrastructure/database/table-queries/query-helpers/activity-log-helpers.ts +53 -0
- package/src/infrastructure/database/table-queries/query-helpers/activity-queries.ts +106 -0
- package/src/infrastructure/database/table-queries/query-helpers/aggregation-helpers.ts +314 -0
- package/src/infrastructure/database/table-queries/query-helpers/comment-queries.ts +414 -0
- package/src/infrastructure/database/table-queries/query-helpers/record-validation-queries.ts +126 -0
- package/src/infrastructure/database/table-queries/query-helpers/trash-helpers.ts +58 -0
- package/src/infrastructure/database/table-queries/shared/error-handling.ts +47 -0
- package/src/infrastructure/database/table-queries/shared/typed-execute.ts +27 -0
- package/src/infrastructure/database/table-queries/shared/user-join-helpers.ts +38 -0
- package/src/infrastructure/database/table-queries/shared/validation.ts +39 -0
- package/src/infrastructure/database/views/view-generators.ts +258 -0
- package/src/infrastructure/devtools/devtools-layer.ts +43 -0
- package/src/infrastructure/devtools/index.ts +8 -0
- package/src/infrastructure/email/email-config.ts +103 -0
- package/src/infrastructure/email/email-service.ts +152 -0
- package/src/infrastructure/email/index.ts +107 -0
- package/src/infrastructure/email/nodemailer.ts +125 -0
- package/src/infrastructure/email/templates.ts +244 -0
- package/src/infrastructure/errors/auth-config-required-error.ts +21 -0
- package/src/infrastructure/errors/auth-error.ts +16 -0
- package/src/infrastructure/errors/css-compilation-error.ts +14 -0
- package/src/infrastructure/errors/index.ts +26 -0
- package/src/infrastructure/errors/schema-initialization-error.ts +19 -0
- package/src/infrastructure/errors/server-creation-error.ts +14 -0
- package/src/infrastructure/filesystem/copy-directory.ts +136 -0
- package/src/infrastructure/layers/app-layer.ts +61 -0
- package/src/infrastructure/layers/page-renderer-layer.ts +41 -0
- package/src/infrastructure/logging/index.ts +8 -0
- package/src/infrastructure/logging/logger.ts +204 -0
- package/src/infrastructure/schema/file-loader.ts +53 -0
- package/src/infrastructure/schema/index.ts +15 -0
- package/src/infrastructure/schema/remote-loader.ts +48 -0
- package/src/infrastructure/server/index.ts +26 -0
- package/src/infrastructure/server/language-detection.ts +87 -0
- package/src/infrastructure/server/lifecycle.ts +67 -0
- package/src/infrastructure/server/route-setup/api-routes.ts +310 -0
- package/src/infrastructure/server/route-setup/auth-route-utils.ts +399 -0
- package/src/infrastructure/server/route-setup/auth-routes.ts +245 -0
- package/src/infrastructure/server/route-setup/openapi-routes.ts +45 -0
- package/src/infrastructure/server/route-setup/openapi-schema.ts +120 -0
- package/src/infrastructure/server/route-setup/page-routes.ts +219 -0
- package/src/infrastructure/server/route-setup/static-assets.ts +191 -0
- package/src/infrastructure/server/server-factory-live.ts +45 -0
- package/src/infrastructure/server/server.ts +275 -0
- package/src/infrastructure/server/ssg-adapter.ts +196 -0
- package/src/infrastructure/server/static-site-generator-live.ts +20 -0
- package/src/infrastructure/utils/accept-language-parser.ts +106 -0
- package/src/infrastructure/utils/glob-matcher.ts +50 -0
- package/src/presentation/api/client.ts +114 -0
- package/src/presentation/api/middleware/auth.ts +233 -0
- package/src/presentation/api/middleware/table.ts +155 -0
- package/src/presentation/api/middleware/validation.ts +88 -0
- package/src/presentation/api/routes/activity/get-activity-by-id-handler.ts +77 -0
- package/src/presentation/api/routes/activity/index.ts +28 -0
- package/src/presentation/api/routes/activity.ts +339 -0
- package/src/presentation/api/routes/analytics.ts +328 -0
- package/src/presentation/api/routes/auth.ts +169 -0
- package/src/presentation/api/routes/index.ts +11 -0
- package/src/presentation/api/routes/tables/activity-handlers.ts +57 -0
- package/src/presentation/api/routes/tables/batch-permission-helpers.ts +163 -0
- package/src/presentation/api/routes/tables/batch-routes.ts +355 -0
- package/src/presentation/api/routes/tables/comment-handlers.ts +377 -0
- package/src/presentation/api/routes/tables/create-record-helpers.ts +179 -0
- package/src/presentation/api/routes/tables/effect-runner.ts +58 -0
- package/src/presentation/api/routes/tables/error-handlers.ts +53 -0
- package/src/presentation/api/routes/tables/field-permission-validation.ts +167 -0
- package/src/presentation/api/routes/tables/filter-parser.ts +75 -0
- package/src/presentation/api/routes/tables/formula-parser.ts +118 -0
- package/src/presentation/api/routes/tables/index.ts +113 -0
- package/src/presentation/api/routes/tables/list-records-filter.ts +54 -0
- package/src/presentation/api/routes/tables/param-parsers.ts +59 -0
- package/src/presentation/api/routes/tables/record-handlers.ts +484 -0
- package/src/presentation/api/routes/tables/record-routes.ts +53 -0
- package/src/presentation/api/routes/tables/record-update-handler.ts +200 -0
- package/src/presentation/api/routes/tables/sort-validation.ts +85 -0
- package/src/presentation/api/routes/tables/table-routes.ts +76 -0
- package/src/presentation/api/routes/tables/timezone-validation.ts +41 -0
- package/src/presentation/api/routes/tables/upsert-helpers.ts +471 -0
- package/src/presentation/api/routes/tables/utils.ts +159 -0
- package/src/presentation/api/routes/tables/view-routes.ts +51 -0
- package/src/presentation/api/routes/tables.ts +9 -0
- package/src/presentation/api/utils/context-helpers.ts +43 -0
- package/src/presentation/api/utils/error-sanitizer.ts +235 -0
- package/src/presentation/api/utils/field-permission-validator.ts +53 -0
- package/src/presentation/api/utils/filter-field-validator.ts +90 -0
- package/src/presentation/api/utils/index.ts +13 -0
- package/src/presentation/api/utils/run-effect.ts +94 -0
- package/src/presentation/api/utils/validate-request.ts +89 -0
- package/src/presentation/api/validation/index.ts +29 -0
- package/src/presentation/api/validation/rules/field-rules.ts +158 -0
- package/src/presentation/api/validation/rules/record-rules.ts +73 -0
- package/src/presentation/cli/index.ts +19 -0
- package/src/presentation/cli/schema-loader.ts +172 -0
- package/src/presentation/hooks/use-breakpoint.ts +155 -0
- package/src/presentation/rendering/render-error-pages.tsx +60 -0
- package/src/presentation/rendering/render-homepage.tsx +137 -0
- package/src/presentation/scripts/script-renderers.ts +112 -0
- package/src/presentation/styling/animation-composer.ts +117 -0
- package/src/presentation/styling/index.ts +13 -0
- package/src/presentation/styling/parse-style.ts +243 -0
- package/src/presentation/styling/style-utils.ts +50 -0
- package/src/presentation/styling/theme-colors.ts +53 -0
- package/src/presentation/translations/component-utils.ts +54 -0
- package/src/presentation/translations/index.ts +16 -0
- package/src/presentation/translations/translation-resolver.ts +22 -0
- package/src/presentation/ui/languages/language-switcher.tsx +119 -0
- package/src/presentation/ui/metadata/analytics-builders.tsx +174 -0
- package/src/presentation/ui/metadata/analytics-head.tsx +39 -0
- package/src/presentation/ui/metadata/custom-elements-builders.tsx +157 -0
- package/src/presentation/ui/metadata/extract-component-meta.ts +108 -0
- package/src/presentation/ui/metadata/head-elements.tsx +164 -0
- package/src/presentation/ui/metadata/index.tsx +35 -0
- package/src/presentation/ui/metadata/meta-utils.tsx +42 -0
- package/src/presentation/ui/metadata/open-graph-meta.tsx +57 -0
- package/src/presentation/ui/metadata/structured-data-from-component.tsx +134 -0
- package/src/presentation/ui/metadata/structured-data.tsx +88 -0
- package/src/presentation/ui/metadata/twitter-card-meta.tsx +80 -0
- package/src/presentation/ui/pages/DefaultHomePage.tsx +43 -0
- package/src/presentation/ui/pages/DefaultPageConfigs.ts +220 -0
- package/src/presentation/ui/pages/DynamicPage.tsx +307 -0
- package/src/presentation/ui/pages/ErrorPage.tsx +25 -0
- package/src/presentation/ui/pages/NotFoundPage.tsx +25 -0
- package/src/presentation/ui/pages/PageBodyScripts.tsx +242 -0
- package/src/presentation/ui/pages/PageBodyStyles.ts +52 -0
- package/src/presentation/ui/pages/PageHead.tsx +380 -0
- package/src/presentation/ui/pages/PageLangResolver.ts +58 -0
- package/src/presentation/ui/pages/PageMain.tsx +58 -0
- package/src/presentation/ui/pages/PageMetadata.ts +168 -0
- package/src/presentation/ui/pages/PageMetadataI18n.ts +169 -0
- package/src/presentation/ui/pages/PageScripts.ts +78 -0
- package/src/presentation/ui/pages/SectionRenderer.tsx +67 -0
- package/src/presentation/ui/pages/SectionSpacing.tsx +131 -0
- package/src/presentation/ui/sections/component-renderer.tsx +426 -0
- package/src/presentation/ui/sections/component-renderer.types.ts +33 -0
- package/src/presentation/ui/sections/components/component-reference-handler.tsx +74 -0
- package/src/presentation/ui/sections/components/component-resolution.ts +65 -0
- package/src/presentation/ui/sections/components/index.ts +9 -0
- package/src/presentation/ui/sections/hero.tsx +394 -0
- package/src/presentation/ui/sections/props/component-builder.ts +183 -0
- package/src/presentation/ui/sections/props/element-props.ts +179 -0
- package/src/presentation/ui/sections/props/index.ts +9 -0
- package/src/presentation/ui/sections/props/prop-conversion.ts +171 -0
- package/src/presentation/ui/sections/props/props-builder-config.ts +42 -0
- package/src/presentation/ui/sections/props/props-builder.ts +296 -0
- package/src/presentation/ui/sections/renderers/element-renderers/html-element-renderer.tsx +124 -0
- package/src/presentation/ui/sections/renderers/element-renderers/index.ts +59 -0
- package/src/presentation/ui/sections/renderers/element-renderers/interactive-renderers.tsx +231 -0
- package/src/presentation/ui/sections/renderers/element-renderers/media-renderers.tsx +102 -0
- package/src/presentation/ui/sections/renderers/element-renderers/text-content-renderers.tsx +42 -0
- package/src/presentation/ui/sections/renderers/element-renderers.ts +53 -0
- package/src/presentation/ui/sections/renderers/html-element-helpers.ts +100 -0
- package/src/presentation/ui/sections/renderers/specialized-renderers.tsx +212 -0
- package/src/presentation/ui/sections/rendering/component-dispatch-config.ts +31 -0
- package/src/presentation/ui/sections/rendering/component-registry/index.ts +39 -0
- package/src/presentation/ui/sections/rendering/component-registry/interactive-components.ts +54 -0
- package/src/presentation/ui/sections/rendering/component-registry/media-components.ts +36 -0
- package/src/presentation/ui/sections/rendering/component-registry/special-components.tsx +153 -0
- package/src/presentation/ui/sections/rendering/component-registry/structural-components.ts +215 -0
- package/src/presentation/ui/sections/rendering/component-registry/text-components.ts +57 -0
- package/src/presentation/ui/sections/rendering/component-registry-helpers.tsx +29 -0
- package/src/presentation/ui/sections/rendering/component-registry.tsx +21 -0
- package/src/presentation/ui/sections/rendering/component-type-dispatcher.tsx +33 -0
- package/src/presentation/ui/sections/rendering/index.ts +9 -0
- package/src/presentation/ui/sections/responsive/responsive-children-builder.tsx +96 -0
- package/src/presentation/ui/sections/responsive/responsive-content-builder.tsx +95 -0
- package/src/presentation/ui/sections/responsive/responsive-props-merger.ts +195 -0
- package/src/presentation/ui/sections/responsive/responsive-resolver.ts +213 -0
- package/src/presentation/ui/sections/styling/animation-composer-wrapper.ts +65 -0
- package/src/presentation/ui/sections/styling/class-builders.ts +45 -0
- package/src/presentation/ui/sections/styling/color-resolver.ts +43 -0
- package/src/presentation/ui/sections/styling/hover-interaction-handler.ts +107 -0
- package/src/presentation/ui/sections/styling/index.ts +9 -0
- package/src/presentation/ui/sections/styling/interaction-props-builder.ts +55 -0
- package/src/presentation/ui/sections/styling/shadow-resolver.ts +83 -0
- package/src/presentation/ui/sections/styling/spacing-resolver.ts +104 -0
- package/src/presentation/ui/sections/styling/style-processor.ts +170 -0
- package/src/presentation/ui/sections/styling/theme-tokens.ts +184 -0
- package/src/presentation/ui/sections/translations/i18n-content-resolver.ts +198 -0
- package/src/presentation/ui/sections/translations/index.ts +9 -0
- package/src/presentation/ui/sections/translations/translation-handler.ts +143 -0
- package/src/presentation/ui/sections/translations/variable-substitution.ts +225 -0
- package/src/presentation/ui/sections/utils/time-parser.ts +82 -0
- package/src/presentation/utils/link-attributes.ts +50 -0
- package/src/presentation/utils/string-utils.ts +58 -0
- package/src/presentation/utils/styles.ts +50 -0
- package/tsconfig.json +46 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 ESSENTIAL SERVICES
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the Business Source License 1.1
|
|
5
|
+
* found in the LICENSE.md file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { sql } from 'drizzle-orm'
|
|
9
|
+
import { Effect } from 'effect'
|
|
10
|
+
import { db, SessionContextError, UniqueConstraintViolationError } from '@/infrastructure/database'
|
|
11
|
+
import {
|
|
12
|
+
injectCreateAuthorship,
|
|
13
|
+
injectUpdateAuthorship,
|
|
14
|
+
} from '../mutation-helpers/authorship-helpers'
|
|
15
|
+
import {
|
|
16
|
+
buildInsertClauses,
|
|
17
|
+
isUniqueConstraintViolation,
|
|
18
|
+
} from '../mutation-helpers/create-record-helpers'
|
|
19
|
+
import {
|
|
20
|
+
cascadeSoftDelete,
|
|
21
|
+
executeSoftDelete,
|
|
22
|
+
executeHardDelete,
|
|
23
|
+
checkDeletedAtColumn,
|
|
24
|
+
} from '../mutation-helpers/delete-helpers'
|
|
25
|
+
import { fetchRecordById } from '../mutation-helpers/record-fetch-helpers'
|
|
26
|
+
import {
|
|
27
|
+
validateFieldsNotEmpty,
|
|
28
|
+
buildUpdateSetClauseCRUD,
|
|
29
|
+
executeRecordUpdateCRUD,
|
|
30
|
+
} from '../mutation-helpers/update-helpers'
|
|
31
|
+
import { logActivity } from '../query-helpers/activity-log-helpers'
|
|
32
|
+
import { wrapDatabaseError } from '../shared/error-handling'
|
|
33
|
+
import { typedExecute } from '../shared/typed-execute'
|
|
34
|
+
import { validateTableName } from '../shared/validation'
|
|
35
|
+
import type { App } from '@/domain/models/app'
|
|
36
|
+
import type { Session } from '@/infrastructure/auth/better-auth/schema'
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create a new record
|
|
40
|
+
*
|
|
41
|
+
* @param session - Better Auth session
|
|
42
|
+
* @param tableName - Name of the table
|
|
43
|
+
* @param fields - Record fields
|
|
44
|
+
* @returns Effect resolving to created record
|
|
45
|
+
*/
|
|
46
|
+
export function createRecord(
|
|
47
|
+
session: Readonly<Session>,
|
|
48
|
+
tableName: string,
|
|
49
|
+
fields: Readonly<Record<string, unknown>>
|
|
50
|
+
): Effect.Effect<Record<string, unknown>, SessionContextError | UniqueConstraintViolationError> {
|
|
51
|
+
return Effect.gen(function* () {
|
|
52
|
+
const record = yield* Effect.tryPromise({
|
|
53
|
+
try: () =>
|
|
54
|
+
db.transaction(async (tx) => {
|
|
55
|
+
validateTableName(tableName)
|
|
56
|
+
|
|
57
|
+
// Validate we have fields to insert
|
|
58
|
+
if (Object.keys(fields).length === 0) {
|
|
59
|
+
// eslint-disable-next-line functional/no-throw-statements -- Required for transaction error handling
|
|
60
|
+
throw new SessionContextError('Cannot create record with no fields', undefined)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Inject authorship metadata (created_by, updated_by) from session
|
|
64
|
+
const fieldsWithAuthorship = await injectCreateAuthorship(
|
|
65
|
+
fields,
|
|
66
|
+
session.userId,
|
|
67
|
+
tx,
|
|
68
|
+
tableName
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
// Build INSERT query
|
|
72
|
+
const { columnsClause, valuesClause } = buildInsertClauses(fieldsWithAuthorship)
|
|
73
|
+
|
|
74
|
+
// Execute INSERT directly (avoid Effect.runPromise which wraps errors in FiberFailure)
|
|
75
|
+
const insertResult = (await tx.execute(
|
|
76
|
+
sql`INSERT INTO ${sql.identifier(tableName)} (${columnsClause}) VALUES (${valuesClause}) RETURNING *`
|
|
77
|
+
)) as readonly Record<string, unknown>[]
|
|
78
|
+
return insertResult[0] ?? {}
|
|
79
|
+
}),
|
|
80
|
+
catch: (error) => {
|
|
81
|
+
if (error instanceof SessionContextError) return error
|
|
82
|
+
if (error instanceof UniqueConstraintViolationError) return error
|
|
83
|
+
if (isUniqueConstraintViolation(error)) {
|
|
84
|
+
return new UniqueConstraintViolationError('Unique constraint violation', error)
|
|
85
|
+
}
|
|
86
|
+
return new SessionContextError(`Failed to create record in ${tableName}`, error)
|
|
87
|
+
},
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// Log activity for record creation
|
|
91
|
+
yield* logActivity({
|
|
92
|
+
session,
|
|
93
|
+
tableName,
|
|
94
|
+
action: 'create',
|
|
95
|
+
recordId: String(record.id),
|
|
96
|
+
changes: { after: record },
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
return record
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Log activity for record update
|
|
105
|
+
*/
|
|
106
|
+
function logRecordUpdateActivity(config: {
|
|
107
|
+
readonly session: Readonly<Session>
|
|
108
|
+
readonly tableName: string
|
|
109
|
+
readonly recordId: string
|
|
110
|
+
readonly changes: {
|
|
111
|
+
readonly before: Record<string, unknown> | undefined
|
|
112
|
+
readonly after: Record<string, unknown>
|
|
113
|
+
}
|
|
114
|
+
readonly app?: App
|
|
115
|
+
}): Effect.Effect<void, never> {
|
|
116
|
+
const { session, tableName, recordId, changes, app } = config
|
|
117
|
+
return logActivity({
|
|
118
|
+
session,
|
|
119
|
+
tableName,
|
|
120
|
+
action: 'update',
|
|
121
|
+
recordId,
|
|
122
|
+
changes,
|
|
123
|
+
app,
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Update a record
|
|
129
|
+
*
|
|
130
|
+
* @param session - Better Auth session
|
|
131
|
+
* @param tableName - Name of the table
|
|
132
|
+
* @param recordId - Record ID
|
|
133
|
+
* @param params - Update parameters
|
|
134
|
+
* @returns Effect resolving to updated record
|
|
135
|
+
*/
|
|
136
|
+
export function updateRecord(
|
|
137
|
+
session: Readonly<Session>,
|
|
138
|
+
tableName: string,
|
|
139
|
+
recordId: string,
|
|
140
|
+
params: {
|
|
141
|
+
readonly fields: Readonly<Record<string, unknown>>
|
|
142
|
+
readonly app?: App
|
|
143
|
+
}
|
|
144
|
+
): Effect.Effect<Record<string, unknown>, SessionContextError> {
|
|
145
|
+
const { fields, app } = params
|
|
146
|
+
return Effect.gen(function* () {
|
|
147
|
+
const { recordBefore, updatedRecord } = yield* Effect.tryPromise({
|
|
148
|
+
try: () =>
|
|
149
|
+
db.transaction(async (tx) => {
|
|
150
|
+
validateTableName(tableName)
|
|
151
|
+
|
|
152
|
+
// Inject updated_by from session
|
|
153
|
+
const fieldsWithUpdatedBy = await injectUpdateAuthorship(
|
|
154
|
+
fields,
|
|
155
|
+
session.userId,
|
|
156
|
+
tx,
|
|
157
|
+
tableName
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
const entries = await validateFieldsNotEmpty(fieldsWithUpdatedBy)
|
|
161
|
+
const before = await fetchRecordById(tx, tableName, recordId)
|
|
162
|
+
const setClause = buildUpdateSetClauseCRUD(entries)
|
|
163
|
+
const updated = await executeRecordUpdateCRUD(tx, tableName, recordId, setClause)
|
|
164
|
+
return { recordBefore: before, updatedRecord: updated }
|
|
165
|
+
}),
|
|
166
|
+
catch: wrapDatabaseError(`Failed to update record in ${tableName}`),
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
yield* logRecordUpdateActivity({
|
|
170
|
+
session,
|
|
171
|
+
tableName,
|
|
172
|
+
recordId,
|
|
173
|
+
changes: {
|
|
174
|
+
before: recordBefore,
|
|
175
|
+
after: updatedRecord,
|
|
176
|
+
},
|
|
177
|
+
app,
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
return updatedRecord
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Delete a record (soft delete if deleted_at field exists)
|
|
186
|
+
*
|
|
187
|
+
* Implements soft delete pattern:
|
|
188
|
+
* - If table has deleted_at field: Sets deleted_at to NOW() (soft delete)
|
|
189
|
+
* - If no deleted_at field: Performs hard delete
|
|
190
|
+
* - Permissions applied via application layer
|
|
191
|
+
* - Cascade soft delete to related records if configured with onDelete: 'cascade'
|
|
192
|
+
* - Activity logging captures record state before deletion (non-blocking)
|
|
193
|
+
*
|
|
194
|
+
* @param session - Better Auth session
|
|
195
|
+
* @param tableName - Name of the table
|
|
196
|
+
* @param recordId - Record ID
|
|
197
|
+
* @param app - App schema (optional, for cascade delete logic)
|
|
198
|
+
* @returns Effect resolving to success boolean
|
|
199
|
+
*/
|
|
200
|
+
// eslint-disable-next-line max-lines-per-function -- Delete logic requires soft-delete check, cascade, and hard-delete fallback
|
|
201
|
+
export function deleteRecord(
|
|
202
|
+
session: Readonly<Session>,
|
|
203
|
+
tableName: string,
|
|
204
|
+
recordId: string,
|
|
205
|
+
app?: {
|
|
206
|
+
readonly tables?: ReadonlyArray<{
|
|
207
|
+
readonly name: string
|
|
208
|
+
readonly fields: ReadonlyArray<{
|
|
209
|
+
readonly name: string
|
|
210
|
+
readonly type: string
|
|
211
|
+
readonly relatedTable?: string
|
|
212
|
+
readonly onDelete?: string
|
|
213
|
+
}>
|
|
214
|
+
}>
|
|
215
|
+
}
|
|
216
|
+
): Effect.Effect<boolean, SessionContextError> {
|
|
217
|
+
return Effect.gen(function* () {
|
|
218
|
+
const result = yield* Effect.tryPromise({
|
|
219
|
+
try: () =>
|
|
220
|
+
db.transaction(async (tx) => {
|
|
221
|
+
validateTableName(tableName)
|
|
222
|
+
|
|
223
|
+
// Check if table supports soft delete
|
|
224
|
+
const hasSoftDelete = await checkDeletedAtColumn(tx, tableName)
|
|
225
|
+
|
|
226
|
+
// Fetch record before deletion for activity logging
|
|
227
|
+
const recordBeforeData = await fetchRecordById(tx, tableName, recordId)
|
|
228
|
+
|
|
229
|
+
if (hasSoftDelete) {
|
|
230
|
+
// Execute soft delete
|
|
231
|
+
const success = await executeSoftDelete(tx, tableName, recordId, session.userId)
|
|
232
|
+
|
|
233
|
+
if (!success) {
|
|
234
|
+
return { success: false, recordBeforeData: undefined }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Cascade to related records if configured
|
|
238
|
+
if (app) {
|
|
239
|
+
// eslint-disable-next-line functional/no-expression-statements -- Required for cascade operation
|
|
240
|
+
await cascadeSoftDelete(tx, tableName, recordId, app, session.userId)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return { success: true, recordBeforeData }
|
|
244
|
+
} else {
|
|
245
|
+
// Execute hard delete
|
|
246
|
+
const success = await executeHardDelete(tx, tableName, recordId)
|
|
247
|
+
return { success, recordBeforeData: undefined }
|
|
248
|
+
}
|
|
249
|
+
}),
|
|
250
|
+
catch: wrapDatabaseError(`Failed to delete record from ${tableName}`),
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
// Log activity for soft delete (outside transaction)
|
|
254
|
+
if (result.success && result.recordBeforeData) {
|
|
255
|
+
yield* logActivity({
|
|
256
|
+
session,
|
|
257
|
+
tableName,
|
|
258
|
+
action: 'delete',
|
|
259
|
+
recordId,
|
|
260
|
+
changes: { before: result.recordBeforeData },
|
|
261
|
+
})
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return result.success
|
|
265
|
+
})
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Permanently delete a record (hard delete)
|
|
270
|
+
*
|
|
271
|
+
* Permanently removes the record from the database, regardless of deleted_at field.
|
|
272
|
+
* This operation is irreversible and should only be allowed for admin roles.
|
|
273
|
+
* Permissions applied via application layer.
|
|
274
|
+
* Activity logging captures record state before deletion (non-blocking).
|
|
275
|
+
*
|
|
276
|
+
* @param session - Better Auth session
|
|
277
|
+
* @param tableName - Name of the table
|
|
278
|
+
* @param recordId - Record ID
|
|
279
|
+
* @returns Effect resolving to success boolean
|
|
280
|
+
*/
|
|
281
|
+
export function permanentlyDeleteRecord(
|
|
282
|
+
session: Readonly<Session>,
|
|
283
|
+
tableName: string,
|
|
284
|
+
recordId: string
|
|
285
|
+
): Effect.Effect<boolean, SessionContextError> {
|
|
286
|
+
return Effect.gen(function* () {
|
|
287
|
+
const result = yield* Effect.tryPromise({
|
|
288
|
+
try: () =>
|
|
289
|
+
db.transaction(async (tx) => {
|
|
290
|
+
validateTableName(tableName)
|
|
291
|
+
|
|
292
|
+
// Fetch record before deletion for activity logging
|
|
293
|
+
const recordBeforeData = await fetchRecordById(tx, tableName, recordId)
|
|
294
|
+
|
|
295
|
+
// Execute hard delete
|
|
296
|
+
const success = await executeHardDelete(tx, tableName, recordId)
|
|
297
|
+
|
|
298
|
+
return { success, recordBeforeData: success ? recordBeforeData : undefined }
|
|
299
|
+
}),
|
|
300
|
+
catch: wrapDatabaseError(`Failed to permanently delete record from ${tableName}`),
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
// Log activity for permanent delete (outside transaction)
|
|
304
|
+
if (result.success && result.recordBeforeData) {
|
|
305
|
+
yield* logActivity({
|
|
306
|
+
session,
|
|
307
|
+
tableName,
|
|
308
|
+
action: 'delete',
|
|
309
|
+
recordId,
|
|
310
|
+
changes: { before: result.recordBeforeData },
|
|
311
|
+
})
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return result.success
|
|
315
|
+
})
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Restore a soft-deleted record
|
|
320
|
+
*
|
|
321
|
+
* Clears the deleted_at timestamp to restore a soft-deleted record.
|
|
322
|
+
* Returns error if record doesn't exist or is not soft-deleted.
|
|
323
|
+
* Permissions applied via application layer.
|
|
324
|
+
*
|
|
325
|
+
* @param session - Better Auth session
|
|
326
|
+
* @param tableName - Name of the table
|
|
327
|
+
* @param recordId - Record ID
|
|
328
|
+
* @returns Effect resolving to restored record or null
|
|
329
|
+
*/
|
|
330
|
+
// eslint-disable-next-line max-lines-per-function -- Restore logic requires deleted_by column check and conditional restore
|
|
331
|
+
export function restoreRecord(
|
|
332
|
+
session: Readonly<Session>,
|
|
333
|
+
tableName: string,
|
|
334
|
+
recordId: string
|
|
335
|
+
): Effect.Effect<Record<string, unknown> | null, SessionContextError> {
|
|
336
|
+
return Effect.gen(function* () {
|
|
337
|
+
const restoredRecord = yield* Effect.tryPromise({
|
|
338
|
+
try: () =>
|
|
339
|
+
db.transaction(async (tx) => {
|
|
340
|
+
validateTableName(tableName)
|
|
341
|
+
const tableIdent = sql.identifier(tableName)
|
|
342
|
+
|
|
343
|
+
// Check if record exists (including soft-deleted records)
|
|
344
|
+
const checkResult = await typedExecute(
|
|
345
|
+
tx,
|
|
346
|
+
sql`SELECT id, deleted_at FROM ${tableIdent} WHERE id = ${recordId} LIMIT 1`
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
if (checkResult.length === 0) {
|
|
350
|
+
// eslint-disable-next-line unicorn/no-null -- Null is intentional for non-existent records
|
|
351
|
+
return null // Record not found
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const record = checkResult[0]
|
|
355
|
+
|
|
356
|
+
// Check if record is soft-deleted
|
|
357
|
+
if (!record?.deleted_at) {
|
|
358
|
+
// Record exists but is not deleted - return error via special marker
|
|
359
|
+
return { _error: 'not_deleted' } as Record<string, unknown>
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Check if table has deleted_by column
|
|
363
|
+
const deletedByCheck = await typedExecute(
|
|
364
|
+
tx,
|
|
365
|
+
sql`SELECT column_name FROM information_schema.columns WHERE table_name = ${tableName} AND column_name = 'deleted_by'`
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
const hasDeletedBy = deletedByCheck.length > 0
|
|
369
|
+
|
|
370
|
+
// Restore record by clearing deleted_at and deleted_by (if column exists)
|
|
371
|
+
const result = hasDeletedBy
|
|
372
|
+
? await typedExecute(
|
|
373
|
+
tx,
|
|
374
|
+
sql`UPDATE ${tableIdent} SET deleted_at = NULL, deleted_by = NULL WHERE id = ${recordId} RETURNING *`
|
|
375
|
+
)
|
|
376
|
+
: await typedExecute(
|
|
377
|
+
tx,
|
|
378
|
+
sql`UPDATE ${tableIdent} SET deleted_at = NULL WHERE id = ${recordId} RETURNING *`
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
return result[0] ?? {}
|
|
382
|
+
}),
|
|
383
|
+
catch: wrapDatabaseError(`Failed to restore record ${recordId} from ${tableName}`),
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
// Log activity for record restoration (outside transaction)
|
|
387
|
+
if (restoredRecord && !('_error' in restoredRecord)) {
|
|
388
|
+
yield* logActivity({
|
|
389
|
+
session,
|
|
390
|
+
tableName,
|
|
391
|
+
action: 'restore',
|
|
392
|
+
recordId,
|
|
393
|
+
changes: { after: restoredRecord },
|
|
394
|
+
})
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return restoredRecord
|
|
398
|
+
})
|
|
399
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 ESSENTIAL SERVICES
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the Business Source License 1.1
|
|
5
|
+
* found in the LICENSE.md file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Re-export all CRUD operations from modular files
|
|
9
|
+
export { listRecords, computeAggregations, listTrash, getRecord } from './crud-read'
|
|
10
|
+
export {
|
|
11
|
+
createRecord,
|
|
12
|
+
updateRecord,
|
|
13
|
+
deleteRecord,
|
|
14
|
+
permanentlyDeleteRecord,
|
|
15
|
+
restoreRecord,
|
|
16
|
+
} from './crud-write'
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 ESSENTIAL SERVICES
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the Business Source License 1.1
|
|
5
|
+
* found in the LICENSE.md file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Re-export all table query functions from modular files
|
|
9
|
+
export * from './shared/validation'
|
|
10
|
+
export * from './crud/crud'
|
|
11
|
+
export * from './batch/batch'
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 ESSENTIAL SERVICES
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the Business Source License 1.1
|
|
5
|
+
* found in the LICENSE.md file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { sql } from 'drizzle-orm'
|
|
9
|
+
import { typedExecute } from '../shared/typed-execute'
|
|
10
|
+
import type { DrizzleTransaction } from '@/infrastructure/database'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Authorship field names used across the system
|
|
14
|
+
*/
|
|
15
|
+
export const AUTHORSHIP_FIELDS = {
|
|
16
|
+
CREATED_BY: 'created_by',
|
|
17
|
+
UPDATED_BY: 'updated_by',
|
|
18
|
+
DELETED_BY: 'deleted_by',
|
|
19
|
+
} as const
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Check if table has specific authorship columns
|
|
23
|
+
* Queries information_schema once per transaction for efficiency
|
|
24
|
+
*
|
|
25
|
+
* @param tx - Database transaction
|
|
26
|
+
* @param tableName - Table name
|
|
27
|
+
* @param columnNames - Column names to check
|
|
28
|
+
* @returns Set of existing column names
|
|
29
|
+
*/
|
|
30
|
+
async function checkAuthorshipColumns(
|
|
31
|
+
tx: Readonly<DrizzleTransaction>,
|
|
32
|
+
tableName: string,
|
|
33
|
+
columnNames: readonly string[]
|
|
34
|
+
): Promise<Set<string>> {
|
|
35
|
+
// Query information_schema for column existence
|
|
36
|
+
const rows = await typedExecute<{ column_name: string }>(
|
|
37
|
+
tx,
|
|
38
|
+
sql.raw(
|
|
39
|
+
`SELECT column_name FROM information_schema.columns WHERE table_name = '${tableName}' AND column_name IN (${columnNames.map((name) => `'${name}'`).join(', ')})`
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
return new Set(rows.map((row) => row.column_name))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Normalize user ID for database storage
|
|
48
|
+
* Converts 'guest' to NULL for unauthenticated users
|
|
49
|
+
*
|
|
50
|
+
* @param userId - User ID from session
|
|
51
|
+
* @returns Normalized user ID or NULL for guest users
|
|
52
|
+
*/
|
|
53
|
+
function normalizeUserIdForDb(userId: string | undefined): string | null {
|
|
54
|
+
// eslint-disable-next-line unicorn/no-null -- NULL is intentional for database columns when no auth configured
|
|
55
|
+
if (!userId || userId === 'guest') return null
|
|
56
|
+
return userId
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Inject authorship fields into record for INSERT operations
|
|
61
|
+
* Sets both created_by and updated_by to the same user ID on creation
|
|
62
|
+
*
|
|
63
|
+
* @param fields - Original record fields
|
|
64
|
+
* @param userId - User ID from session
|
|
65
|
+
* @param tx - Database transaction
|
|
66
|
+
* @param tableName - Table name
|
|
67
|
+
* @returns Fields with authorship metadata injected
|
|
68
|
+
*/
|
|
69
|
+
export async function injectCreateAuthorship(
|
|
70
|
+
fields: Readonly<Record<string, unknown>>,
|
|
71
|
+
userId: string | undefined,
|
|
72
|
+
tx: Readonly<DrizzleTransaction>,
|
|
73
|
+
tableName: string
|
|
74
|
+
): Promise<Record<string, unknown>> {
|
|
75
|
+
// Check which authorship columns exist
|
|
76
|
+
const existingColumns = await checkAuthorshipColumns(tx, tableName, [
|
|
77
|
+
AUTHORSHIP_FIELDS.CREATED_BY,
|
|
78
|
+
AUTHORSHIP_FIELDS.UPDATED_BY,
|
|
79
|
+
])
|
|
80
|
+
|
|
81
|
+
// Normalize user ID for database
|
|
82
|
+
const authorUserId = normalizeUserIdForDb(userId)
|
|
83
|
+
|
|
84
|
+
// Build fields object with authorship metadata (immutable)
|
|
85
|
+
return {
|
|
86
|
+
...fields,
|
|
87
|
+
...(existingColumns.has(AUTHORSHIP_FIELDS.CREATED_BY)
|
|
88
|
+
? { [AUTHORSHIP_FIELDS.CREATED_BY]: authorUserId }
|
|
89
|
+
: {}),
|
|
90
|
+
...(existingColumns.has(AUTHORSHIP_FIELDS.UPDATED_BY)
|
|
91
|
+
? { [AUTHORSHIP_FIELDS.UPDATED_BY]: authorUserId }
|
|
92
|
+
: {}),
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Inject updated_by field for UPDATE operations
|
|
98
|
+
*
|
|
99
|
+
* @param fields - Original record fields
|
|
100
|
+
* @param userId - User ID from session
|
|
101
|
+
* @param tx - Database transaction
|
|
102
|
+
* @param tableName - Table name
|
|
103
|
+
* @returns Fields with updated_by injected
|
|
104
|
+
*/
|
|
105
|
+
export async function injectUpdateAuthorship(
|
|
106
|
+
fields: Readonly<Record<string, unknown>>,
|
|
107
|
+
userId: string | undefined,
|
|
108
|
+
tx: Readonly<DrizzleTransaction>,
|
|
109
|
+
tableName: string
|
|
110
|
+
): Promise<Record<string, unknown>> {
|
|
111
|
+
// Check if updated_by column exists
|
|
112
|
+
const existingColumns = await checkAuthorshipColumns(tx, tableName, [
|
|
113
|
+
AUTHORSHIP_FIELDS.UPDATED_BY,
|
|
114
|
+
])
|
|
115
|
+
|
|
116
|
+
const authorUserId = normalizeUserIdForDb(userId)
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
...fields,
|
|
120
|
+
...(existingColumns.has(AUTHORSHIP_FIELDS.UPDATED_BY)
|
|
121
|
+
? { [AUTHORSHIP_FIELDS.UPDATED_BY]: authorUserId }
|
|
122
|
+
: {}),
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Check if table has deleted_by column for soft delete authorship tracking
|
|
128
|
+
*
|
|
129
|
+
* @param tx - Database transaction
|
|
130
|
+
* @param tableName - Table name
|
|
131
|
+
* @returns True if deleted_by column exists
|
|
132
|
+
*/
|
|
133
|
+
export async function hasDeletedByColumn(
|
|
134
|
+
tx: Readonly<DrizzleTransaction>,
|
|
135
|
+
tableName: string
|
|
136
|
+
): Promise<boolean> {
|
|
137
|
+
const existingColumns = await checkAuthorshipColumns(tx, tableName, [
|
|
138
|
+
AUTHORSHIP_FIELDS.DELETED_BY,
|
|
139
|
+
])
|
|
140
|
+
return existingColumns.has(AUTHORSHIP_FIELDS.DELETED_BY)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Get normalized user ID for soft delete operations
|
|
145
|
+
* Returns NULL for guest users, user ID otherwise
|
|
146
|
+
*
|
|
147
|
+
* @param userId - User ID from session
|
|
148
|
+
* @returns Normalized user ID or NULL
|
|
149
|
+
*/
|
|
150
|
+
export function getDeletedByValue(userId: string | undefined): string | null {
|
|
151
|
+
return normalizeUserIdForDb(userId)
|
|
152
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 ESSENTIAL SERVICES
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the Business Source License 1.1
|
|
5
|
+
* found in the LICENSE.md file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { sql } from 'drizzle-orm'
|
|
9
|
+
import { Effect } from 'effect'
|
|
10
|
+
import {
|
|
11
|
+
SessionContextError,
|
|
12
|
+
UniqueConstraintViolationError,
|
|
13
|
+
type DrizzleTransaction,
|
|
14
|
+
} from '@/infrastructure/database'
|
|
15
|
+
import { validateColumnName } from '../shared/validation'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Shape of PostgreSQL driver error objects that may contain unique constraint info.
|
|
19
|
+
* Compatible with bun:sql, postgres.js, and pg error objects.
|
|
20
|
+
*/
|
|
21
|
+
interface PostgresErrorLike {
|
|
22
|
+
readonly code?: string
|
|
23
|
+
readonly constraint?: string
|
|
24
|
+
readonly message?: string
|
|
25
|
+
readonly cause?: PostgresErrorLike
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if an object has PostgreSQL unique constraint violation markers
|
|
30
|
+
* (code 23505, constraint name, or 'unique constraint' in message)
|
|
31
|
+
*/
|
|
32
|
+
function hasUniqueViolationMarkers(obj: PostgresErrorLike | null | undefined): boolean {
|
|
33
|
+
return obj?.code === '23505' || !!obj?.constraint || !!obj?.message?.includes('unique constraint')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check if an error is a PostgreSQL unique constraint violation (code 23505)
|
|
38
|
+
* Checks the error itself and its cause for violation markers.
|
|
39
|
+
*/
|
|
40
|
+
export function isUniqueConstraintViolation(error: unknown): boolean {
|
|
41
|
+
const err = error as PostgresErrorLike | null | undefined
|
|
42
|
+
return hasUniqueViolationMarkers(err) || hasUniqueViolationMarkers(err?.cause)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Build SQL columns and values for INSERT query
|
|
47
|
+
*/
|
|
48
|
+
export function buildInsertClauses(
|
|
49
|
+
fields: Readonly<Record<string, unknown>>
|
|
50
|
+
): Readonly<{ columnsClause: unknown; valuesClause: unknown }> {
|
|
51
|
+
const entries = Object.entries(fields)
|
|
52
|
+
|
|
53
|
+
// Build column identifiers and values
|
|
54
|
+
const columnIdentifiers = entries.map(([key]) => {
|
|
55
|
+
validateColumnName(key)
|
|
56
|
+
return sql.identifier(key)
|
|
57
|
+
})
|
|
58
|
+
const valueParams = entries.map(([, value]) => sql`${value}`)
|
|
59
|
+
|
|
60
|
+
// Build INSERT query using sql.join for columns and values
|
|
61
|
+
const columnsClause = sql.join(columnIdentifiers, sql.raw(', '))
|
|
62
|
+
const valuesClause = sql.join(valueParams, sql.raw(', '))
|
|
63
|
+
|
|
64
|
+
return { columnsClause, valuesClause }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Execute INSERT query and handle errors
|
|
69
|
+
*/
|
|
70
|
+
export function executeInsert(
|
|
71
|
+
tableName: string,
|
|
72
|
+
columnsClause: unknown,
|
|
73
|
+
valuesClause: unknown,
|
|
74
|
+
tx: Readonly<DrizzleTransaction>
|
|
75
|
+
): Effect.Effect<Record<string, unknown>, SessionContextError | UniqueConstraintViolationError> {
|
|
76
|
+
return Effect.tryPromise({
|
|
77
|
+
try: async () => {
|
|
78
|
+
const insertResult = (await tx.execute(
|
|
79
|
+
sql`INSERT INTO ${sql.identifier(tableName)} (${columnsClause}) VALUES (${valuesClause}) RETURNING *`
|
|
80
|
+
)) as readonly Record<string, unknown>[]
|
|
81
|
+
return insertResult[0] ?? {}
|
|
82
|
+
},
|
|
83
|
+
catch: (error) => {
|
|
84
|
+
if (isUniqueConstraintViolation(error)) {
|
|
85
|
+
return new UniqueConstraintViolationError('Unique constraint violation', error)
|
|
86
|
+
}
|
|
87
|
+
return new SessionContextError(`Failed to create record in ${tableName}`, error)
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
}
|