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,440 @@
|
|
|
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
|
+
/**
|
|
9
|
+
* Reserved SQL keywords that require escaping when used in identifiers
|
|
10
|
+
* Based on PostgreSQL reserved keywords list
|
|
11
|
+
* @see https://www.postgresql.org/docs/current/sql-keywords-appendix.html
|
|
12
|
+
*/
|
|
13
|
+
const SQL_RESERVED_KEYWORDS = new Set([
|
|
14
|
+
'select',
|
|
15
|
+
'insert',
|
|
16
|
+
'update',
|
|
17
|
+
'delete',
|
|
18
|
+
'from',
|
|
19
|
+
'where',
|
|
20
|
+
'join',
|
|
21
|
+
'inner',
|
|
22
|
+
'outer',
|
|
23
|
+
'left',
|
|
24
|
+
'right',
|
|
25
|
+
'full',
|
|
26
|
+
'cross',
|
|
27
|
+
'on',
|
|
28
|
+
'as',
|
|
29
|
+
'table',
|
|
30
|
+
'create',
|
|
31
|
+
'alter',
|
|
32
|
+
'drop',
|
|
33
|
+
'truncate',
|
|
34
|
+
'add',
|
|
35
|
+
'column',
|
|
36
|
+
'constraint',
|
|
37
|
+
'primary',
|
|
38
|
+
'foreign',
|
|
39
|
+
'key',
|
|
40
|
+
'references',
|
|
41
|
+
'unique',
|
|
42
|
+
'index',
|
|
43
|
+
'view',
|
|
44
|
+
'database',
|
|
45
|
+
'schema',
|
|
46
|
+
'grant',
|
|
47
|
+
'revoke',
|
|
48
|
+
'transaction',
|
|
49
|
+
'commit',
|
|
50
|
+
'rollback',
|
|
51
|
+
'union',
|
|
52
|
+
'intersect',
|
|
53
|
+
'except',
|
|
54
|
+
'group',
|
|
55
|
+
'having',
|
|
56
|
+
'order',
|
|
57
|
+
'limit',
|
|
58
|
+
'offset',
|
|
59
|
+
'distinct',
|
|
60
|
+
'all',
|
|
61
|
+
'any',
|
|
62
|
+
'some',
|
|
63
|
+
'exists',
|
|
64
|
+
'in',
|
|
65
|
+
'between',
|
|
66
|
+
'like',
|
|
67
|
+
'ilike',
|
|
68
|
+
'and',
|
|
69
|
+
'or',
|
|
70
|
+
'not',
|
|
71
|
+
'null',
|
|
72
|
+
'is',
|
|
73
|
+
'true',
|
|
74
|
+
'false',
|
|
75
|
+
'case',
|
|
76
|
+
'when',
|
|
77
|
+
'then',
|
|
78
|
+
'else',
|
|
79
|
+
'end',
|
|
80
|
+
'cast',
|
|
81
|
+
'default',
|
|
82
|
+
'check',
|
|
83
|
+
'user',
|
|
84
|
+
'current_user',
|
|
85
|
+
'session_user',
|
|
86
|
+
'current_date',
|
|
87
|
+
'current_time',
|
|
88
|
+
'current_timestamp',
|
|
89
|
+
])
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Check if an identifier needs escaping due to reserved words
|
|
93
|
+
* Split identifier by underscores and check if any token is a reserved word
|
|
94
|
+
* Examples:
|
|
95
|
+
* - "order" → needs escaping (is reserved word)
|
|
96
|
+
* - "order_num" → needs escaping (token "order" is reserved)
|
|
97
|
+
* - "created_at" → no escaping ("created" and "at" on their own are not problematic)
|
|
98
|
+
* - "user_id" → needs escaping (token "user" is reserved)
|
|
99
|
+
* - "select" → needs escaping (is reserved word)
|
|
100
|
+
*/
|
|
101
|
+
const containsReservedWord = (identifier: string): boolean => {
|
|
102
|
+
const lowerIdentifier = identifier.toLowerCase()
|
|
103
|
+
// Check if the identifier itself is a reserved word
|
|
104
|
+
if (SQL_RESERVED_KEYWORDS.has(lowerIdentifier)) {
|
|
105
|
+
return true
|
|
106
|
+
}
|
|
107
|
+
// Split by underscores and check each token
|
|
108
|
+
const tokens = lowerIdentifier.split('_')
|
|
109
|
+
return tokens.some((token) => SQL_RESERVED_KEYWORDS.has(token))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Escape a field name for use in SQL if it contains reserved words
|
|
114
|
+
* PostgreSQL uses double quotes for identifier escaping
|
|
115
|
+
*/
|
|
116
|
+
const escapeFieldName = (fieldName: string): string =>
|
|
117
|
+
containsReservedWord(fieldName) ? `"${fieldName}"` : fieldName
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Volatile SQL functions that cannot be used in GENERATED ALWAYS AS columns
|
|
121
|
+
* These functions return different values on each call or depend on external state
|
|
122
|
+
*
|
|
123
|
+
* NOTE: TO_CHAR is marked as STABLE (not IMMUTABLE) in PostgreSQL because its output
|
|
124
|
+
* can vary based on LC_TIME locale settings, making it unsuitable for GENERATED columns.
|
|
125
|
+
* Formulas using TO_CHAR will use trigger-based computation instead.
|
|
126
|
+
*/
|
|
127
|
+
const volatileSQLFunctions = [
|
|
128
|
+
'CURRENT_DATE',
|
|
129
|
+
'CURRENT_TIME',
|
|
130
|
+
'CURRENT_TIMESTAMP',
|
|
131
|
+
'NOW()',
|
|
132
|
+
'TIMEOFDAY()',
|
|
133
|
+
'TRANSACTION_TIMESTAMP()',
|
|
134
|
+
'STATEMENT_TIMESTAMP()',
|
|
135
|
+
'CLOCK_TIMESTAMP()',
|
|
136
|
+
'RANDOM()',
|
|
137
|
+
'SETSEED(',
|
|
138
|
+
'DECODE(',
|
|
139
|
+
'CONVERT_FROM(',
|
|
140
|
+
'TO_CHAR(',
|
|
141
|
+
'TO_DATE(',
|
|
142
|
+
'DATE_TRUNC(',
|
|
143
|
+
'ARRAY_TO_STRING(',
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Type casts that make expressions non-immutable in PostgreSQL
|
|
148
|
+
* Casts to TIMESTAMP types depend on locale settings (DateStyle, TimeZone)
|
|
149
|
+
* making them volatile even when used with immutable functions like EXTRACT
|
|
150
|
+
*/
|
|
151
|
+
const volatileTypeCasts = ['::TIMESTAMP', '::TIMESTAMPTZ', '::DATE', '::TIME']
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Check if formula contains volatile functions that make it non-immutable
|
|
155
|
+
* PostgreSQL GENERATED ALWAYS AS columns must be immutable (deterministic)
|
|
156
|
+
*
|
|
157
|
+
* Special timestamp fields (created_at, updated_at) are set by triggers, so formulas
|
|
158
|
+
* referencing them must use trigger-based computation, not GENERATED columns
|
|
159
|
+
*/
|
|
160
|
+
export const isFormulaVolatile = (formula: string): boolean => {
|
|
161
|
+
const upperFormula = formula.toUpperCase()
|
|
162
|
+
return (
|
|
163
|
+
volatileSQLFunctions.some((fn) => upperFormula.includes(fn)) ||
|
|
164
|
+
volatileTypeCasts.some((cast) => upperFormula.includes(cast)) ||
|
|
165
|
+
upperFormula.includes('CREATED_AT') ||
|
|
166
|
+
upperFormula.includes('UPDATED_AT')
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* SQL functions that return array types
|
|
172
|
+
* Used to automatically adjust column type when formula returns an array
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* STRING_TO_ARRAY('a,b,c', ',') → ['a', 'b', 'c'] (TEXT[])
|
|
176
|
+
*/
|
|
177
|
+
const arrayReturningFunctions = ['STRING_TO_ARRAY']
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Check if formula returns an array type
|
|
181
|
+
* Some PostgreSQL functions return arrays regardless of input type
|
|
182
|
+
* NOTE: ARRAY_TO_STRING wraps an array and returns text, so check for it first
|
|
183
|
+
* NOTE: CARDINALITY wraps an array and returns integer, so check for it too
|
|
184
|
+
*/
|
|
185
|
+
export const isFormulaReturningArray = (formula: string): boolean => {
|
|
186
|
+
const upperFormula = formula.toUpperCase().trim()
|
|
187
|
+
|
|
188
|
+
// If formula starts with ARRAY_TO_STRING, the result is text, not array
|
|
189
|
+
if (upperFormula.startsWith('ARRAY_TO_STRING(')) {
|
|
190
|
+
return false
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// If formula starts with CARDINALITY, the result is integer, not array
|
|
194
|
+
if (upperFormula.startsWith('CARDINALITY(')) {
|
|
195
|
+
return false
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return arrayReturningFunctions.some((fn) => upperFormula.includes(fn))
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Parse ROUND function arguments, handling nested parentheses
|
|
203
|
+
* Returns {firstArg, secondArg, start, end} or undefined if not a valid ROUND call
|
|
204
|
+
*/
|
|
205
|
+
const parseRoundArgs = (
|
|
206
|
+
formula: string,
|
|
207
|
+
matchIndex: number,
|
|
208
|
+
matchLength: number
|
|
209
|
+
):
|
|
210
|
+
| {
|
|
211
|
+
readonly firstArg: string
|
|
212
|
+
readonly secondArg: string
|
|
213
|
+
readonly start: number
|
|
214
|
+
readonly end: number
|
|
215
|
+
}
|
|
216
|
+
| undefined => {
|
|
217
|
+
const argsStart = matchIndex + matchLength
|
|
218
|
+
const chars = [...formula.slice(argsStart)]
|
|
219
|
+
|
|
220
|
+
// Track state through the argument parsing
|
|
221
|
+
const state = chars.reduce<{
|
|
222
|
+
depth: number
|
|
223
|
+
firstArgEnd: number
|
|
224
|
+
currentIndex: number
|
|
225
|
+
done: boolean
|
|
226
|
+
}>(
|
|
227
|
+
(acc, char, idx) => {
|
|
228
|
+
if (acc.done) return acc
|
|
229
|
+
|
|
230
|
+
const absoluteIndex = argsStart + idx
|
|
231
|
+
|
|
232
|
+
if (char === '(') {
|
|
233
|
+
return { ...acc, depth: acc.depth + 1, currentIndex: absoluteIndex }
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (char === ')') {
|
|
237
|
+
const newDepth = acc.depth - 1
|
|
238
|
+
if (newDepth === 0) {
|
|
239
|
+
return { ...acc, depth: newDepth, currentIndex: absoluteIndex, done: true }
|
|
240
|
+
}
|
|
241
|
+
return { ...acc, depth: newDepth, currentIndex: absoluteIndex }
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (char === ',' && acc.depth === 1 && acc.firstArgEnd === -1) {
|
|
245
|
+
return { ...acc, firstArgEnd: absoluteIndex, currentIndex: absoluteIndex }
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return { ...acc, currentIndex: absoluteIndex }
|
|
249
|
+
},
|
|
250
|
+
{ depth: 1, firstArgEnd: -1, currentIndex: argsStart, done: false }
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
if (state.firstArgEnd === -1) return undefined
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
firstArg: formula.slice(argsStart, state.firstArgEnd).trim(),
|
|
257
|
+
secondArg: formula.slice(state.firstArgEnd + 1, state.currentIndex).trim(),
|
|
258
|
+
start: matchIndex,
|
|
259
|
+
end: state.currentIndex + 1,
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Translate date/datetime/time field casts to TEXT using TO_CHAR
|
|
265
|
+
* DATE::TEXT depends on DateStyle (volatile), but TO_CHAR with format is immutable
|
|
266
|
+
*/
|
|
267
|
+
const translateDateCastsToText = (
|
|
268
|
+
formula: string,
|
|
269
|
+
allFields?: readonly { name: string; type: string }[]
|
|
270
|
+
): string => {
|
|
271
|
+
if (!allFields) return formula
|
|
272
|
+
|
|
273
|
+
return formula.replace(/(\w+)::TEXT/gi, (match, fieldName) => {
|
|
274
|
+
const field = allFields.find((f) => f.name.toLowerCase() === fieldName.toLowerCase())
|
|
275
|
+
if (field && (field.type === 'date' || field.type === 'datetime' || field.type === 'time')) {
|
|
276
|
+
// Use appropriate format based on field type
|
|
277
|
+
if (field.type === 'date') {
|
|
278
|
+
return `TO_CHAR(${escapeFieldName(fieldName)}, 'YYYY-MM-DD')`
|
|
279
|
+
}
|
|
280
|
+
if (field.type === 'datetime') {
|
|
281
|
+
return `TO_CHAR(${escapeFieldName(fieldName)}, 'YYYY-MM-DD"T"HH24:MI:SS"Z"')`
|
|
282
|
+
}
|
|
283
|
+
if (field.type === 'time') {
|
|
284
|
+
return `TO_CHAR(${escapeFieldName(fieldName)}, 'HH24:MI:SS')`
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return match // Keep original for non-date fields (e.g., num::TEXT)
|
|
288
|
+
})
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Translate SUBSTR to PostgreSQL SUBSTRING syntax
|
|
293
|
+
* SUBSTR(text, start, length) → SUBSTRING(text FROM start FOR length)
|
|
294
|
+
*/
|
|
295
|
+
const translateSubstrToSubstring = (formula: string): string =>
|
|
296
|
+
formula.replace(
|
|
297
|
+
/SUBSTR\s*\(\s*([^,]+?)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/gi,
|
|
298
|
+
(_, text, start, length) => {
|
|
299
|
+
const trimmedText = text.trim()
|
|
300
|
+
// Only escape if it's a field name (not already a function call or quoted)
|
|
301
|
+
const escapedText = trimmedText.match(/^\w+$/) ? escapeFieldName(trimmedText) : trimmedText
|
|
302
|
+
return `SUBSTRING(${escapedText} FROM ${start} FOR ${length})`
|
|
303
|
+
}
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Add NUMERIC casts to ROUND functions that use double precision functions
|
|
308
|
+
* PostgreSQL's ROUND(numeric, integer) exists but ROUND(double precision, integer) does not
|
|
309
|
+
*/
|
|
310
|
+
const addNumericCastsToRound = (formula: string): string => {
|
|
311
|
+
// Find all ROUND( occurrences and build replacement list
|
|
312
|
+
const roundMatches = [...formula.matchAll(/ROUND\s*\(/gi)]
|
|
313
|
+
const replacements = roundMatches
|
|
314
|
+
.map((match) => parseRoundArgs(formula, match.index ?? 0, match[0].length))
|
|
315
|
+
.filter((parsed): parsed is NonNullable<typeof parsed> => parsed !== undefined)
|
|
316
|
+
.filter((parsed) => /SQRT|POWER|EXP|LN|LOG/i.test(parsed.firstArg))
|
|
317
|
+
.map((parsed) => ({
|
|
318
|
+
start: parsed.start,
|
|
319
|
+
end: parsed.end,
|
|
320
|
+
replacement: `ROUND((${parsed.firstArg})::NUMERIC, ${parsed.secondArg})`,
|
|
321
|
+
}))
|
|
322
|
+
|
|
323
|
+
// Apply replacements in reverse order to maintain indices
|
|
324
|
+
const sortedReplacements = replacements.toSorted((a, b) => b.start - a.start)
|
|
325
|
+
|
|
326
|
+
return sortedReplacements.reduce(
|
|
327
|
+
(result, { start, end, replacement }) =>
|
|
328
|
+
result.slice(0, start) + replacement + result.slice(end),
|
|
329
|
+
formula
|
|
330
|
+
)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Escape field names that contain reserved words
|
|
335
|
+
* This handles references like "order_num * 2" → "\"order_num\" * 2"
|
|
336
|
+
*/
|
|
337
|
+
const escapeReservedFieldNames = (
|
|
338
|
+
formula: string,
|
|
339
|
+
allFields?: readonly { name: string; type: string }[]
|
|
340
|
+
): string => {
|
|
341
|
+
if (!allFields) return formula
|
|
342
|
+
|
|
343
|
+
return allFields.reduce((acc, field) => {
|
|
344
|
+
// Only escape this field if it needs escaping
|
|
345
|
+
if (!containsReservedWord(field.name)) {
|
|
346
|
+
return acc
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Create a regex that matches the field name as a whole word
|
|
350
|
+
// Use word boundaries (\b) to ensure we only match complete field names
|
|
351
|
+
// Use negative lookbehind to avoid matching if already quoted
|
|
352
|
+
const fieldRegex = new RegExp(`(?<!["'])\\b${field.name}\\b(?!["'])`, 'gi')
|
|
353
|
+
return acc.replace(fieldRegex, (match) => escapeFieldName(match))
|
|
354
|
+
}, formula)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Translate formula from user-friendly syntax to PostgreSQL syntax
|
|
359
|
+
* Converts SUBSTR(text, start, length) to SUBSTRING(text FROM start FOR length)
|
|
360
|
+
* Converts date_field::TEXT to TO_CHAR(date_field, 'YYYY-MM-DD') for immutability
|
|
361
|
+
* Casts ROUND arguments to NUMERIC when input may be double precision
|
|
362
|
+
* Escapes field names that contain reserved words (e.g., order_num → "order_num")
|
|
363
|
+
*
|
|
364
|
+
* NOTE: PostgreSQL natively supports nested function calls like ROUND(SQRT(ABS(value)), 2)
|
|
365
|
+
* and all standard mathematical functions (ABS, SQRT, ROUND, POWER, etc.), but ROUND only
|
|
366
|
+
* accepts NUMERIC as first argument, not double precision. Functions like SQRT, POWER, EXP, LN
|
|
367
|
+
* return double precision, so we cast them to NUMERIC before passing to ROUND.
|
|
368
|
+
*/
|
|
369
|
+
export const translateFormulaToPostgres = (
|
|
370
|
+
formula: string,
|
|
371
|
+
allFields?: readonly { name: string; type: string }[]
|
|
372
|
+
): string => {
|
|
373
|
+
const withDateToText = translateDateCastsToText(formula, allFields)
|
|
374
|
+
const withSubstring = translateSubstrToSubstring(withDateToText)
|
|
375
|
+
const withRoundCast = addNumericCastsToRound(withSubstring)
|
|
376
|
+
return escapeReservedFieldNames(withRoundCast, allFields)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* EXTRACT date/time field keywords that should not be qualified
|
|
381
|
+
* These are valid arguments to EXTRACT() function and not column references
|
|
382
|
+
*/
|
|
383
|
+
const EXTRACT_KEYWORDS = new Set([
|
|
384
|
+
'year',
|
|
385
|
+
'month',
|
|
386
|
+
'day',
|
|
387
|
+
'hour',
|
|
388
|
+
'minute',
|
|
389
|
+
'second',
|
|
390
|
+
'dow',
|
|
391
|
+
'doy',
|
|
392
|
+
'week',
|
|
393
|
+
'quarter',
|
|
394
|
+
'decade',
|
|
395
|
+
'century',
|
|
396
|
+
'millennium',
|
|
397
|
+
'epoch',
|
|
398
|
+
'timezone',
|
|
399
|
+
'timezone_hour',
|
|
400
|
+
'timezone_minute',
|
|
401
|
+
])
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Qualify column references in a formula with a prefix
|
|
405
|
+
* Used by trigger functions to reference columns from subqueries or NEW/OLD records
|
|
406
|
+
*
|
|
407
|
+
* @example
|
|
408
|
+
* qualifyColumnReferences('NOT paid AND due_date < CURRENT_DATE', fields, 't')
|
|
409
|
+
* // Returns: 'NOT t.paid AND t.due_date < CURRENT_DATE'
|
|
410
|
+
*
|
|
411
|
+
* @example
|
|
412
|
+
* qualifyColumnReferences('EXTRACT(HOUR FROM timestamp_value::TIMESTAMP)', fields, 't')
|
|
413
|
+
* // Returns: 'EXTRACT(HOUR FROM t.timestamp_value::TIMESTAMP)' (HOUR not qualified)
|
|
414
|
+
*/
|
|
415
|
+
export const qualifyColumnReferences = (
|
|
416
|
+
formula: string,
|
|
417
|
+
allFields: readonly { name: string; type: string }[],
|
|
418
|
+
prefix: string
|
|
419
|
+
): string =>
|
|
420
|
+
allFields.reduce((acc, field) => {
|
|
421
|
+
// Don't qualify EXTRACT keywords (e.g., HOUR, MINUTE, DAY)
|
|
422
|
+
if (EXTRACT_KEYWORDS.has(field.name.toLowerCase())) {
|
|
423
|
+
// Check if this field name appears as an EXTRACT keyword
|
|
424
|
+
const extractPattern = new RegExp(`\\bEXTRACT\\s*\\(\\s*${field.name}\\s+FROM\\b`, 'gi')
|
|
425
|
+
if (extractPattern.test(acc)) {
|
|
426
|
+
// This is an EXTRACT keyword, only qualify non-keyword occurrences
|
|
427
|
+
// Match the field name but exclude it when preceded by EXTRACT(
|
|
428
|
+
const fieldRegex = new RegExp(
|
|
429
|
+
`(?<!EXTRACT\\s{0,10}\\(\\s{0,10})(?<![."])\\b${field.name}\\b(?!["']|\\s+FROM)`,
|
|
430
|
+
'gi'
|
|
431
|
+
)
|
|
432
|
+
return acc.replace(fieldRegex, `${prefix}.${field.name}`)
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Create regex that matches field name as a whole word
|
|
437
|
+
// Use word boundaries (\b) and negative lookbehind for dots (avoid double-qualifying)
|
|
438
|
+
const fieldRegex = new RegExp(`(?<![."])\\b${field.name}\\b(?!["'])`, 'gi')
|
|
439
|
+
return acc.replace(fieldRegex, `${prefix}.${field.name}`)
|
|
440
|
+
}, formula)
|
|
@@ -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 { sanitizeTableName } from '../field-utils'
|
|
9
|
+
import { isRelationshipField, isUserField } from '../sql/sql-generators'
|
|
10
|
+
import type { Table } from '@/domain/models/app/table'
|
|
11
|
+
import type { Fields } from '@/domain/models/app/table/fields'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Generate standard indexes for indexed fields
|
|
15
|
+
*/
|
|
16
|
+
const generateStandardIndexes = (table: Table): readonly string[] => {
|
|
17
|
+
const sanitized = sanitizeTableName(table.name)
|
|
18
|
+
return table.fields
|
|
19
|
+
.filter(
|
|
20
|
+
(field): field is Fields[number] & { indexed: true } => 'indexed' in field && !!field.indexed
|
|
21
|
+
)
|
|
22
|
+
.map((field) => {
|
|
23
|
+
// Status fields use special naming: idx_{table}_status instead of idx_{table}_{field_name}
|
|
24
|
+
const indexSuffix = field.type === 'status' ? 'status' : field.name
|
|
25
|
+
const indexName = `idx_${sanitized}_${indexSuffix}`
|
|
26
|
+
const indexType =
|
|
27
|
+
field.type === 'array' || field.type === 'json'
|
|
28
|
+
? 'USING gin'
|
|
29
|
+
: field.type === 'geolocation'
|
|
30
|
+
? 'USING gist'
|
|
31
|
+
: 'USING btree'
|
|
32
|
+
return `CREATE INDEX IF NOT EXISTS ${indexName} ON public.${sanitized} ${indexType} (${field.name})`
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Generate unique indexes for autonumber fields
|
|
38
|
+
*/
|
|
39
|
+
const generateAutonumberIndexes = (table: Table): readonly string[] => {
|
|
40
|
+
const sanitized = sanitizeTableName(table.name)
|
|
41
|
+
return table.fields
|
|
42
|
+
.filter((field) => field.type === 'autonumber')
|
|
43
|
+
.map((field) => {
|
|
44
|
+
const indexName = `idx_${sanitized}_${field.name}_unique`
|
|
45
|
+
return `CREATE UNIQUE INDEX IF NOT EXISTS ${indexName} ON public.${sanitized} (${field.name})`
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Generate exclusion constraints for geolocation fields with unique constraint
|
|
51
|
+
* NOTE: POINT type doesn't support btree UNIQUE constraints or GiST UNIQUE indexes
|
|
52
|
+
* PostgreSQL requires EXCLUDE USING gist for uniqueness on geometric types using ~= operator
|
|
53
|
+
*/
|
|
54
|
+
const generateGeolocationConstraints = (table: Table): readonly string[] => {
|
|
55
|
+
const sanitized = sanitizeTableName(table.name)
|
|
56
|
+
return table.fields
|
|
57
|
+
.filter(
|
|
58
|
+
(field): field is Fields[number] & { type: 'geolocation'; unique: true } =>
|
|
59
|
+
field.type === 'geolocation' && 'unique' in field && !!field.unique
|
|
60
|
+
)
|
|
61
|
+
.map((field) => {
|
|
62
|
+
// Use PostgreSQL naming convention: {table}_{column}_key (matches constraint naming)
|
|
63
|
+
const constraintName = `${sanitized}_${field.name}_key`
|
|
64
|
+
return `ALTER TABLE public.${sanitized} ADD CONSTRAINT ${constraintName} EXCLUDE USING gist (${field.name} WITH ~=)`
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Generate full-text search GIN indexes for rich-text fields
|
|
70
|
+
*/
|
|
71
|
+
const generateFullTextSearchIndexes = (table: Table): readonly string[] => {
|
|
72
|
+
const sanitized = sanitizeTableName(table.name)
|
|
73
|
+
return table.fields
|
|
74
|
+
.filter(
|
|
75
|
+
(field): field is Fields[number] & { type: 'rich-text'; fullTextSearch: true } =>
|
|
76
|
+
field.type === 'rich-text' && 'fullTextSearch' in field && !!field.fullTextSearch
|
|
77
|
+
)
|
|
78
|
+
.map((field) => {
|
|
79
|
+
const indexName = `idx_${sanitized}_${field.name}_fulltext`
|
|
80
|
+
return `CREATE INDEX IF NOT EXISTS ${indexName} ON public.${sanitized} USING gin (to_tsvector('english'::regconfig, ${field.name}))`
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Generate custom indexes from table.indexes configuration
|
|
86
|
+
*/
|
|
87
|
+
const generateCustomIndexes = (table: Table): readonly string[] => {
|
|
88
|
+
const sanitized = sanitizeTableName(table.name)
|
|
89
|
+
return (
|
|
90
|
+
table.indexes?.map((index) => {
|
|
91
|
+
const uniqueClause = index.unique ? 'UNIQUE ' : ''
|
|
92
|
+
const fields = index.fields.join(', ')
|
|
93
|
+
const whereClause = 'where' in index && index.where ? ` WHERE ${index.where}` : ''
|
|
94
|
+
return `CREATE ${uniqueClause}INDEX IF NOT EXISTS ${index.name} ON public.${sanitized} (${fields})${whereClause}`
|
|
95
|
+
}) ?? []
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Generate index for intrinsic deleted_at column (soft-delete optimization)
|
|
101
|
+
* This index improves performance for common soft-delete queries:
|
|
102
|
+
* - WHERE deleted_at IS NULL (active records)
|
|
103
|
+
* - WHERE deleted_at IS NOT NULL (deleted records)
|
|
104
|
+
*/
|
|
105
|
+
const generateDeletedAtIndex = (table: Table): readonly string[] => {
|
|
106
|
+
const sanitized = sanitizeTableName(table.name)
|
|
107
|
+
const indexName = `idx_${sanitized}_deleted_at`
|
|
108
|
+
return [`CREATE INDEX IF NOT EXISTS ${indexName} ON public.${sanitized} USING btree (deleted_at)`]
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Generate indexes for foreign key columns (relationship and user fields)
|
|
113
|
+
* Foreign key columns benefit from indexes for JOIN operations and referential integrity checks
|
|
114
|
+
* This improves query performance when filtering or joining on relationships
|
|
115
|
+
*/
|
|
116
|
+
const generateForeignKeyIndexes = (table: Table): readonly string[] => {
|
|
117
|
+
const sanitized = sanitizeTableName(table.name)
|
|
118
|
+
const relationshipIndexes = table.fields
|
|
119
|
+
.filter(isRelationshipField)
|
|
120
|
+
.filter((field) => {
|
|
121
|
+
// Only create indexes for many-to-one relationships (where FK exists on this table)
|
|
122
|
+
// Exclude one-to-many (FK in related table) and many-to-many (uses junction table)
|
|
123
|
+
return (
|
|
124
|
+
!('relationType' in field) ||
|
|
125
|
+
(field.relationType !== 'one-to-many' && field.relationType !== 'many-to-many')
|
|
126
|
+
)
|
|
127
|
+
})
|
|
128
|
+
.map((field) => {
|
|
129
|
+
const indexName = `idx_${sanitized}_${field.name}_fk`
|
|
130
|
+
return `CREATE INDEX IF NOT EXISTS ${indexName} ON public.${sanitized} USING btree (${field.name})`
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
const userFieldIndexes = table.fields.filter(isUserField).map((field) => {
|
|
134
|
+
const indexName = `idx_${sanitized}_${field.name}_fk`
|
|
135
|
+
return `CREATE INDEX IF NOT EXISTS ${indexName} ON public.${sanitized} USING btree (${field.name})`
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
return [...relationshipIndexes, ...userFieldIndexes]
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Generate CREATE INDEX statements for indexed fields and autonumber fields
|
|
143
|
+
*/
|
|
144
|
+
export const generateIndexStatements = (table: Table): readonly string[] => [
|
|
145
|
+
...generateStandardIndexes(table),
|
|
146
|
+
...generateAutonumberIndexes(table),
|
|
147
|
+
...generateGeolocationConstraints(table),
|
|
148
|
+
...generateFullTextSearchIndexes(table),
|
|
149
|
+
...generateCustomIndexes(table),
|
|
150
|
+
...generateForeignKeyIndexes(table),
|
|
151
|
+
...generateDeletedAtIndex(table),
|
|
152
|
+
]
|