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,471 @@
|
|
|
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
|
+
hasCreatePermission,
|
|
12
|
+
hasUpdatePermission,
|
|
13
|
+
} from '@/application/use-cases/tables/permissions/permissions'
|
|
14
|
+
import { filterReadableFields } from '@/application/use-cases/tables/utils/field-read-filter'
|
|
15
|
+
import { db } from '@/infrastructure/database/drizzle'
|
|
16
|
+
import { validateFieldWritePermissions } from '@/presentation/api/utils/field-permission-validator'
|
|
17
|
+
import { validateRequiredFieldsForRecord } from './create-record-helpers'
|
|
18
|
+
import type { App } from '@/domain/models/app'
|
|
19
|
+
import type { Context } from 'hono'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Validate required fields for upsert records
|
|
23
|
+
* Records come from schema in nested format: { fields: {...} }
|
|
24
|
+
*/
|
|
25
|
+
export async function validateUpsertRequiredFields(
|
|
26
|
+
table: NonNullable<App['tables']>[number] | undefined,
|
|
27
|
+
records: readonly { fields: Record<string, unknown> }[]
|
|
28
|
+
): Promise<Array<{ record: number; field: string; error: string }>> {
|
|
29
|
+
return records.flatMap((record, index) => {
|
|
30
|
+
// Extract fields from nested format
|
|
31
|
+
const missingFields = validateRequiredFieldsForRecord(table, record.fields)
|
|
32
|
+
return missingFields.map((field: string) => ({
|
|
33
|
+
record: index,
|
|
34
|
+
field,
|
|
35
|
+
error: 'Required field is missing',
|
|
36
|
+
}))
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Check if any records exist in database based on merge fields
|
|
42
|
+
*/
|
|
43
|
+
export async function checkForExistingRecords(
|
|
44
|
+
tableName: string,
|
|
45
|
+
records: readonly { fields: Record<string, unknown> }[],
|
|
46
|
+
fieldsToMergeOn: readonly string[]
|
|
47
|
+
): Promise<boolean> {
|
|
48
|
+
// Build WHERE clause - skip records missing merge fields (will fail validation)
|
|
49
|
+
const mergeConditions = records
|
|
50
|
+
.filter((record) =>
|
|
51
|
+
fieldsToMergeOn.every((fieldName) => record.fields[fieldName] !== undefined)
|
|
52
|
+
)
|
|
53
|
+
.map((record) => {
|
|
54
|
+
const conditions = fieldsToMergeOn.map((fieldName) => {
|
|
55
|
+
const value = record.fields[fieldName]
|
|
56
|
+
return sql`${sql.identifier(fieldName)} = ${value}`
|
|
57
|
+
})
|
|
58
|
+
return conditions.length > 0 ? sql.join(conditions, sql` AND `) : sql`1=0`
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// If no valid records to check, return false
|
|
62
|
+
if (mergeConditions.length === 0) return false
|
|
63
|
+
|
|
64
|
+
const whereClause = sql.join(mergeConditions, sql` OR `)
|
|
65
|
+
const existingRecords = (await db.execute(
|
|
66
|
+
sql`SELECT COUNT(*) as count FROM ${sql.identifier(tableName)} WHERE ${whereClause}`
|
|
67
|
+
)) as readonly Record<string, unknown>[]
|
|
68
|
+
|
|
69
|
+
const firstRecord = existingRecords[0]
|
|
70
|
+
return firstRecord !== undefined && Number(firstRecord.count) > 0
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Validate field-level write permissions for records
|
|
75
|
+
*/
|
|
76
|
+
export function checkFieldPermissions(config: {
|
|
77
|
+
readonly app: App
|
|
78
|
+
readonly tableName: string
|
|
79
|
+
readonly userRole: string
|
|
80
|
+
readonly records: readonly { fields: Record<string, unknown> }[]
|
|
81
|
+
readonly c: Context
|
|
82
|
+
}): { allowed: true } | { allowed: false; response: Response } {
|
|
83
|
+
const { app, tableName, userRole, records, c } = config
|
|
84
|
+
|
|
85
|
+
const allForbiddenFields = records
|
|
86
|
+
.map((record) => validateFieldWritePermissions(app, tableName, userRole, record.fields))
|
|
87
|
+
.filter((fields) => fields.length > 0)
|
|
88
|
+
|
|
89
|
+
if (allForbiddenFields.length > 0) {
|
|
90
|
+
const uniqueForbiddenFields = [...new Set(allForbiddenFields.flat())]
|
|
91
|
+
const firstForbiddenField = uniqueForbiddenFields[0]
|
|
92
|
+
return {
|
|
93
|
+
allowed: false,
|
|
94
|
+
response: c.json(
|
|
95
|
+
{
|
|
96
|
+
success: false,
|
|
97
|
+
message: `Cannot write to field '${firstForbiddenField}': insufficient permissions`,
|
|
98
|
+
code: 'FORBIDDEN',
|
|
99
|
+
},
|
|
100
|
+
403
|
|
101
|
+
),
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { allowed: true }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Check upsert permissions including update permission check
|
|
110
|
+
* This function determines if records will be created or updated, then checks appropriate permissions
|
|
111
|
+
* Note: Field-level permissions should be checked separately before calling this function
|
|
112
|
+
*/
|
|
113
|
+
|
|
114
|
+
export async function checkUpsertPermissionsWithUpdateCheck(config: {
|
|
115
|
+
readonly app: App
|
|
116
|
+
readonly tableName: string
|
|
117
|
+
readonly userRole: string
|
|
118
|
+
readonly records: readonly { fields: Record<string, unknown> }[]
|
|
119
|
+
readonly fieldsToMergeOn: readonly string[]
|
|
120
|
+
readonly c: Context
|
|
121
|
+
}): Promise<{ allowed: true } | { allowed: false; response: Response }> {
|
|
122
|
+
const { app, tableName, userRole, records, fieldsToMergeOn, c } = config
|
|
123
|
+
const table = app.tables?.find((t) => t.name === tableName)
|
|
124
|
+
|
|
125
|
+
// Check if any records will be updated
|
|
126
|
+
const hasExistingRecords = await checkForExistingRecords(tableName, records, fieldsToMergeOn)
|
|
127
|
+
|
|
128
|
+
// If records will be updated, check update permission
|
|
129
|
+
if (hasExistingRecords && !hasUpdatePermission(table, userRole, app.tables)) {
|
|
130
|
+
return {
|
|
131
|
+
allowed: false,
|
|
132
|
+
response: c.json(
|
|
133
|
+
{
|
|
134
|
+
success: false,
|
|
135
|
+
message: 'You do not have permission to update records in this table',
|
|
136
|
+
code: 'FORBIDDEN',
|
|
137
|
+
},
|
|
138
|
+
403
|
|
139
|
+
),
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Check table-level create permission (for new records)
|
|
144
|
+
if (!hasCreatePermission(table, userRole, app.tables)) {
|
|
145
|
+
return {
|
|
146
|
+
allowed: false,
|
|
147
|
+
response: c.json(
|
|
148
|
+
{
|
|
149
|
+
success: false,
|
|
150
|
+
message: 'You do not have permission to create records in this table',
|
|
151
|
+
code: 'FORBIDDEN',
|
|
152
|
+
},
|
|
153
|
+
403
|
|
154
|
+
),
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { allowed: true }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Check if a field type is readonly (cannot be set by users)
|
|
163
|
+
*/
|
|
164
|
+
export function isReadonlyFieldType(fieldType: string): boolean {
|
|
165
|
+
const readonlyTypes = new Set(['created-at', 'updated-at', 'auto-number'])
|
|
166
|
+
return readonlyTypes.has(fieldType)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Validate that no readonly fields are being set
|
|
171
|
+
* Returns error response if readonly fields detected, undefined otherwise
|
|
172
|
+
*/
|
|
173
|
+
export function validateReadonlyFields(
|
|
174
|
+
table:
|
|
175
|
+
| {
|
|
176
|
+
readonly fields: ReadonlyArray<{
|
|
177
|
+
readonly name: string
|
|
178
|
+
readonly type: string
|
|
179
|
+
}>
|
|
180
|
+
}
|
|
181
|
+
| undefined,
|
|
182
|
+
records: readonly { fields: Record<string, unknown> }[],
|
|
183
|
+
c: Context
|
|
184
|
+
) {
|
|
185
|
+
// Check for 'id' field (always readonly)
|
|
186
|
+
const recordWithId = records.find((record) => 'id' in record.fields)
|
|
187
|
+
if (recordWithId) {
|
|
188
|
+
return c.json(
|
|
189
|
+
{
|
|
190
|
+
success: false,
|
|
191
|
+
message: "Cannot write to readonly field 'id'",
|
|
192
|
+
code: 'VALIDATION_ERROR',
|
|
193
|
+
},
|
|
194
|
+
400
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Check for readonly field types (created-at, updated-at, auto-number)
|
|
199
|
+
if (table) {
|
|
200
|
+
const readonlyFieldNames = new Set(
|
|
201
|
+
table.fields.filter((field) => isReadonlyFieldType(field.type)).map((field) => field.name)
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
const attemptedReadonlyField = records
|
|
205
|
+
.flatMap((record) => Object.keys(record.fields))
|
|
206
|
+
.find((fieldName) => readonlyFieldNames.has(fieldName))
|
|
207
|
+
|
|
208
|
+
if (attemptedReadonlyField) {
|
|
209
|
+
return c.json(
|
|
210
|
+
{
|
|
211
|
+
success: false,
|
|
212
|
+
message: `Cannot write to readonly field '${attemptedReadonlyField}'`,
|
|
213
|
+
code: 'VALIDATION_ERROR',
|
|
214
|
+
},
|
|
215
|
+
400
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return undefined
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Strip protected fields that user cannot write from records
|
|
225
|
+
* This prevents 403 errors for fields user doesn't have write access to
|
|
226
|
+
*/
|
|
227
|
+
export function stripUnwritableFields<T extends { fields: Record<string, unknown> }>(
|
|
228
|
+
app: App,
|
|
229
|
+
tableName: string,
|
|
230
|
+
userRole: string,
|
|
231
|
+
records: readonly T[]
|
|
232
|
+
): T[] {
|
|
233
|
+
return records.map((record) => {
|
|
234
|
+
const forbiddenFields = validateFieldWritePermissions(app, tableName, userRole, record.fields)
|
|
235
|
+
if (forbiddenFields.length === 0) {
|
|
236
|
+
return record
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Remove forbidden fields from the record
|
|
240
|
+
const filteredFields = Object.keys(record.fields).reduce<Record<string, unknown>>(
|
|
241
|
+
(acc, key) => {
|
|
242
|
+
if (!forbiddenFields.includes(key)) {
|
|
243
|
+
return { ...acc, [key]: record.fields[key] }
|
|
244
|
+
}
|
|
245
|
+
return acc
|
|
246
|
+
},
|
|
247
|
+
{}
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
return { ...record, fields: filteredFields } as T
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
type UpsertResponse = {
|
|
255
|
+
readonly created: number
|
|
256
|
+
readonly updated: number
|
|
257
|
+
readonly records?: ReadonlyArray<{ readonly fields: Record<string, unknown> }>
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Apply field-level read filtering to upsert response
|
|
262
|
+
*/
|
|
263
|
+
export function applyReadFiltering<E, R>(config: {
|
|
264
|
+
readonly program: Effect.Effect<UpsertResponse, E, R>
|
|
265
|
+
readonly app: App
|
|
266
|
+
readonly tableName: string
|
|
267
|
+
readonly userRole: string
|
|
268
|
+
readonly userId: string
|
|
269
|
+
}): Effect.Effect<UpsertResponse, E, R> {
|
|
270
|
+
const { program, app, tableName, userRole, userId } = config
|
|
271
|
+
|
|
272
|
+
return program.pipe(
|
|
273
|
+
Effect.map((response) => {
|
|
274
|
+
if (!response.records) return response
|
|
275
|
+
|
|
276
|
+
const filteredRecords = response.records.map(
|
|
277
|
+
(record) =>
|
|
278
|
+
({
|
|
279
|
+
...record,
|
|
280
|
+
fields: filterReadableFields({
|
|
281
|
+
app,
|
|
282
|
+
tableName,
|
|
283
|
+
userRole,
|
|
284
|
+
userId,
|
|
285
|
+
record: record.fields,
|
|
286
|
+
}),
|
|
287
|
+
}) as { readonly fields: Record<string, unknown> }
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
created: response.created,
|
|
292
|
+
updated: response.updated,
|
|
293
|
+
records: filteredRecords as ReadonlyArray<{ readonly fields: Record<string, unknown> }>,
|
|
294
|
+
}
|
|
295
|
+
})
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Create 403 response for protected field write attempt
|
|
301
|
+
*/
|
|
302
|
+
function createForbiddenFieldResponse(c: Context, forbiddenField: string): Response {
|
|
303
|
+
return c.json(
|
|
304
|
+
{
|
|
305
|
+
success: false,
|
|
306
|
+
message: `Cannot write to field '${forbiddenField}': insufficient permissions`,
|
|
307
|
+
code: 'FORBIDDEN',
|
|
308
|
+
},
|
|
309
|
+
403
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Check if single-record upsert contains protected fields
|
|
315
|
+
* Single-record upserts reject if ANY protected fields present
|
|
316
|
+
*/
|
|
317
|
+
function checkSingleRecordProtectedFields(config: {
|
|
318
|
+
readonly c: Context
|
|
319
|
+
readonly app: App
|
|
320
|
+
readonly tableName: string
|
|
321
|
+
readonly userRole: string
|
|
322
|
+
readonly records: readonly { fields: Record<string, unknown> }[]
|
|
323
|
+
}): { success: true } | { success: false; response: Response } {
|
|
324
|
+
const { c, app, tableName, userRole, records } = config
|
|
325
|
+
|
|
326
|
+
if (records.length !== 1) {
|
|
327
|
+
return { success: true }
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const allForbiddenFields = records
|
|
331
|
+
.map((record) => validateFieldWritePermissions(app, tableName, userRole, record.fields))
|
|
332
|
+
.filter((fields) => fields.length > 0)
|
|
333
|
+
|
|
334
|
+
if (allForbiddenFields.length > 0) {
|
|
335
|
+
const uniqueForbiddenFields = [...new Set(allForbiddenFields.flat())]
|
|
336
|
+
const firstForbiddenField = uniqueForbiddenFields[0]
|
|
337
|
+
return {
|
|
338
|
+
success: false,
|
|
339
|
+
response: createForbiddenFieldResponse(c, firstForbiddenField!),
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return { success: true }
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Check if all fields were stripped from records (user tried to write only protected fields)
|
|
348
|
+
*/
|
|
349
|
+
function checkAllFieldsStripped(config: {
|
|
350
|
+
readonly c: Context
|
|
351
|
+
readonly app: App
|
|
352
|
+
readonly tableName: string
|
|
353
|
+
readonly userRole: string
|
|
354
|
+
readonly records: readonly { fields: Record<string, unknown> }[]
|
|
355
|
+
readonly strippedRecords: ReadonlyArray<{ fields: Record<string, unknown> }>
|
|
356
|
+
}): { success: true } | { success: false; response: Response } {
|
|
357
|
+
const { c, app, tableName, userRole, records, strippedRecords } = config
|
|
358
|
+
|
|
359
|
+
const hasWritableFields = strippedRecords.some((record) => Object.keys(record.fields).length > 0)
|
|
360
|
+
if (hasWritableFields) {
|
|
361
|
+
return { success: true }
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// All fields were stripped
|
|
365
|
+
const allForbiddenFields = records
|
|
366
|
+
.map((record) => validateFieldWritePermissions(app, tableName, userRole, record.fields))
|
|
367
|
+
.filter((fields) => fields.length > 0)
|
|
368
|
+
const uniqueForbiddenFields = [...new Set(allForbiddenFields.flat())]
|
|
369
|
+
const firstForbiddenField = uniqueForbiddenFields[0]
|
|
370
|
+
|
|
371
|
+
return {
|
|
372
|
+
success: false,
|
|
373
|
+
response: createForbiddenFieldResponse(c, firstForbiddenField!),
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Check required field validation
|
|
379
|
+
*/
|
|
380
|
+
async function checkRequiredFields(
|
|
381
|
+
table: NonNullable<App['tables']>[number] | undefined,
|
|
382
|
+
strippedRecords: ReadonlyArray<{ fields: Record<string, unknown> }>,
|
|
383
|
+
c: Context
|
|
384
|
+
): Promise<{ success: true } | { success: false; response: Response }> {
|
|
385
|
+
const validationErrors = await validateUpsertRequiredFields(table, strippedRecords)
|
|
386
|
+
if (validationErrors.length === 0) {
|
|
387
|
+
return { success: true }
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
success: false,
|
|
392
|
+
response: c.json(
|
|
393
|
+
{
|
|
394
|
+
success: false,
|
|
395
|
+
message: 'Validation failed: one or more records have invalid data',
|
|
396
|
+
code: 'VALIDATION_ERROR',
|
|
397
|
+
details: validationErrors,
|
|
398
|
+
},
|
|
399
|
+
400
|
|
400
|
+
),
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Validate upsert request (permissions and required fields)
|
|
406
|
+
*
|
|
407
|
+
* Upsert behavior for protected fields:
|
|
408
|
+
* - Single-record upserts: Reject with 403 if ANY protected fields present
|
|
409
|
+
* - Multi-record upserts (batch): Strip protected fields, succeed if any writable fields remain
|
|
410
|
+
* - Filter protected fields from response
|
|
411
|
+
*/
|
|
412
|
+
export async function validateUpsertRequest(config: {
|
|
413
|
+
readonly c: Context
|
|
414
|
+
readonly app: App
|
|
415
|
+
readonly tableName: string
|
|
416
|
+
readonly userRole: string
|
|
417
|
+
readonly records: readonly { fields: Record<string, unknown> }[]
|
|
418
|
+
readonly fieldsToMergeOn: readonly string[]
|
|
419
|
+
}) {
|
|
420
|
+
const { c, app, tableName, userRole, records, fieldsToMergeOn } = config
|
|
421
|
+
const table = app.tables?.find((t) => t.name === tableName)
|
|
422
|
+
|
|
423
|
+
// Single-record upsert: reject if ANY protected fields present
|
|
424
|
+
const singleRecordCheck = checkSingleRecordProtectedFields({
|
|
425
|
+
c,
|
|
426
|
+
app,
|
|
427
|
+
tableName,
|
|
428
|
+
userRole,
|
|
429
|
+
records,
|
|
430
|
+
})
|
|
431
|
+
if (!singleRecordCheck.success) {
|
|
432
|
+
return singleRecordCheck
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// For multi-record upserts, strip unwritable fields
|
|
436
|
+
const strippedRecords = stripUnwritableFields(app, tableName, userRole, records)
|
|
437
|
+
|
|
438
|
+
// Check if all fields were stripped
|
|
439
|
+
const stripCheck = checkAllFieldsStripped({
|
|
440
|
+
c,
|
|
441
|
+
app,
|
|
442
|
+
tableName,
|
|
443
|
+
userRole,
|
|
444
|
+
records,
|
|
445
|
+
strippedRecords,
|
|
446
|
+
})
|
|
447
|
+
if (!stripCheck.success) {
|
|
448
|
+
return stripCheck
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Check table-level permissions (create/update)
|
|
452
|
+
const permissionCheck = await checkUpsertPermissionsWithUpdateCheck({
|
|
453
|
+
app,
|
|
454
|
+
tableName,
|
|
455
|
+
userRole,
|
|
456
|
+
records: strippedRecords,
|
|
457
|
+
fieldsToMergeOn,
|
|
458
|
+
c,
|
|
459
|
+
})
|
|
460
|
+
if (!permissionCheck.allowed) {
|
|
461
|
+
return { success: false as const, response: permissionCheck.response }
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Validate required fields
|
|
465
|
+
const requiredCheck = await checkRequiredFields(table, strippedRecords, c)
|
|
466
|
+
if (!requiredCheck.success) {
|
|
467
|
+
return { success: false as const, response: requiredCheck.response }
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return { success: true as const, strippedRecords }
|
|
471
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
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 { getSessionContext } from '@/presentation/api/utils/context-helpers'
|
|
9
|
+
import type { Session } from '@/application/ports/models/user-session'
|
|
10
|
+
import type { App } from '@/domain/models/app'
|
|
11
|
+
import type { Context } from 'hono'
|
|
12
|
+
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Type Re-exports
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Re-export Session type for sibling files in this directory
|
|
19
|
+
*/
|
|
20
|
+
export type { Session }
|
|
21
|
+
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// Constants
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
const AUTH_KEYWORDS = ['not found', 'access denied'] as const
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// Table ID Resolution
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get table name from tableId parameter
|
|
34
|
+
*
|
|
35
|
+
* Looks up the table in the app schema by either:
|
|
36
|
+
* - Table ID (numeric or string match)
|
|
37
|
+
* - Table name (exact match)
|
|
38
|
+
*
|
|
39
|
+
* @param app - Application configuration containing tables
|
|
40
|
+
* @param tableId - Table identifier from route parameter
|
|
41
|
+
* @returns Table name if found, undefined otherwise
|
|
42
|
+
*/
|
|
43
|
+
export const getTableNameFromId = (app: App, tableId: string): string | undefined => {
|
|
44
|
+
const table = app.tables?.find((t) => String(t.id) === tableId || t.name === tableId)
|
|
45
|
+
return table?.name
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Check if a string contains authorization-related keywords
|
|
50
|
+
*/
|
|
51
|
+
const containsAuthKeywords = (text: string): boolean =>
|
|
52
|
+
AUTH_KEYWORDS.some((keyword) => text.includes(keyword))
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Extract error details from an error object
|
|
56
|
+
* Centralizes error information extraction logic
|
|
57
|
+
*/
|
|
58
|
+
const extractErrorDetails = (
|
|
59
|
+
error: unknown
|
|
60
|
+
): { message: string; name: string; causeMessage: string; errorString: string } => {
|
|
61
|
+
const errorMessage = error instanceof Error ? error.message : ''
|
|
62
|
+
const errorName = error instanceof Error ? error.name : ''
|
|
63
|
+
const errorString = String(error)
|
|
64
|
+
const causeMessage =
|
|
65
|
+
error instanceof Error && 'cause' in error && error.cause instanceof Error
|
|
66
|
+
? error.cause.message
|
|
67
|
+
: ''
|
|
68
|
+
|
|
69
|
+
return { message: errorMessage, name: errorName, causeMessage, errorString }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Check if error is an authorization error (SessionContextError or access denied)
|
|
74
|
+
*
|
|
75
|
+
* @param error - The error to check
|
|
76
|
+
* @returns true if error is authorization-related (should return 404 instead of 500)
|
|
77
|
+
*/
|
|
78
|
+
export const isAuthorizationError = (error: unknown): boolean => {
|
|
79
|
+
const { message, name, causeMessage, errorString } = extractErrorDetails(error)
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
containsAuthKeywords(message) ||
|
|
83
|
+
containsAuthKeywords(causeMessage) ||
|
|
84
|
+
name.includes('SessionContextError') ||
|
|
85
|
+
errorString.includes('SessionContextError')
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Handle batch restore errors with appropriate HTTP responses
|
|
91
|
+
*/
|
|
92
|
+
export const handleBatchRestoreError = (c: Context, error: unknown) => {
|
|
93
|
+
// Handle ForbiddenError (viewer role attempting write operation)
|
|
94
|
+
// Use name check to handle multiple import paths resolving to different class instances
|
|
95
|
+
if (error instanceof Error && error.name === 'ForbiddenError') {
|
|
96
|
+
return c.json(
|
|
97
|
+
{
|
|
98
|
+
error: 'Forbidden',
|
|
99
|
+
message: error.message,
|
|
100
|
+
},
|
|
101
|
+
403
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
|
106
|
+
|
|
107
|
+
// Handle "Record X not found" errors (404)
|
|
108
|
+
if (errorMessage.includes('not found')) {
|
|
109
|
+
const recordIdMatch = errorMessage.match(/Record (\S+) not found/)
|
|
110
|
+
const recordId = recordIdMatch?.[1] ? Number.parseInt(recordIdMatch[1]) : undefined
|
|
111
|
+
return c.json(
|
|
112
|
+
{
|
|
113
|
+
success: false,
|
|
114
|
+
message: 'Resource not found',
|
|
115
|
+
code: 'NOT_FOUND',
|
|
116
|
+
recordId,
|
|
117
|
+
},
|
|
118
|
+
404
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Handle "Record X is not deleted" errors (400)
|
|
123
|
+
if (errorMessage.includes('is not deleted')) {
|
|
124
|
+
const recordIdMatch = errorMessage.match(/Record (\S+) is not deleted/)
|
|
125
|
+
const recordId = recordIdMatch?.[1] ? Number.parseInt(recordIdMatch[1]) : undefined
|
|
126
|
+
return c.json(
|
|
127
|
+
{
|
|
128
|
+
error: 'Bad Request',
|
|
129
|
+
message: 'Record is not deleted',
|
|
130
|
+
recordId,
|
|
131
|
+
},
|
|
132
|
+
400
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return c.json({ error: 'Internal server error', message: errorMessage }, 500)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ============================================================================
|
|
140
|
+
// Validation Helper Functions
|
|
141
|
+
// ============================================================================
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Extract session from Hono context
|
|
145
|
+
* Returns undefined if no session exists
|
|
146
|
+
*
|
|
147
|
+
* @deprecated Use getSessionContext from @/presentation/api/utils/context-helpers instead
|
|
148
|
+
*/
|
|
149
|
+
export const getSessionFromContext = (c: Context): Readonly<Session> | undefined => {
|
|
150
|
+
return getSessionContext(c)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Validate table exists and return table name
|
|
155
|
+
* Returns undefined if table not found
|
|
156
|
+
*/
|
|
157
|
+
export const validateAndGetTableName = (app: App, tableId: string): string | undefined => {
|
|
158
|
+
return getTableNameFromId(app, tableId)
|
|
159
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
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 { Effect } from 'effect'
|
|
9
|
+
import {
|
|
10
|
+
listViewsProgram,
|
|
11
|
+
getViewProgram,
|
|
12
|
+
getViewRecordsProgram,
|
|
13
|
+
} from '@/application/use-cases/tables/programs'
|
|
14
|
+
import { getViewResponseSchema, getViewRecordsResponseSchema } from '@/domain/models/api/tables'
|
|
15
|
+
import { runEffect } from '@/presentation/api/utils'
|
|
16
|
+
import { getTableContext } from '@/presentation/api/utils/context-helpers'
|
|
17
|
+
import { provideTableLive } from './effect-runner'
|
|
18
|
+
import type { App } from '@/domain/models/app'
|
|
19
|
+
import type { Hono } from 'hono'
|
|
20
|
+
|
|
21
|
+
export function chainViewRoutesMethods<T extends Hono>(honoApp: T, app: App) {
|
|
22
|
+
return honoApp
|
|
23
|
+
.get('/api/tables/:tableId/views', async (c) => {
|
|
24
|
+
// Session, tableId, and userRole are guaranteed by middleware chain
|
|
25
|
+
const { tableId, userRole } = getTableContext(c)
|
|
26
|
+
|
|
27
|
+
const program = Effect.gen(function* () {
|
|
28
|
+
const result = yield* listViewsProgram(tableId, app, userRole)
|
|
29
|
+
// Return the views array directly (unwrapped) to match test expectations
|
|
30
|
+
// No schema validation - test expects minimal view objects without timestamps
|
|
31
|
+
return result
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
return runEffect(c, program)
|
|
35
|
+
})
|
|
36
|
+
.get('/api/tables/:tableId/views/:viewId', async (c) =>
|
|
37
|
+
runEffect(
|
|
38
|
+
c,
|
|
39
|
+
getViewProgram(c.req.param('tableId'), c.req.param('viewId'), app),
|
|
40
|
+
getViewResponseSchema
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
.get('/api/tables/:tableId/views/:viewId/records', async (c) => {
|
|
44
|
+
const { session, tableId, userRole } = getTableContext(c)
|
|
45
|
+
const viewId = c.req.param('viewId')
|
|
46
|
+
|
|
47
|
+
const program = getViewRecordsProgram({ tableId, viewId, app, userRole, session })
|
|
48
|
+
|
|
49
|
+
return runEffect(c, provideTableLive(program), getViewRecordsResponseSchema)
|
|
50
|
+
})
|
|
51
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
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 from modular structure
|
|
9
|
+
export { chainTableRoutes } from './tables/'
|