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,339 @@
|
|
|
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 { GetActivityById } from '@/application/use-cases/activity/programs'
|
|
10
|
+
import {
|
|
11
|
+
type ActivityLogOutput,
|
|
12
|
+
ListActivityLogs,
|
|
13
|
+
ListActivityLogsLayer,
|
|
14
|
+
} from '@/application/use-cases/list-activity-logs'
|
|
15
|
+
import { getUserRole } from '@/application/use-cases/tables/user-role'
|
|
16
|
+
import { DatabaseLive } from '@/infrastructure/database'
|
|
17
|
+
import { getSessionContext } from '@/presentation/api/utils/context-helpers'
|
|
18
|
+
import { sanitizeError, getStatusCode } from '@/presentation/api/utils/error-sanitizer'
|
|
19
|
+
import type { Context, Hono } from 'hono'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* User metadata in activity log API response
|
|
23
|
+
*/
|
|
24
|
+
interface ActivityLogResponseUser {
|
|
25
|
+
readonly id: string
|
|
26
|
+
readonly name: string
|
|
27
|
+
readonly email: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Activity log API response type
|
|
32
|
+
*
|
|
33
|
+
* Maps application ActivityLogOutput to API JSON response format.
|
|
34
|
+
* Uses camelCase for all fields per API conventions.
|
|
35
|
+
* user is null for system-logged activities (no user_id).
|
|
36
|
+
*/
|
|
37
|
+
interface ActivityLogResponse {
|
|
38
|
+
readonly id: string
|
|
39
|
+
readonly createdAt: string
|
|
40
|
+
readonly userId: string | undefined
|
|
41
|
+
readonly action: 'create' | 'update' | 'delete' | 'restore'
|
|
42
|
+
readonly tableName: string
|
|
43
|
+
readonly recordId: string
|
|
44
|
+
readonly user: ActivityLogResponseUser | null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Pagination metadata for list responses
|
|
49
|
+
*/
|
|
50
|
+
interface PaginationMeta {
|
|
51
|
+
readonly total: number
|
|
52
|
+
readonly page: number
|
|
53
|
+
readonly pageSize: number
|
|
54
|
+
readonly totalPages: number
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Parsed and validated pagination parameters
|
|
59
|
+
*/
|
|
60
|
+
interface PaginationParams {
|
|
61
|
+
readonly page: number
|
|
62
|
+
readonly pageSize: number
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Map ActivityLogOutput to API response format
|
|
67
|
+
*/
|
|
68
|
+
function mapToApiResponse(log: ActivityLogOutput): ActivityLogResponse {
|
|
69
|
+
return {
|
|
70
|
+
id: log.id,
|
|
71
|
+
createdAt: log.createdAt,
|
|
72
|
+
userId: log.userId,
|
|
73
|
+
action: log.action,
|
|
74
|
+
tableName: log.tableName,
|
|
75
|
+
recordId: log.recordId,
|
|
76
|
+
user: log.user,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Parse and validate pagination query parameters
|
|
82
|
+
*
|
|
83
|
+
* Returns undefined if parameters are invalid.
|
|
84
|
+
*/
|
|
85
|
+
function parsePaginationParams(
|
|
86
|
+
pageParam: string | undefined,
|
|
87
|
+
pageSizeParam: string | undefined
|
|
88
|
+
): PaginationParams | undefined {
|
|
89
|
+
const page = pageParam === undefined ? 1 : parseInt(pageParam, 10)
|
|
90
|
+
const pageSize = pageSizeParam === undefined ? 50 : parseInt(pageSizeParam, 10)
|
|
91
|
+
|
|
92
|
+
if (isNaN(page) || page < 1) return undefined
|
|
93
|
+
if (isNaN(pageSize) || pageSize < 1 || pageSize > 100) return undefined
|
|
94
|
+
|
|
95
|
+
return { page, pageSize }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Build paginated response from activity log list
|
|
100
|
+
*/
|
|
101
|
+
function buildPaginatedResponse(
|
|
102
|
+
logs: readonly ActivityLogOutput[],
|
|
103
|
+
page: number,
|
|
104
|
+
pageSize: number
|
|
105
|
+
): { activities: readonly ActivityLogResponse[]; pagination: PaginationMeta } {
|
|
106
|
+
const total = logs.length
|
|
107
|
+
const totalPages = Math.ceil(total / pageSize)
|
|
108
|
+
const start = (page - 1) * pageSize
|
|
109
|
+
const paginatedLogs = logs.slice(start, start + pageSize)
|
|
110
|
+
const pagination: PaginationMeta = { total, page, pageSize, totalPages }
|
|
111
|
+
return { activities: paginatedLogs.map(mapToApiResponse), pagination }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Handle GET /api/activity/:activityId - Get activity log details
|
|
116
|
+
*/
|
|
117
|
+
async function handleGetActivityById(c: Context) {
|
|
118
|
+
const activityId = c.req.param('activityId')
|
|
119
|
+
|
|
120
|
+
const program = GetActivityById(activityId).pipe(Effect.provide(DatabaseLive))
|
|
121
|
+
|
|
122
|
+
const result = await Effect.runPromise(program.pipe(Effect.either))
|
|
123
|
+
|
|
124
|
+
if (result._tag === 'Left') {
|
|
125
|
+
const error = result.left
|
|
126
|
+
|
|
127
|
+
if (error._tag === 'InvalidActivityIdError') {
|
|
128
|
+
return c.json(
|
|
129
|
+
{ success: false, message: 'Invalid activity ID format', code: 'INVALID_ACTIVITY_ID' },
|
|
130
|
+
400
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (error._tag === 'ActivityNotFoundError') {
|
|
135
|
+
return c.json(
|
|
136
|
+
{ success: false, message: 'Activity not found', code: 'ACTIVITY_NOT_FOUND' },
|
|
137
|
+
404
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return c.json(
|
|
142
|
+
{ success: false, message: 'Failed to fetch activity', code: 'DATABASE_ERROR' },
|
|
143
|
+
500
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return c.json(result.right, 200)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Valid activity action types
|
|
152
|
+
*/
|
|
153
|
+
const VALID_ACTIONS = ['create', 'update', 'delete', 'restore'] as const
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Parse and validate action filter parameter
|
|
157
|
+
*
|
|
158
|
+
* Returns undefined if no filter, null if invalid value.
|
|
159
|
+
*/
|
|
160
|
+
function parseActionFilter(
|
|
161
|
+
action: string | undefined
|
|
162
|
+
): 'create' | 'update' | 'delete' | 'restore' | undefined | null {
|
|
163
|
+
if (action === undefined) return undefined
|
|
164
|
+
if (VALID_ACTIONS.includes(action as (typeof VALID_ACTIONS)[number])) {
|
|
165
|
+
return action as 'create' | 'update' | 'delete' | 'restore'
|
|
166
|
+
}
|
|
167
|
+
// eslint-disable-next-line unicorn/no-null -- Null signals invalid action (vs undefined = no filter)
|
|
168
|
+
return null
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Check if a user is authorized to filter by the given userId
|
|
173
|
+
*
|
|
174
|
+
* Admins can filter by any userId.
|
|
175
|
+
* Non-admin users can only filter by their own userId.
|
|
176
|
+
* Returns true if authorized, false if forbidden.
|
|
177
|
+
*/
|
|
178
|
+
async function isAuthorizedForUserIdFilter(
|
|
179
|
+
sessionUserId: string,
|
|
180
|
+
userIdFilter: string | undefined
|
|
181
|
+
): Promise<boolean> {
|
|
182
|
+
if (userIdFilter === undefined) return true
|
|
183
|
+
if (userIdFilter === sessionUserId) return true
|
|
184
|
+
const role = await getUserRole(sessionUserId)
|
|
185
|
+
return role === 'admin'
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Filter options for activity log queries
|
|
190
|
+
*/
|
|
191
|
+
interface ActivityFilters {
|
|
192
|
+
readonly tableName?: string
|
|
193
|
+
readonly action?: 'create' | 'update' | 'delete' | 'restore'
|
|
194
|
+
readonly userId?: string
|
|
195
|
+
readonly startDate?: Date
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Apply tableName, action, userId, and startDate filters to activity logs
|
|
200
|
+
*/
|
|
201
|
+
function applyFilters(
|
|
202
|
+
logs: readonly ActivityLogOutput[],
|
|
203
|
+
filters: ActivityFilters
|
|
204
|
+
): readonly ActivityLogOutput[] {
|
|
205
|
+
return logs.filter(
|
|
206
|
+
(log) =>
|
|
207
|
+
(filters.tableName === undefined || log.tableName === filters.tableName) &&
|
|
208
|
+
(filters.action === undefined || log.action === filters.action) &&
|
|
209
|
+
(filters.userId === undefined || log.userId === filters.userId) &&
|
|
210
|
+
(filters.startDate === undefined || new Date(log.createdAt) >= filters.startDate)
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Parse query filter parameters from request context
|
|
216
|
+
*
|
|
217
|
+
* Returns undefined for tableName/userId if not provided,
|
|
218
|
+
* null for action if invalid value provided.
|
|
219
|
+
*/
|
|
220
|
+
function parseQueryFilters(c: Context): {
|
|
221
|
+
tableName: string | undefined
|
|
222
|
+
action: 'create' | 'update' | 'delete' | 'restore' | undefined | null
|
|
223
|
+
userId: string | undefined
|
|
224
|
+
startDate: Date | undefined
|
|
225
|
+
} {
|
|
226
|
+
return {
|
|
227
|
+
tableName: c.req.query('tableName'),
|
|
228
|
+
action: parseActionFilter(c.req.query('action')),
|
|
229
|
+
userId: c.req.query('userId'),
|
|
230
|
+
startDate:
|
|
231
|
+
c.req.query('startDate') !== undefined ? new Date(c.req.query('startDate')!) : undefined,
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Validation error response from list activity request validation
|
|
237
|
+
*/
|
|
238
|
+
interface ListActivityValidationError {
|
|
239
|
+
readonly status: number
|
|
240
|
+
readonly body: { success: false; message: string; code: string }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Validate list activity request parameters
|
|
245
|
+
*
|
|
246
|
+
* Returns error response object if invalid, or undefined if valid.
|
|
247
|
+
*/
|
|
248
|
+
async function validateListActivityRequest(
|
|
249
|
+
c: Context,
|
|
250
|
+
sessionUserId: string
|
|
251
|
+
): Promise<ListActivityValidationError | undefined> {
|
|
252
|
+
const params = parsePaginationParams(c.req.query('page'), c.req.query('pageSize'))
|
|
253
|
+
if (params === undefined) {
|
|
254
|
+
return {
|
|
255
|
+
status: 400,
|
|
256
|
+
body: { success: false, message: 'Invalid pagination parameters', code: 'INVALID_PARAMETER' },
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const { action } = parseQueryFilters(c)
|
|
261
|
+
if (action === null) {
|
|
262
|
+
return {
|
|
263
|
+
status: 400,
|
|
264
|
+
body: { success: false, message: 'Invalid action filter', code: 'INVALID_PARAMETER' },
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const userIdFilter = c.req.query('userId')
|
|
269
|
+
const authorized = await isAuthorizedForUserIdFilter(sessionUserId, userIdFilter)
|
|
270
|
+
if (!authorized) {
|
|
271
|
+
return {
|
|
272
|
+
status: 403,
|
|
273
|
+
body: {
|
|
274
|
+
success: false,
|
|
275
|
+
message: 'Forbidden: cannot view other users activities',
|
|
276
|
+
code: 'FORBIDDEN',
|
|
277
|
+
},
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return undefined
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Handle GET /api/activity - List activity logs with pagination
|
|
286
|
+
*/
|
|
287
|
+
async function handleListActivityLogs(c: Context) {
|
|
288
|
+
const session = getSessionContext(c)
|
|
289
|
+
if (!session) {
|
|
290
|
+
return c.json({ success: false, message: 'Authentication required', code: 'UNAUTHORIZED' }, 401)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const validationError = await validateListActivityRequest(c, session.userId)
|
|
294
|
+
if (validationError !== undefined) {
|
|
295
|
+
return c.json(validationError.body, validationError.status as 400 | 403)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const params = parsePaginationParams(c.req.query('page'), c.req.query('pageSize'))!
|
|
299
|
+
const { tableName, action, userId, startDate } = parseQueryFilters(c)
|
|
300
|
+
|
|
301
|
+
const result = await Effect.runPromise(
|
|
302
|
+
ListActivityLogs({ userId: session.userId }).pipe(
|
|
303
|
+
Effect.provide(ListActivityLogsLayer),
|
|
304
|
+
Effect.either
|
|
305
|
+
)
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
if (result._tag === 'Left') {
|
|
309
|
+
const sanitized = sanitizeError(result.left, crypto.randomUUID())
|
|
310
|
+
return c.json(
|
|
311
|
+
{ success: false, message: sanitized.message ?? sanitized.error, code: sanitized.code },
|
|
312
|
+
getStatusCode(sanitized.code)
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const filtered = applyFilters(result.right, {
|
|
317
|
+
tableName,
|
|
318
|
+
action: action ?? undefined,
|
|
319
|
+
userId,
|
|
320
|
+
startDate,
|
|
321
|
+
})
|
|
322
|
+
return c.json(buildPaginatedResponse(filtered, params.page, params.pageSize), 200)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Chain activity routes onto a Hono app
|
|
327
|
+
*
|
|
328
|
+
* Provides:
|
|
329
|
+
* - GET /api/activity/:activityId - Get activity log details
|
|
330
|
+
* - GET /api/activity - List activity logs (admin/member only)
|
|
331
|
+
*
|
|
332
|
+
* @param honoApp - Hono instance to chain routes onto
|
|
333
|
+
* @returns Hono app with activity routes chained
|
|
334
|
+
*/
|
|
335
|
+
export function chainActivityRoutes<T extends Hono>(honoApp: T): T {
|
|
336
|
+
return honoApp
|
|
337
|
+
.get('/api/activity/:activityId', handleGetActivityById)
|
|
338
|
+
.get('/api/activity', handleListActivityLogs) as T
|
|
339
|
+
}
|
|
@@ -0,0 +1,328 @@
|
|
|
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 { zValidator } from '@hono/zod-validator'
|
|
9
|
+
import { Effect } from 'effect'
|
|
10
|
+
import { collectPageView } from '@/application/use-cases/analytics/collect-page-view'
|
|
11
|
+
import { purgeOldAnalyticsData } from '@/application/use-cases/analytics/purge-old-data'
|
|
12
|
+
import { queryCampaigns } from '@/application/use-cases/analytics/query-campaigns'
|
|
13
|
+
import { queryDevices } from '@/application/use-cases/analytics/query-devices'
|
|
14
|
+
import { queryOverview } from '@/application/use-cases/analytics/query-overview'
|
|
15
|
+
import { queryPages } from '@/application/use-cases/analytics/query-pages'
|
|
16
|
+
import { queryReferrers } from '@/application/use-cases/analytics/query-referrers'
|
|
17
|
+
import { analyticsCollectSchema, analyticsQuerySchema } from '@/domain/models/api/analytics'
|
|
18
|
+
import { AnalyticsRepositoryLive } from '@/infrastructure/database/repositories/analytics-repository-live'
|
|
19
|
+
import { matchesAnyGlobPattern } from '@/infrastructure/utils/glob-matcher'
|
|
20
|
+
import { getSessionContext } from '@/presentation/api/utils/context-helpers'
|
|
21
|
+
import type { Context, Hono } from 'hono'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Extract client IP from request headers
|
|
25
|
+
*/
|
|
26
|
+
function extractClientIp(xForwardedFor: string | undefined): string {
|
|
27
|
+
if (xForwardedFor) {
|
|
28
|
+
const first = xForwardedFor.split(',')[0]
|
|
29
|
+
return first?.trim() ?? 'unknown'
|
|
30
|
+
}
|
|
31
|
+
return 'unknown'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parse and validate analytics query parameters from request
|
|
36
|
+
*/
|
|
37
|
+
function parseAnalyticsQuery(
|
|
38
|
+
c: Context,
|
|
39
|
+
appName: string
|
|
40
|
+
):
|
|
41
|
+
| {
|
|
42
|
+
readonly appName: string
|
|
43
|
+
readonly from: Date
|
|
44
|
+
readonly to: Date
|
|
45
|
+
readonly granularity: 'hour' | 'day' | 'week' | 'month'
|
|
46
|
+
}
|
|
47
|
+
| undefined {
|
|
48
|
+
const fromStr = c.req.query('from')
|
|
49
|
+
const toStr = c.req.query('to')
|
|
50
|
+
const validGranularities = new Set(['hour', 'day', 'week', 'month'])
|
|
51
|
+
const rawGranularity = c.req.query('granularity') ?? 'day'
|
|
52
|
+
const granularity = (validGranularities.has(rawGranularity) ? rawGranularity : 'day') as
|
|
53
|
+
| 'hour'
|
|
54
|
+
| 'day'
|
|
55
|
+
| 'week'
|
|
56
|
+
| 'month'
|
|
57
|
+
|
|
58
|
+
if (!fromStr || !toStr) return undefined
|
|
59
|
+
|
|
60
|
+
const parsed = analyticsQuerySchema.safeParse({ from: fromStr, to: toStr, granularity })
|
|
61
|
+
if (!parsed.success) return undefined
|
|
62
|
+
|
|
63
|
+
// Round `to` up to the end of its second so that the query range captures
|
|
64
|
+
// all events within the same wall-clock second. Without this, sub-second
|
|
65
|
+
// differences between the client timestamp and server-side NOW() can exclude
|
|
66
|
+
// rows that logically fall within the requested window.
|
|
67
|
+
const toDate = new Date(parsed.data.to)
|
|
68
|
+
const toEndOfSecond = new Date(Math.ceil(toDate.getTime() / 1000) * 1000 + 999)
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
appName,
|
|
72
|
+
from: new Date(parsed.data.from),
|
|
73
|
+
to: toEndOfSecond,
|
|
74
|
+
granularity: parsed.data.granularity,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Handle POST /api/analytics/collect — public endpoint, no auth required
|
|
80
|
+
*
|
|
81
|
+
* Records a page view with privacy-safe visitor hashing.
|
|
82
|
+
* Also triggers retention cleanup (fire-and-forget) to purge stale records.
|
|
83
|
+
* Returns 204 No Content for fastest response.
|
|
84
|
+
*/
|
|
85
|
+
// eslint-disable-next-line max-params -- All parameters are configured per-app and forwarded from route registration
|
|
86
|
+
async function handleCollect(
|
|
87
|
+
c: Context,
|
|
88
|
+
appName: string,
|
|
89
|
+
retentionDays?: number,
|
|
90
|
+
excludedPaths?: readonly string[],
|
|
91
|
+
respectDoNotTrack?: boolean
|
|
92
|
+
): Promise<Response> {
|
|
93
|
+
const body = c.req.valid('json' as never)
|
|
94
|
+
const pagePath = (body as { readonly p: string }).p
|
|
95
|
+
|
|
96
|
+
// Check if path is excluded - return 204 without recording
|
|
97
|
+
if (matchesAnyGlobPattern(excludedPaths, pagePath)) {
|
|
98
|
+
// eslint-disable-next-line unicorn/no-null
|
|
99
|
+
return c.body(null, 204)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Check Do Not Track header when respectDoNotTrack is enabled
|
|
103
|
+
const dntHeader = c.req.header('DNT')
|
|
104
|
+
if (respectDoNotTrack && dntHeader === '1') {
|
|
105
|
+
// eslint-disable-next-line unicorn/no-null
|
|
106
|
+
return c.body(null, 204)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const ip = extractClientIp(c.req.header('x-forwarded-for'))
|
|
110
|
+
const userAgent = c.req.header('user-agent') ?? ''
|
|
111
|
+
const acceptLanguage = c.req.header('accept-language') ?? ''
|
|
112
|
+
|
|
113
|
+
// Fire-and-forget: record page view and purge stale data asynchronously
|
|
114
|
+
// eslint-disable-next-line functional/no-expression-statements
|
|
115
|
+
void Effect.runPromise(
|
|
116
|
+
Effect.all(
|
|
117
|
+
[
|
|
118
|
+
collectPageView({
|
|
119
|
+
appName,
|
|
120
|
+
pagePath,
|
|
121
|
+
pageTitle: (body as { readonly t?: string }).t,
|
|
122
|
+
referrerUrl: (body as { readonly r?: string }).r,
|
|
123
|
+
ip,
|
|
124
|
+
userAgent,
|
|
125
|
+
acceptLanguage,
|
|
126
|
+
screenWidth: (body as { readonly sw?: number }).sw,
|
|
127
|
+
screenHeight: (body as { readonly sh?: number }).sh,
|
|
128
|
+
utmSource: (body as { readonly us?: string }).us,
|
|
129
|
+
utmMedium: (body as { readonly um?: string }).um,
|
|
130
|
+
utmCampaign: (body as { readonly uc?: string }).uc,
|
|
131
|
+
utmContent: (body as { readonly ux?: string }).ux,
|
|
132
|
+
utmTerm: (body as { readonly ut?: string }).ut,
|
|
133
|
+
}),
|
|
134
|
+
purgeOldAnalyticsData(appName, retentionDays),
|
|
135
|
+
],
|
|
136
|
+
{ concurrency: 'unbounded' }
|
|
137
|
+
).pipe(
|
|
138
|
+
Effect.provide(AnalyticsRepositoryLive),
|
|
139
|
+
Effect.catchAll(() => Effect.void)
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
// eslint-disable-next-line unicorn/no-null
|
|
144
|
+
return c.body(null, 204)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Handle GET /api/analytics/overview — requires auth
|
|
149
|
+
*/
|
|
150
|
+
async function handleOverview(c: Context, appName: string): Promise<Response> {
|
|
151
|
+
const session = getSessionContext(c)
|
|
152
|
+
if (!session) {
|
|
153
|
+
return c.json({ error: 'Authentication required', code: 'UNAUTHORIZED' }, 401)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const params = parseAnalyticsQuery(c, appName)
|
|
157
|
+
if (!params) {
|
|
158
|
+
return c.json({ error: 'Missing or invalid from/to parameters', code: 'VALIDATION_ERROR' }, 400)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const result = await Effect.runPromise(
|
|
162
|
+
queryOverview({
|
|
163
|
+
appName: params.appName,
|
|
164
|
+
from: params.from,
|
|
165
|
+
to: params.to,
|
|
166
|
+
granularity: params.granularity,
|
|
167
|
+
}).pipe(Effect.provide(AnalyticsRepositoryLive), Effect.either)
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
if (result._tag === 'Left') {
|
|
171
|
+
return c.json({ error: 'Failed to query analytics', code: 'INTERNAL_ERROR' }, 500)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return c.json(result.right, 200)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Handle GET /api/analytics/pages — requires auth
|
|
179
|
+
*/
|
|
180
|
+
async function handlePages(c: Context, appName: string): Promise<Response> {
|
|
181
|
+
const session = getSessionContext(c)
|
|
182
|
+
if (!session) {
|
|
183
|
+
return c.json({ error: 'Authentication required', code: 'UNAUTHORIZED' }, 401)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const params = parseAnalyticsQuery(c, appName)
|
|
187
|
+
if (!params) {
|
|
188
|
+
return c.json({ error: 'Missing or invalid from/to parameters', code: 'VALIDATION_ERROR' }, 400)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const result = await Effect.runPromise(
|
|
192
|
+
queryPages({
|
|
193
|
+
appName: params.appName,
|
|
194
|
+
from: params.from,
|
|
195
|
+
to: params.to,
|
|
196
|
+
}).pipe(Effect.provide(AnalyticsRepositoryLive), Effect.either)
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
if (result._tag === 'Left') {
|
|
200
|
+
return c.json({ error: 'Failed to query pages', code: 'INTERNAL_ERROR' }, 500)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return c.json(result.right, 200)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Handle GET /api/analytics/referrers — requires auth
|
|
208
|
+
*/
|
|
209
|
+
async function handleReferrers(c: Context, appName: string): Promise<Response> {
|
|
210
|
+
const session = getSessionContext(c)
|
|
211
|
+
if (!session) {
|
|
212
|
+
return c.json({ error: 'Authentication required', code: 'UNAUTHORIZED' }, 401)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const params = parseAnalyticsQuery(c, appName)
|
|
216
|
+
if (!params) {
|
|
217
|
+
return c.json({ error: 'Missing or invalid from/to parameters', code: 'VALIDATION_ERROR' }, 400)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const result = await Effect.runPromise(
|
|
221
|
+
queryReferrers({
|
|
222
|
+
appName: params.appName,
|
|
223
|
+
from: params.from,
|
|
224
|
+
to: params.to,
|
|
225
|
+
}).pipe(Effect.provide(AnalyticsRepositoryLive), Effect.either)
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
if (result._tag === 'Left') {
|
|
229
|
+
return c.json({ error: 'Failed to query referrers', code: 'INTERNAL_ERROR' }, 500)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return c.json(result.right, 200)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Handle GET /api/analytics/devices — requires auth
|
|
237
|
+
*/
|
|
238
|
+
async function handleDevices(c: Context, appName: string): Promise<Response> {
|
|
239
|
+
const session = getSessionContext(c)
|
|
240
|
+
if (!session) {
|
|
241
|
+
return c.json({ error: 'Authentication required', code: 'UNAUTHORIZED' }, 401)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const params = parseAnalyticsQuery(c, appName)
|
|
245
|
+
if (!params) {
|
|
246
|
+
return c.json({ error: 'Missing or invalid from/to parameters', code: 'VALIDATION_ERROR' }, 400)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const result = await Effect.runPromise(
|
|
250
|
+
queryDevices({
|
|
251
|
+
appName: params.appName,
|
|
252
|
+
from: params.from,
|
|
253
|
+
to: params.to,
|
|
254
|
+
}).pipe(Effect.provide(AnalyticsRepositoryLive), Effect.either)
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
if (result._tag === 'Left') {
|
|
258
|
+
return c.json({ error: 'Failed to query devices', code: 'INTERNAL_ERROR' }, 500)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return c.json(result.right, 200)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Handle GET /api/analytics/campaigns — requires auth
|
|
266
|
+
*/
|
|
267
|
+
async function handleCampaigns(c: Context, appName: string): Promise<Response> {
|
|
268
|
+
const session = getSessionContext(c)
|
|
269
|
+
if (!session) {
|
|
270
|
+
return c.json({ error: 'Authentication required', code: 'UNAUTHORIZED' }, 401)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const params = parseAnalyticsQuery(c, appName)
|
|
274
|
+
if (!params) {
|
|
275
|
+
return c.json({ error: 'Missing or invalid from/to parameters', code: 'VALIDATION_ERROR' }, 400)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const result = await Effect.runPromise(
|
|
279
|
+
queryCampaigns({
|
|
280
|
+
appName: params.appName,
|
|
281
|
+
from: params.from,
|
|
282
|
+
to: params.to,
|
|
283
|
+
}).pipe(Effect.provide(AnalyticsRepositoryLive), Effect.either)
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
if (result._tag === 'Left') {
|
|
287
|
+
return c.json({ error: 'Failed to query campaigns', code: 'INTERNAL_ERROR' }, 500)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return c.json(result.right, 200)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Chain analytics routes onto a Hono app
|
|
295
|
+
*
|
|
296
|
+
* Provides:
|
|
297
|
+
* - POST /api/analytics/collect - Record page view (public, no auth)
|
|
298
|
+
* - GET /api/analytics/overview - Summary + time series (auth required)
|
|
299
|
+
* - GET /api/analytics/pages - Top pages (auth required)
|
|
300
|
+
* - GET /api/analytics/referrers - Top referrers (auth required)
|
|
301
|
+
* - GET /api/analytics/devices - Device breakdown (auth required)
|
|
302
|
+
* - GET /api/analytics/campaigns - UTM campaigns (auth required)
|
|
303
|
+
*
|
|
304
|
+
* @param honoApp - Hono instance to chain routes onto
|
|
305
|
+
* @param appName - Application name for multi-tenant analytics
|
|
306
|
+
* @param retentionDays - Number of days to retain analytics data (triggers cleanup on collect)
|
|
307
|
+
* @param excludedPaths - Glob patterns for paths excluded from tracking
|
|
308
|
+
* @param respectDoNotTrack - Whether to honor Do Not Track (DNT:1) header
|
|
309
|
+
* @returns Hono app with analytics routes chained
|
|
310
|
+
*/
|
|
311
|
+
// eslint-disable-next-line max-params -- All parameters are configured per-app and forwarded from route setup
|
|
312
|
+
export function chainAnalyticsRoutes<T extends Hono>(
|
|
313
|
+
honoApp: T,
|
|
314
|
+
appName: string,
|
|
315
|
+
retentionDays?: number,
|
|
316
|
+
excludedPaths?: readonly string[],
|
|
317
|
+
respectDoNotTrack?: boolean
|
|
318
|
+
): T {
|
|
319
|
+
return honoApp
|
|
320
|
+
.post('/api/analytics/collect', zValidator('json', analyticsCollectSchema), (c) =>
|
|
321
|
+
handleCollect(c, appName, retentionDays, excludedPaths, respectDoNotTrack)
|
|
322
|
+
)
|
|
323
|
+
.get('/api/analytics/overview', (c) => handleOverview(c, appName))
|
|
324
|
+
.get('/api/analytics/pages', (c) => handlePages(c, appName))
|
|
325
|
+
.get('/api/analytics/referrers', (c) => handleReferrers(c, appName))
|
|
326
|
+
.get('/api/analytics/devices', (c) => handleDevices(c, appName))
|
|
327
|
+
.get('/api/analytics/campaigns', (c) => handleCampaigns(c, appName)) as T
|
|
328
|
+
}
|