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,38 @@
|
|
|
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 type { UserMetadataWithOptionalImage } from '@/application/ports/models/user-metadata'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Row shape from a LEFT JOIN on the users table
|
|
12
|
+
*
|
|
13
|
+
* Fields are nullable because LEFT JOIN returns NULL when no user matches.
|
|
14
|
+
*/
|
|
15
|
+
export interface UserJoinRow {
|
|
16
|
+
readonly userId: string | null | undefined
|
|
17
|
+
readonly userName: string | null | undefined
|
|
18
|
+
readonly userEmail: string | null | undefined
|
|
19
|
+
readonly userImage: string | null | undefined
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Extract user metadata from a LEFT JOIN row
|
|
24
|
+
*
|
|
25
|
+
* Returns undefined when required fields (userId, userName, userEmail) are missing,
|
|
26
|
+
* which happens when the LEFT JOIN finds no matching user.
|
|
27
|
+
*/
|
|
28
|
+
export function extractUserFromRow(row: UserJoinRow): UserMetadataWithOptionalImage | undefined {
|
|
29
|
+
if (row.userId && row.userName && row.userEmail) {
|
|
30
|
+
return {
|
|
31
|
+
id: row.userId,
|
|
32
|
+
name: row.userName,
|
|
33
|
+
email: row.userEmail,
|
|
34
|
+
image: row.userImage ?? undefined,
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return undefined
|
|
38
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
* Validate a table name to prevent SQL injection
|
|
10
|
+
*
|
|
11
|
+
* PostgreSQL identifiers cannot be fully parameterized, so we validate them.
|
|
12
|
+
* This function ensures table names:
|
|
13
|
+
* - Only contain alphanumeric characters, underscores
|
|
14
|
+
* - Start with a letter or underscore
|
|
15
|
+
* - Are within PostgreSQL's 63-character limit
|
|
16
|
+
*
|
|
17
|
+
* @param tableName - Raw table name from user input
|
|
18
|
+
* @throws Error if table name contains invalid characters
|
|
19
|
+
*/
|
|
20
|
+
export const validateTableName = (tableName: string): void => {
|
|
21
|
+
// PostgreSQL identifier rules: start with letter/underscore, contain alphanumeric/underscore
|
|
22
|
+
// Max 63 characters (PostgreSQL limit)
|
|
23
|
+
const validIdentifier = /^[a-z_][a-z0-9_]*$/i
|
|
24
|
+
if (!validIdentifier.test(tableName) || tableName.length > 63) {
|
|
25
|
+
// eslint-disable-next-line functional/no-throw-statements -- Validation requires throwing for invalid input
|
|
26
|
+
throw new Error(`Invalid table name: ${tableName}`)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Validate a column name to prevent SQL injection
|
|
32
|
+
*/
|
|
33
|
+
export const validateColumnName = (columnName: string): void => {
|
|
34
|
+
const validIdentifier = /^[a-z_][a-z0-9_]*$/i
|
|
35
|
+
if (!validIdentifier.test(columnName) || columnName.length > 63) {
|
|
36
|
+
// eslint-disable-next-line functional/no-throw-statements -- Validation requires throwing for invalid input
|
|
37
|
+
throw new Error(`Invalid column name: ${columnName}`)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
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 { generateSqlCondition } from '../filter-operators'
|
|
10
|
+
import {
|
|
11
|
+
getExistingViews,
|
|
12
|
+
getExistingMaterializedViews,
|
|
13
|
+
executeSQLStatements,
|
|
14
|
+
executeSQLStatementsParallel,
|
|
15
|
+
type TransactionLike,
|
|
16
|
+
} from '../sql/sql-execution'
|
|
17
|
+
import type { Table } from '@/domain/models/app/table'
|
|
18
|
+
import type { View } from '@/domain/models/app/table/views'
|
|
19
|
+
import type { ViewFilterNode } from '@/domain/models/app/table/views/filters'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Map filter nodes to SQL condition strings
|
|
23
|
+
* Extracts field, operator, value from each condition and generates SQL
|
|
24
|
+
* Handles leaf conditions only (not nested AND/OR groups)
|
|
25
|
+
*/
|
|
26
|
+
const mapFilterConditions = (nodes: readonly ViewFilterNode[]): readonly string[] => {
|
|
27
|
+
return nodes
|
|
28
|
+
.map((node) => {
|
|
29
|
+
if ('field' in node && 'operator' in node && 'value' in node) {
|
|
30
|
+
return generateSqlCondition(node.field, node.operator, node.value)
|
|
31
|
+
}
|
|
32
|
+
return ''
|
|
33
|
+
})
|
|
34
|
+
.filter((c) => c !== '')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Generate SQL WHERE clause from view filters
|
|
39
|
+
* Supports comparison operators (equals, greaterThan, lessThan, etc.)
|
|
40
|
+
* Values are properly escaped to prevent SQL injection
|
|
41
|
+
*/
|
|
42
|
+
const generateWhereClause = (filters: View['filters']): string => {
|
|
43
|
+
if (!filters) return ''
|
|
44
|
+
|
|
45
|
+
// Handle AND filters
|
|
46
|
+
if ('and' in filters && filters.and) {
|
|
47
|
+
const conditions = mapFilterConditions(filters.and)
|
|
48
|
+
return conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Handle OR filters
|
|
52
|
+
if ('or' in filters && filters.or) {
|
|
53
|
+
const conditions = mapFilterConditions(filters.or)
|
|
54
|
+
return conditions.length > 0 ? `WHERE ${conditions.join(' OR ')}` : ''
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return ''
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Generate SQL ORDER BY clause from view sorts and groupBy
|
|
62
|
+
* GroupBy takes precedence - when present, it's used for ordering
|
|
63
|
+
* If both groupBy and sorts are present, groupBy is applied first
|
|
64
|
+
*/
|
|
65
|
+
const generateOrderByClause = (sorts: View['sorts'], groupBy: View['groupBy']): string => {
|
|
66
|
+
// Build order items immutably
|
|
67
|
+
const groupByItems = groupBy
|
|
68
|
+
? [`${groupBy.field} ${(groupBy.direction || 'asc').toUpperCase()}`]
|
|
69
|
+
: []
|
|
70
|
+
|
|
71
|
+
const sortItems =
|
|
72
|
+
sorts && sorts.length > 0 && !groupBy
|
|
73
|
+
? sorts.map((sort) => `${sort.field} ${sort.direction.toUpperCase()}`)
|
|
74
|
+
: []
|
|
75
|
+
|
|
76
|
+
const orderItems = [...groupByItems, ...sortItems]
|
|
77
|
+
|
|
78
|
+
return orderItems.length > 0 ? `ORDER BY ${orderItems.join(', ')}` : ''
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Generate CREATE VIEW or CREATE MATERIALIZED VIEW statement for a table view
|
|
83
|
+
* PostgreSQL doesn't support IF NOT EXISTS for CREATE VIEW, so we drop first
|
|
84
|
+
*/
|
|
85
|
+
export const generateViewSQL = (table: Table, view: View): string => {
|
|
86
|
+
const viewType = view.materialized ? 'MATERIALIZED VIEW' : 'VIEW'
|
|
87
|
+
// Convert view.id to string (ViewId can be number or string)
|
|
88
|
+
const viewIdStr = String(view.id)
|
|
89
|
+
|
|
90
|
+
// If view has a custom query, use it directly
|
|
91
|
+
if (view.query) {
|
|
92
|
+
return `CREATE ${viewType} ${viewIdStr} AS ${view.query}`
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Otherwise, build query from filters, sorts, fields, groupBy
|
|
96
|
+
const fields = view.fields && view.fields.length > 0 ? view.fields.join(', ') : '*'
|
|
97
|
+
const whereClause = generateWhereClause(view.filters)
|
|
98
|
+
const orderByClause = generateOrderByClause(view.sorts, view.groupBy)
|
|
99
|
+
|
|
100
|
+
const clauses = [`SELECT ${fields}`, `FROM ${table.name}`, whereClause, orderByClause].filter(
|
|
101
|
+
(clause) => clause !== ''
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
const query = clauses.join(' ')
|
|
105
|
+
|
|
106
|
+
return `CREATE ${viewType} ${viewIdStr} AS ${query}`
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Generate all CREATE VIEW statements for a table
|
|
111
|
+
*/
|
|
112
|
+
export const generateTableViewStatements = (table: Table): readonly string[] => {
|
|
113
|
+
if (!table.views || table.views.length === 0) return []
|
|
114
|
+
|
|
115
|
+
return table.views.map((view) => generateViewSQL(table, view))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Generate trigger to make a view read-only
|
|
120
|
+
* PostgreSQL views can be automatically updatable if they meet certain criteria
|
|
121
|
+
* To ensure views are truly read-only, we create INSTEAD OF triggers that reject modifications
|
|
122
|
+
*/
|
|
123
|
+
export const generateReadOnlyViewTrigger = (viewId: string | number): readonly string[] => {
|
|
124
|
+
const viewIdStr = String(viewId)
|
|
125
|
+
const triggerBaseName = `${viewIdStr}_readonly`
|
|
126
|
+
|
|
127
|
+
return [
|
|
128
|
+
// INSTEAD OF INSERT trigger
|
|
129
|
+
`CREATE OR REPLACE FUNCTION ${triggerBaseName}_insert_fn()
|
|
130
|
+
RETURNS TRIGGER AS $$
|
|
131
|
+
BEGIN
|
|
132
|
+
RAISE EXCEPTION 'cannot insert into view "%"', TG_TABLE_NAME;
|
|
133
|
+
END;
|
|
134
|
+
$$ LANGUAGE plpgsql`,
|
|
135
|
+
`CREATE TRIGGER ${triggerBaseName}_insert
|
|
136
|
+
INSTEAD OF INSERT ON ${viewIdStr}
|
|
137
|
+
FOR EACH ROW EXECUTE FUNCTION ${triggerBaseName}_insert_fn()`,
|
|
138
|
+
|
|
139
|
+
// INSTEAD OF UPDATE trigger
|
|
140
|
+
`CREATE OR REPLACE FUNCTION ${triggerBaseName}_update_fn()
|
|
141
|
+
RETURNS TRIGGER AS $$
|
|
142
|
+
BEGIN
|
|
143
|
+
RAISE EXCEPTION 'cannot update view "%"', TG_TABLE_NAME;
|
|
144
|
+
END;
|
|
145
|
+
$$ LANGUAGE plpgsql`,
|
|
146
|
+
`CREATE TRIGGER ${triggerBaseName}_update
|
|
147
|
+
INSTEAD OF UPDATE ON ${viewIdStr}
|
|
148
|
+
FOR EACH ROW EXECUTE FUNCTION ${triggerBaseName}_update_fn()`,
|
|
149
|
+
|
|
150
|
+
// INSTEAD OF DELETE trigger
|
|
151
|
+
`CREATE OR REPLACE FUNCTION ${triggerBaseName}_delete_fn()
|
|
152
|
+
RETURNS TRIGGER AS $$
|
|
153
|
+
BEGIN
|
|
154
|
+
RAISE EXCEPTION 'cannot delete from view "%"', TG_TABLE_NAME;
|
|
155
|
+
END;
|
|
156
|
+
$$ LANGUAGE plpgsql`,
|
|
157
|
+
`CREATE TRIGGER ${triggerBaseName}_delete
|
|
158
|
+
INSTEAD OF DELETE ON ${viewIdStr}
|
|
159
|
+
FOR EACH ROW EXECUTE FUNCTION ${triggerBaseName}_delete_fn()`,
|
|
160
|
+
]
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Drop views that no longer exist in the schema
|
|
165
|
+
* Called once for all tables to ensure clean state before creating views
|
|
166
|
+
*/
|
|
167
|
+
/* eslint-disable functional/no-expression-statements */
|
|
168
|
+
export const generateDropObsoleteViewsSQL = async (
|
|
169
|
+
tx: TransactionLike,
|
|
170
|
+
tables: readonly Table[]
|
|
171
|
+
): Promise<void> => {
|
|
172
|
+
// Get existing views using Effect-based queries
|
|
173
|
+
const program = Effect.gen(function* () {
|
|
174
|
+
// Query both view types in parallel
|
|
175
|
+
const [existingViewNames, existingMatViewNames] = yield* Effect.all(
|
|
176
|
+
[getExistingViews(tx), getExistingMaterializedViews(tx)],
|
|
177
|
+
{ concurrency: 2 }
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
const existingViews = new Set(existingViewNames)
|
|
181
|
+
const existingMatViews = new Set(existingMatViewNames)
|
|
182
|
+
|
|
183
|
+
// Collect ALL view IDs from schema (across all tables)
|
|
184
|
+
const allSchemaViewIds = new Set<string>(
|
|
185
|
+
tables.flatMap((table) =>
|
|
186
|
+
table.views && table.views.length > 0 ? table.views.map((view) => String(view.id)) : []
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
// Find views to drop: exist in DB but not in schema
|
|
191
|
+
const viewsToDrop = Array.from(existingViews).filter(
|
|
192
|
+
(viewName) => !allSchemaViewIds.has(viewName)
|
|
193
|
+
)
|
|
194
|
+
const matViewsToDrop = Array.from(existingMatViews).filter(
|
|
195
|
+
(viewName) => !allSchemaViewIds.has(viewName)
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
// Generate DROP statements
|
|
199
|
+
const dropViewStatements = viewsToDrop.map(
|
|
200
|
+
(viewName) => `DROP VIEW IF EXISTS ${viewName} CASCADE`
|
|
201
|
+
)
|
|
202
|
+
const dropMatViewStatements = matViewsToDrop.map(
|
|
203
|
+
(viewName) => `DROP MATERIALIZED VIEW IF EXISTS ${viewName} CASCADE`
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
// Execute all DROP statements sequentially (not in parallel)
|
|
207
|
+
// Sequential execution ensures CASCADE dependencies are handled correctly
|
|
208
|
+
// When dropping view A that view B depends on, CASCADE will drop B automatically
|
|
209
|
+
// If we then try to drop B, IF EXISTS makes it a no-op
|
|
210
|
+
yield* executeSQLStatements(tx, [...dropViewStatements, ...dropMatViewStatements])
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
await Effect.runPromise(program)
|
|
214
|
+
}
|
|
215
|
+
/* eslint-enable functional/no-expression-statements */
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Drop all views that are not defined in any table's schema
|
|
219
|
+
* This ensures orphaned views (manually created or from previous schemas) are cleaned up
|
|
220
|
+
*/
|
|
221
|
+
/* eslint-disable functional/no-expression-statements */
|
|
222
|
+
export const dropAllObsoleteViews = async (
|
|
223
|
+
tx: TransactionLike,
|
|
224
|
+
tables: readonly Table[]
|
|
225
|
+
): Promise<void> => {
|
|
226
|
+
const program = Effect.gen(function* () {
|
|
227
|
+
// Query both view types in parallel
|
|
228
|
+
const [existingViewNames, existingMatViewNames] = yield* Effect.all(
|
|
229
|
+
[getExistingViews(tx), getExistingMaterializedViews(tx)],
|
|
230
|
+
{ concurrency: 2 }
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
// Collect all view IDs from all tables (functional construction)
|
|
234
|
+
const allSchemaViews = new Set<string>(
|
|
235
|
+
tables.flatMap((table) => (table.views ? table.views.map((view) => String(view.id)) : []))
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
// Find views to drop - any view in DB that's not in schema
|
|
239
|
+
const viewsToDrop = existingViewNames.filter((viewName) => !allSchemaViews.has(viewName))
|
|
240
|
+
const matViewsToDrop = existingMatViewNames.filter((viewName) => !allSchemaViews.has(viewName))
|
|
241
|
+
|
|
242
|
+
// Generate DROP statements with CASCADE
|
|
243
|
+
const dropViewStatements = viewsToDrop.map(
|
|
244
|
+
(viewName) => `DROP VIEW IF EXISTS ${viewName} CASCADE`
|
|
245
|
+
)
|
|
246
|
+
const dropMatViewStatements = matViewsToDrop.map(
|
|
247
|
+
(viewName) => `DROP MATERIALIZED VIEW IF EXISTS ${viewName} CASCADE`
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
// Execute all DROP statements in parallel
|
|
251
|
+
if (dropViewStatements.length > 0 || dropMatViewStatements.length > 0) {
|
|
252
|
+
yield* executeSQLStatementsParallel(tx, [...dropViewStatements, ...dropMatViewStatements])
|
|
253
|
+
}
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
await Effect.runPromise(program)
|
|
257
|
+
}
|
|
258
|
+
/* eslint-enable functional/no-expression-statements */
|
|
@@ -0,0 +1,43 @@
|
|
|
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 { DevTools } from '@effect/experimental'
|
|
9
|
+
import { Layer } from 'effect'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Effect DevTools layer for development debugging
|
|
13
|
+
*
|
|
14
|
+
* Provides runtime tracing and telemetry via the Effect DevTools extension.
|
|
15
|
+
* Enable by setting EFFECT_DEVTOOLS=1 environment variable.
|
|
16
|
+
*
|
|
17
|
+
* Requirements:
|
|
18
|
+
* - Install VS Code/Cursor Effect extension: https://marketplace.visualstudio.com/items?itemName=effectful-tech.effect-vscode
|
|
19
|
+
* - Set EFFECT_DEVTOOLS=1 in your environment
|
|
20
|
+
* - Run the application
|
|
21
|
+
* - Open Effect panel in your editor to see telemetry
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```bash
|
|
25
|
+
* EFFECT_DEVTOOLS=1 bun run start
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @see https://effect.website/docs/getting-started/devtools/
|
|
29
|
+
*/
|
|
30
|
+
export const DevToolsLayer = DevTools.layer()
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check if DevTools should be enabled based on environment variable
|
|
34
|
+
*/
|
|
35
|
+
export const isDevToolsEnabled = (): boolean => process.env['EFFECT_DEVTOOLS'] === '1'
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Conditionally provide DevTools layer based on environment
|
|
39
|
+
*
|
|
40
|
+
* Returns the DevTools layer if EFFECT_DEVTOOLS=1, otherwise returns an empty layer.
|
|
41
|
+
* This allows safe integration without runtime overhead when disabled.
|
|
42
|
+
*/
|
|
43
|
+
export const DevToolsLayerOptional = isDevToolsEnabled() ? DevToolsLayer : Layer.empty
|
|
@@ -0,0 +1,8 @@
|
|
|
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
|
+
export { DevToolsLayer, DevToolsLayerOptional, isDevToolsEnabled } from './devtools-layer'
|
|
@@ -0,0 +1,103 @@
|
|
|
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 { logError, logWarning } from '../logging'
|
|
9
|
+
import type { EmailConfig } from './nodemailer'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* SMTP Configuration using type-safe environment variable access
|
|
13
|
+
*
|
|
14
|
+
* This module provides email configuration with:
|
|
15
|
+
* 1. Type-safe environment variable access
|
|
16
|
+
* 2. Sensible defaults for development (Mailpit)
|
|
17
|
+
* 3. Structured logging for missing configuration
|
|
18
|
+
* 4. Clear separation of production vs development modes
|
|
19
|
+
*
|
|
20
|
+
* Environment Variables:
|
|
21
|
+
* - SMTP_HOST: SMTP server hostname (required in production)
|
|
22
|
+
* - SMTP_PORT: SMTP server port (default: 587)
|
|
23
|
+
* - SMTP_SECURE: Use SSL/TLS (default: false for port 587, true for port 465)
|
|
24
|
+
* - SMTP_USER: SMTP authentication username
|
|
25
|
+
* - SMTP_PASS: SMTP authentication password
|
|
26
|
+
* - SMTP_FROM: Default "from" email address (default: noreply@sovrium.com)
|
|
27
|
+
* - SMTP_FROM_NAME: Default "from" display name (default: 'Sovrium')
|
|
28
|
+
*
|
|
29
|
+
* @see https://mailpit.axllent.org/ for local development email testing
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Read optional string from environment
|
|
34
|
+
*/
|
|
35
|
+
const getEnvString = (key: string, defaultValue: string): string => process.env[key] ?? defaultValue
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Read optional number from environment
|
|
39
|
+
*/
|
|
40
|
+
const getEnvNumber = (key: string, defaultValue: number): number => {
|
|
41
|
+
const value = process.env[key]
|
|
42
|
+
return value ? parseInt(value, 10) : defaultValue
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Read optional boolean from environment
|
|
47
|
+
*/
|
|
48
|
+
const getEnvBoolean = (key: string, defaultValue: boolean): boolean => {
|
|
49
|
+
const value = process.env[key]
|
|
50
|
+
return value ? value === 'true' : defaultValue
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get email configuration from environment variables
|
|
55
|
+
*
|
|
56
|
+
* In production, SMTP_HOST is required. In development, falls back to
|
|
57
|
+
* localhost:1025 (Mailpit) for local email testing.
|
|
58
|
+
*/
|
|
59
|
+
export const getEmailConfigFromEffect = (): EmailConfig => {
|
|
60
|
+
const host = process.env.SMTP_HOST
|
|
61
|
+
const isProduction = process.env.NODE_ENV === 'production'
|
|
62
|
+
|
|
63
|
+
// Use real SMTP when host is configured
|
|
64
|
+
if (host) {
|
|
65
|
+
const port = getEnvNumber('SMTP_PORT', 587)
|
|
66
|
+
return {
|
|
67
|
+
host,
|
|
68
|
+
port,
|
|
69
|
+
secure: getEnvBoolean('SMTP_SECURE', false) || port === 465,
|
|
70
|
+
auth: {
|
|
71
|
+
user: getEnvString('SMTP_USER', ''),
|
|
72
|
+
pass: getEnvString('SMTP_PASS', ''),
|
|
73
|
+
},
|
|
74
|
+
from: {
|
|
75
|
+
email: getEnvString('SMTP_FROM', 'noreply@sovrium.com'),
|
|
76
|
+
name: getEnvString('SMTP_FROM_NAME', 'Sovrium'),
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Log warnings for missing SMTP config
|
|
82
|
+
if (isProduction) {
|
|
83
|
+
logError('[EMAIL] SMTP_HOST not configured in production mode')
|
|
84
|
+
} else {
|
|
85
|
+
logWarning('[EMAIL] SMTP_HOST not configured, using 127.0.0.1:1025 (Mailpit)')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Development fallback - Mailpit on localhost
|
|
89
|
+
// Use explicit IPv4 address to avoid IPv6 resolution issues
|
|
90
|
+
return {
|
|
91
|
+
host: '127.0.0.1',
|
|
92
|
+
port: 1025,
|
|
93
|
+
secure: false,
|
|
94
|
+
auth: {
|
|
95
|
+
user: '',
|
|
96
|
+
pass: '',
|
|
97
|
+
},
|
|
98
|
+
from: {
|
|
99
|
+
email: getEnvString('SMTP_FROM', 'noreply@sovrium.local'),
|
|
100
|
+
name: getEnvString('SMTP_FROM_NAME', 'Sovrium (Dev)'),
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -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 { Context, Effect, Layer, Data } from 'effect'
|
|
9
|
+
import { transporter, getDefaultFrom, type SendMailOptions } from './nodemailer'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Email service error types
|
|
13
|
+
*/
|
|
14
|
+
export class EmailError extends Data.TaggedError('EmailError')<{
|
|
15
|
+
readonly message: string
|
|
16
|
+
readonly cause?: unknown
|
|
17
|
+
}> {}
|
|
18
|
+
|
|
19
|
+
export class EmailConnectionError extends Data.TaggedError('EmailConnectionError')<{
|
|
20
|
+
readonly message: string
|
|
21
|
+
readonly cause?: unknown
|
|
22
|
+
}> {}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Email service interface
|
|
26
|
+
*
|
|
27
|
+
* Provides a functional interface for sending emails with Effect-based
|
|
28
|
+
* error handling and composition.
|
|
29
|
+
*/
|
|
30
|
+
export interface EmailService {
|
|
31
|
+
/**
|
|
32
|
+
* Send an email
|
|
33
|
+
*
|
|
34
|
+
* @param options - Nodemailer mail options
|
|
35
|
+
* @returns Effect that resolves with message info or fails with EmailError
|
|
36
|
+
*/
|
|
37
|
+
readonly send: (options: Readonly<SendMailOptions>) => Effect.Effect<string, EmailError>
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Send an email with the default "from" address
|
|
41
|
+
*
|
|
42
|
+
* @param options - Mail options without "from" field
|
|
43
|
+
* @returns Effect that resolves with message info or fails with EmailError
|
|
44
|
+
*/
|
|
45
|
+
readonly sendWithDefaultFrom: (
|
|
46
|
+
options: Readonly<Omit<SendMailOptions, 'from'>>
|
|
47
|
+
) => Effect.Effect<string, EmailError>
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Verify SMTP connection
|
|
51
|
+
*
|
|
52
|
+
* @returns Effect that resolves with true or fails with EmailConnectionError
|
|
53
|
+
*/
|
|
54
|
+
readonly verifyConnection: () => Effect.Effect<boolean, EmailConnectionError>
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Email service tag for Effect dependency injection
|
|
59
|
+
*/
|
|
60
|
+
export class Email extends Context.Tag('Email')<Email, EmailService>() {}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Live implementation of EmailService using Nodemailer
|
|
64
|
+
*/
|
|
65
|
+
export const EmailLive = Layer.succeed(
|
|
66
|
+
Email,
|
|
67
|
+
Email.of({
|
|
68
|
+
send: (options) =>
|
|
69
|
+
Effect.tryPromise({
|
|
70
|
+
try: async () => {
|
|
71
|
+
const info = await transporter.sendMail(options)
|
|
72
|
+
return info.messageId
|
|
73
|
+
},
|
|
74
|
+
catch: (error) =>
|
|
75
|
+
new EmailError({
|
|
76
|
+
message: `Failed to send email: ${error instanceof Error ? error.message : String(error)}`,
|
|
77
|
+
cause: error,
|
|
78
|
+
}),
|
|
79
|
+
}),
|
|
80
|
+
|
|
81
|
+
sendWithDefaultFrom: (options) =>
|
|
82
|
+
Effect.tryPromise({
|
|
83
|
+
try: async () => {
|
|
84
|
+
const info = await transporter.sendMail({
|
|
85
|
+
from: getDefaultFrom(),
|
|
86
|
+
...options,
|
|
87
|
+
})
|
|
88
|
+
return info.messageId
|
|
89
|
+
},
|
|
90
|
+
catch: (error) =>
|
|
91
|
+
new EmailError({
|
|
92
|
+
message: `Failed to send email: ${error instanceof Error ? error.message : String(error)}`,
|
|
93
|
+
cause: error,
|
|
94
|
+
}),
|
|
95
|
+
}),
|
|
96
|
+
|
|
97
|
+
verifyConnection: () =>
|
|
98
|
+
Effect.tryPromise({
|
|
99
|
+
try: () => transporter.verify(),
|
|
100
|
+
catch: (error) =>
|
|
101
|
+
new EmailConnectionError({
|
|
102
|
+
message: `SMTP connection failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
103
|
+
cause: error,
|
|
104
|
+
}),
|
|
105
|
+
}),
|
|
106
|
+
})
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Send email helper function (for use outside Effect context)
|
|
111
|
+
*
|
|
112
|
+
* This is a convenience function for Better Auth integration where
|
|
113
|
+
* we need to use async/await directly instead of Effect.
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```typescript
|
|
117
|
+
* import { sendEmail } from '@/infrastructure/email/email-service'
|
|
118
|
+
*
|
|
119
|
+
* await sendEmail({
|
|
120
|
+
* to: 'user@example.com',
|
|
121
|
+
* subject: 'Welcome',
|
|
122
|
+
* html: '<h1>Welcome!</h1>',
|
|
123
|
+
* })
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
export async function sendEmail(options: Readonly<Omit<SendMailOptions, 'from'>>): Promise<string> {
|
|
127
|
+
const info = await transporter.sendMail({
|
|
128
|
+
from: getDefaultFrom(),
|
|
129
|
+
...options,
|
|
130
|
+
})
|
|
131
|
+
return info.messageId
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Send email with full options (including custom "from")
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* ```typescript
|
|
139
|
+
* import { sendEmailWithOptions } from '@/infrastructure/email/email-service'
|
|
140
|
+
*
|
|
141
|
+
* await sendEmailWithOptions({
|
|
142
|
+
* from: '"Custom Sender" <custom@example.com>',
|
|
143
|
+
* to: 'user@example.com',
|
|
144
|
+
* subject: 'Hello',
|
|
145
|
+
* text: 'Hello World',
|
|
146
|
+
* })
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
export async function sendEmailWithOptions(options: Readonly<SendMailOptions>): Promise<string> {
|
|
150
|
+
const info = await transporter.sendMail(options)
|
|
151
|
+
return info.messageId
|
|
152
|
+
}
|