snackbase 0.12.1__py3-none-any.whl

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.
Files changed (350) hide show
  1. snackbase/__init__.py +11 -0
  2. snackbase/__main__.py +10 -0
  3. snackbase/alembic/README +1 -0
  4. snackbase/alembic/env.py +123 -0
  5. snackbase/alembic/script.py.mako +28 -0
  6. snackbase/alembic/versions/038a7194dd07_enhance_password_reset_template.py +272 -0
  7. snackbase/alembic/versions/084e50e30ae8_create_collection_rules_table.py +131 -0
  8. snackbase/alembic/versions/0ea276bf29bc_add_email_sent_to_invitations.py +39 -0
  9. snackbase/alembic/versions/147572043440_seed_invitation_email_template.py +154 -0
  10. snackbase/alembic/versions/1799ee5d7311_add_email_tables.py +193 -0
  11. snackbase/alembic/versions/20260214_create_token_blacklist.py +38 -0
  12. snackbase/alembic/versions/20260219_add_is_default_to_configurations.py +44 -0
  13. snackbase/alembic/versions/20260330_create_webhooks_tables.py +201 -0
  14. snackbase/alembic/versions/20260401_create_jobs_table.py +159 -0
  15. snackbase/alembic/versions/20260402_create_hooks_table.py +131 -0
  16. snackbase/alembic/versions/20260403_add_hook_condition_and_executions.py +125 -0
  17. snackbase/alembic/versions/20260403_create_endpoints_tables.py +225 -0
  18. snackbase/alembic/versions/20260403_create_workflow_tables.py +256 -0
  19. snackbase/alembic/versions/20260725_create_codelists_tables.py +329 -0
  20. snackbase/alembic/versions/20260725_remove_builtin_regions_seed.py +38 -0
  21. snackbase/alembic/versions/20260803_create_functions_tables.py +266 -0
  22. snackbase/alembic/versions/20260803_encrypt_webhook_secrets.py +115 -0
  23. snackbase/alembic/versions/20260809_login_lockout.py +49 -0
  24. snackbase/alembic/versions/20260812_files_add_files_registry.py +88 -0
  25. snackbase/alembic/versions/3f032f63cb42_drop_permissions_table.py +58 -0
  26. snackbase/alembic/versions/60d278c6a3e3_add_timezone_support_to_all_tables.py +90 -0
  27. snackbase/alembic/versions/7bccaa56da18_create_api_keys_table.py +60 -0
  28. snackbase/alembic/versions/85e1c8c401fe_update_email_verification_template.py +246 -0
  29. snackbase/alembic/versions/8cebef609756_add_audit_log_triggers.py +105 -0
  30. snackbase/alembic/versions/aeef347267b5_initial_migration.py +264 -0
  31. snackbase/alembic/versions/b6a59f965907_add_configurations_table.py +197 -0
  32. snackbase/alembic/versions/b8e206c85cf5_add_variables_to_email_logs.py +41 -0
  33. snackbase/alembic/versions/bb38e4d658cd_create_password_reset_tokens_table.py +52 -0
  34. snackbase/alembic/versions/c69bf537b237_add_auth_provider_tracking_to_users.py +107 -0
  35. snackbase/alembic/versions/def3fc1039eb_add_email_verification_tokens_and_user_.py +76 -0
  36. snackbase/alembic/versions/e4ea5d0adb91_add_oauth_states_table.py +103 -0
  37. snackbase/alembic/versions/ea6f4f210d59_add_migration_revision_to_collections.py +35 -0
  38. snackbase/alembic/versions/f793cd9fde21_add_timezone_support_to_users_table.py +122 -0
  39. snackbase/alembic.ini +141 -0
  40. snackbase/application/__init__.py +0 -0
  41. snackbase/application/commands/__init__.py +0 -0
  42. snackbase/application/queries/__init__.py +0 -0
  43. snackbase/application/services/migration_query_service.py +295 -0
  44. snackbase/cli.py +1294 -0
  45. snackbase/core/__init__.py +23 -0
  46. snackbase/core/config.py +721 -0
  47. snackbase/core/configuration/config_registry.py +436 -0
  48. snackbase/core/context.py +39 -0
  49. snackbase/core/cron/__init__.py +9 -0
  50. snackbase/core/cron/parser.py +384 -0
  51. snackbase/core/cursor.py +49 -0
  52. snackbase/core/hooks/__init__.py +52 -0
  53. snackbase/core/hooks/hook_decorator.py +651 -0
  54. snackbase/core/hooks/hook_events.py +165 -0
  55. snackbase/core/hooks/hook_registry.py +444 -0
  56. snackbase/core/logging.py +215 -0
  57. snackbase/core/macros/engine.py +239 -0
  58. snackbase/core/macros/expander.py +109 -0
  59. snackbase/core/rules/__init__.py +48 -0
  60. snackbase/core/rules/aggregation_parser.py +391 -0
  61. snackbase/core/rules/ast.py +67 -0
  62. snackbase/core/rules/exceptions.py +15 -0
  63. snackbase/core/rules/expression_compiler.py +384 -0
  64. snackbase/core/rules/filter_compiler.py +222 -0
  65. snackbase/core/rules/filter_validator.py +245 -0
  66. snackbase/core/rules/lexer.py +283 -0
  67. snackbase/core/rules/parser.py +199 -0
  68. snackbase/core/rules/rule_validator.py +150 -0
  69. snackbase/core/rules/sql_compiler.py +213 -0
  70. snackbase/domain/__init__.py +0 -0
  71. snackbase/domain/entities/__init__.py +50 -0
  72. snackbase/domain/entities/account.py +52 -0
  73. snackbase/domain/entities/audit_log.py +95 -0
  74. snackbase/domain/entities/codelist.py +126 -0
  75. snackbase/domain/entities/collection.py +40 -0
  76. snackbase/domain/entities/collection_rule.py +139 -0
  77. snackbase/domain/entities/email_log.py +55 -0
  78. snackbase/domain/entities/email_template.py +59 -0
  79. snackbase/domain/entities/email_verification.py +67 -0
  80. snackbase/domain/entities/group.py +41 -0
  81. snackbase/domain/entities/hook_context.py +100 -0
  82. snackbase/domain/entities/invitation.py +63 -0
  83. snackbase/domain/entities/password_reset.py +66 -0
  84. snackbase/domain/entities/role.py +30 -0
  85. snackbase/domain/entities/user.py +64 -0
  86. snackbase/domain/services/__init__.py +75 -0
  87. snackbase/domain/services/account_code_generator.py +247 -0
  88. snackbase/domain/services/account_service.py +203 -0
  89. snackbase/domain/services/audit_checksum.py +101 -0
  90. snackbase/domain/services/audit_log_service.py +709 -0
  91. snackbase/domain/services/codelist_service.py +902 -0
  92. snackbase/domain/services/collection_service.py +699 -0
  93. snackbase/domain/services/collection_validator.py +696 -0
  94. snackbase/domain/services/dashboard_service.py +550 -0
  95. snackbase/domain/services/email_verification_service.py +144 -0
  96. snackbase/domain/services/encryption_migration_service.py +225 -0
  97. snackbase/domain/services/file_storage_service.py +414 -0
  98. snackbase/domain/services/password_reset_service.py +238 -0
  99. snackbase/domain/services/password_validator.py +138 -0
  100. snackbase/domain/services/pii_masking_service.py +260 -0
  101. snackbase/domain/services/record_validator.py +400 -0
  102. snackbase/domain/services/schema_diff.py +125 -0
  103. snackbase/domain/services/slug_generator.py +152 -0
  104. snackbase/domain/services/superadmin_service.py +191 -0
  105. snackbase/domain/services/user_field.py +56 -0
  106. snackbase/embedded.py +261 -0
  107. snackbase/infrastructure/__init__.py +30 -0
  108. snackbase/infrastructure/api/__init__.py +0 -0
  109. snackbase/infrastructure/api/app.py +1092 -0
  110. snackbase/infrastructure/api/dependencies.py +370 -0
  111. snackbase/infrastructure/api/middleware/__init__.py +21 -0
  112. snackbase/infrastructure/api/middleware/authorization.py +459 -0
  113. snackbase/infrastructure/api/middleware/client_ip.py +108 -0
  114. snackbase/infrastructure/api/middleware/context_middleware.py +56 -0
  115. snackbase/infrastructure/api/middleware/rate_limit_middleware.py +149 -0
  116. snackbase/infrastructure/api/middleware/rate_limit_storage.py +162 -0
  117. snackbase/infrastructure/api/middleware/security_headers_middleware.py +95 -0
  118. snackbase/infrastructure/api/routes/__init__.py +69 -0
  119. snackbase/infrastructure/api/routes/accounts_router.py +334 -0
  120. snackbase/infrastructure/api/routes/admin_router.py +919 -0
  121. snackbase/infrastructure/api/routes/api_keys_router.py +240 -0
  122. snackbase/infrastructure/api/routes/audit_log_router.py +162 -0
  123. snackbase/infrastructure/api/routes/auth_router.py +1142 -0
  124. snackbase/infrastructure/api/routes/backups_router.py +546 -0
  125. snackbase/infrastructure/api/routes/codelists_router.py +859 -0
  126. snackbase/infrastructure/api/routes/collection_rules_router.py +202 -0
  127. snackbase/infrastructure/api/routes/collections_router.py +739 -0
  128. snackbase/infrastructure/api/routes/custom_endpoint_dispatcher.py +420 -0
  129. snackbase/infrastructure/api/routes/dashboard_router.py +61 -0
  130. snackbase/infrastructure/api/routes/email_templates_router.py +471 -0
  131. snackbase/infrastructure/api/routes/endpoints_router.py +303 -0
  132. snackbase/infrastructure/api/routes/files_router.py +369 -0
  133. snackbase/infrastructure/api/routes/function_dispatcher.py +469 -0
  134. snackbase/infrastructure/api/routes/functions_router.py +711 -0
  135. snackbase/infrastructure/api/routes/groups_router.py +281 -0
  136. snackbase/infrastructure/api/routes/hooks_router.py +447 -0
  137. snackbase/infrastructure/api/routes/invitations_router.py +794 -0
  138. snackbase/infrastructure/api/routes/jobs_router.py +192 -0
  139. snackbase/infrastructure/api/routes/macros_router.py +316 -0
  140. snackbase/infrastructure/api/routes/migrations_router.py +420 -0
  141. snackbase/infrastructure/api/routes/oauth_router.py +622 -0
  142. snackbase/infrastructure/api/routes/realtime_router.py +229 -0
  143. snackbase/infrastructure/api/routes/records_router.py +2247 -0
  144. snackbase/infrastructure/api/routes/roles_router.py +330 -0
  145. snackbase/infrastructure/api/routes/saml_router.py +611 -0
  146. snackbase/infrastructure/api/routes/users_router.py +623 -0
  147. snackbase/infrastructure/api/routes/webhooks_router.py +284 -0
  148. snackbase/infrastructure/api/routes/workflows_router.py +576 -0
  149. snackbase/infrastructure/api/schemas/__init__.py +193 -0
  150. snackbase/infrastructure/api/schemas/account_schemas.py +87 -0
  151. snackbase/infrastructure/api/schemas/api_key_schemas.py +55 -0
  152. snackbase/infrastructure/api/schemas/audit_log_schemas.py +53 -0
  153. snackbase/infrastructure/api/schemas/auth_schemas.py +206 -0
  154. snackbase/infrastructure/api/schemas/backup_schemas.py +100 -0
  155. snackbase/infrastructure/api/schemas/codelist_schemas.py +177 -0
  156. snackbase/infrastructure/api/schemas/collection_schemas.py +444 -0
  157. snackbase/infrastructure/api/schemas/dashboard_schemas.py +222 -0
  158. snackbase/infrastructure/api/schemas/email_schemas.py +147 -0
  159. snackbase/infrastructure/api/schemas/endpoint_schemas.py +213 -0
  160. snackbase/infrastructure/api/schemas/file_schemas.py +26 -0
  161. snackbase/infrastructure/api/schemas/function_schemas.py +178 -0
  162. snackbase/infrastructure/api/schemas/group_schemas.py +52 -0
  163. snackbase/infrastructure/api/schemas/hook_schemas.py +160 -0
  164. snackbase/infrastructure/api/schemas/invitation_schemas.py +89 -0
  165. snackbase/infrastructure/api/schemas/job_schemas.py +50 -0
  166. snackbase/infrastructure/api/schemas/macro.py +124 -0
  167. snackbase/infrastructure/api/schemas/migration_query_schemas.py +121 -0
  168. snackbase/infrastructure/api/schemas/record_schemas.py +165 -0
  169. snackbase/infrastructure/api/schemas/role_schemas.py +84 -0
  170. snackbase/infrastructure/api/schemas/users_schemas.py +166 -0
  171. snackbase/infrastructure/api/schemas/webhook_schemas.py +133 -0
  172. snackbase/infrastructure/api/schemas/workflow_schemas.py +267 -0
  173. snackbase/infrastructure/auth/__init__.py +56 -0
  174. snackbase/infrastructure/auth/api_key_service.py +172 -0
  175. snackbase/infrastructure/auth/authenticator.py +537 -0
  176. snackbase/infrastructure/auth/jwt_service.py +215 -0
  177. snackbase/infrastructure/auth/login_throttle.py +106 -0
  178. snackbase/infrastructure/auth/middleware.py +72 -0
  179. snackbase/infrastructure/auth/password_hasher.py +100 -0
  180. snackbase/infrastructure/auth/platform_jwks.py +122 -0
  181. snackbase/infrastructure/auth/token_codec.py +113 -0
  182. snackbase/infrastructure/auth/token_types.py +62 -0
  183. snackbase/infrastructure/backup/__init__.py +7 -0
  184. snackbase/infrastructure/backup/alerting.py +173 -0
  185. snackbase/infrastructure/backup/archive.py +192 -0
  186. snackbase/infrastructure/backup/audit.py +106 -0
  187. snackbase/infrastructure/backup/destinations.py +560 -0
  188. snackbase/infrastructure/backup/lock.py +182 -0
  189. snackbase/infrastructure/backup/logical_snapshot.py +234 -0
  190. snackbase/infrastructure/backup/manifest.py +426 -0
  191. snackbase/infrastructure/backup/restart.py +52 -0
  192. snackbase/infrastructure/backup/restore.py +1017 -0
  193. snackbase/infrastructure/backup/retention.py +86 -0
  194. snackbase/infrastructure/backup/scheduler.py +235 -0
  195. snackbase/infrastructure/backup/service.py +518 -0
  196. snackbase/infrastructure/backup/sqlite_snapshot.py +83 -0
  197. snackbase/infrastructure/configuration/providers/__init__.py +5 -0
  198. snackbase/infrastructure/configuration/providers/auth/__init__.py +7 -0
  199. snackbase/infrastructure/configuration/providers/auth/email_password.py +50 -0
  200. snackbase/infrastructure/configuration/providers/backup/__init__.py +1 -0
  201. snackbase/infrastructure/configuration/providers/backup/backup_settings.py +151 -0
  202. snackbase/infrastructure/configuration/providers/email/aws_ses.py +88 -0
  203. snackbase/infrastructure/configuration/providers/email/resend.py +77 -0
  204. snackbase/infrastructure/configuration/providers/email/smtp.py +102 -0
  205. snackbase/infrastructure/configuration/providers/oauth/__init__.py +24 -0
  206. snackbase/infrastructure/configuration/providers/oauth/apple.py +221 -0
  207. snackbase/infrastructure/configuration/providers/oauth/github.py +195 -0
  208. snackbase/infrastructure/configuration/providers/oauth/google.py +174 -0
  209. snackbase/infrastructure/configuration/providers/oauth/microsoft.py +184 -0
  210. snackbase/infrastructure/configuration/providers/oauth/oauth_handler.py +198 -0
  211. snackbase/infrastructure/configuration/providers/saml/__init__.py +19 -0
  212. snackbase/infrastructure/configuration/providers/saml/assertion_validator.py +143 -0
  213. snackbase/infrastructure/configuration/providers/saml/azure_ad.py +236 -0
  214. snackbase/infrastructure/configuration/providers/saml/generic.py +292 -0
  215. snackbase/infrastructure/configuration/providers/saml/okta.py +242 -0
  216. snackbase/infrastructure/configuration/providers/saml/saml_handler.py +189 -0
  217. snackbase/infrastructure/configuration/providers/storage/__init__.py +10 -0
  218. snackbase/infrastructure/configuration/providers/storage/local.py +32 -0
  219. snackbase/infrastructure/configuration/providers/storage/s3.py +67 -0
  220. snackbase/infrastructure/configuration/providers/system/__init__.py +7 -0
  221. snackbase/infrastructure/configuration/providers/system/system_config.py +68 -0
  222. snackbase/infrastructure/endpoints/__init__.py +1 -0
  223. snackbase/infrastructure/endpoints/endpoint_executor.py +503 -0
  224. snackbase/infrastructure/functions/__init__.py +14 -0
  225. snackbase/infrastructure/functions/audit.py +63 -0
  226. snackbase/infrastructure/functions/bootstrap.py +170 -0
  227. snackbase/infrastructure/functions/concurrency.py +80 -0
  228. snackbase/infrastructure/functions/env_builder.py +216 -0
  229. snackbase/infrastructure/functions/invoke_action.py +162 -0
  230. snackbase/infrastructure/functions/nested_budget.py +65 -0
  231. snackbase/infrastructure/functions/pin_parser.py +117 -0
  232. snackbase/infrastructure/functions/redaction.py +61 -0
  233. snackbase/infrastructure/functions/retention.py +27 -0
  234. snackbase/infrastructure/functions/runner.py +539 -0
  235. snackbase/infrastructure/functions/sandbox.py +349 -0
  236. snackbase/infrastructure/functions/source_paths.py +56 -0
  237. snackbase/infrastructure/functions/syntax_preflight.py +46 -0
  238. snackbase/infrastructure/functions/worker_pool.py +110 -0
  239. snackbase/infrastructure/hooks/__init__.py +20 -0
  240. snackbase/infrastructure/hooks/action_executor.py +323 -0
  241. snackbase/infrastructure/hooks/api_defined_hook.py +328 -0
  242. snackbase/infrastructure/hooks/builtin_hooks.py +387 -0
  243. snackbase/infrastructure/hooks/webhook_hook.py +189 -0
  244. snackbase/infrastructure/persistence/__init__.py +0 -0
  245. snackbase/infrastructure/persistence/audit_helper.py +182 -0
  246. snackbase/infrastructure/persistence/database.py +1026 -0
  247. snackbase/infrastructure/persistence/event_listeners.py +360 -0
  248. snackbase/infrastructure/persistence/migration_service.py +652 -0
  249. snackbase/infrastructure/persistence/models/__init__.py +97 -0
  250. snackbase/infrastructure/persistence/models/account.py +112 -0
  251. snackbase/infrastructure/persistence/models/api_key.py +113 -0
  252. snackbase/infrastructure/persistence/models/audit_log.py +252 -0
  253. snackbase/infrastructure/persistence/models/codelist.py +298 -0
  254. snackbase/infrastructure/persistence/models/collection.py +67 -0
  255. snackbase/infrastructure/persistence/models/collection_rule.py +126 -0
  256. snackbase/infrastructure/persistence/models/configuration.py +240 -0
  257. snackbase/infrastructure/persistence/models/email_log.py +117 -0
  258. snackbase/infrastructure/persistence/models/email_template.py +133 -0
  259. snackbase/infrastructure/persistence/models/email_verification.py +87 -0
  260. snackbase/infrastructure/persistence/models/endpoint.py +138 -0
  261. snackbase/infrastructure/persistence/models/endpoint_execution.py +93 -0
  262. snackbase/infrastructure/persistence/models/file.py +93 -0
  263. snackbase/infrastructure/persistence/models/function.py +320 -0
  264. snackbase/infrastructure/persistence/models/group.py +105 -0
  265. snackbase/infrastructure/persistence/models/hook.py +133 -0
  266. snackbase/infrastructure/persistence/models/hook_execution.py +93 -0
  267. snackbase/infrastructure/persistence/models/invitation.py +110 -0
  268. snackbase/infrastructure/persistence/models/job.py +156 -0
  269. snackbase/infrastructure/persistence/models/macro.py +81 -0
  270. snackbase/infrastructure/persistence/models/password_reset.py +87 -0
  271. snackbase/infrastructure/persistence/models/refresh_token.py +80 -0
  272. snackbase/infrastructure/persistence/models/role.py +52 -0
  273. snackbase/infrastructure/persistence/models/token_blacklist.py +49 -0
  274. snackbase/infrastructure/persistence/models/user.py +232 -0
  275. snackbase/infrastructure/persistence/models/users_groups.py +38 -0
  276. snackbase/infrastructure/persistence/models/webhook.py +213 -0
  277. snackbase/infrastructure/persistence/models/workflow.py +115 -0
  278. snackbase/infrastructure/persistence/models/workflow_instance.py +105 -0
  279. snackbase/infrastructure/persistence/models/workflow_step_log.py +112 -0
  280. snackbase/infrastructure/persistence/record_snapshot.py +29 -0
  281. snackbase/infrastructure/persistence/repositories/__init__.py +96 -0
  282. snackbase/infrastructure/persistence/repositories/account_repository.py +324 -0
  283. snackbase/infrastructure/persistence/repositories/api_key_repository.py +166 -0
  284. snackbase/infrastructure/persistence/repositories/audit_log_repository.py +360 -0
  285. snackbase/infrastructure/persistence/repositories/codelist_repository.py +378 -0
  286. snackbase/infrastructure/persistence/repositories/collection_repository.py +194 -0
  287. snackbase/infrastructure/persistence/repositories/collection_rule_repository.py +156 -0
  288. snackbase/infrastructure/persistence/repositories/configuration_repository.py +240 -0
  289. snackbase/infrastructure/persistence/repositories/email_log_repository.py +114 -0
  290. snackbase/infrastructure/persistence/repositories/email_template_repository.py +189 -0
  291. snackbase/infrastructure/persistence/repositories/email_verification_repository.py +180 -0
  292. snackbase/infrastructure/persistence/repositories/endpoint_execution_repository.py +58 -0
  293. snackbase/infrastructure/persistence/repositories/endpoint_repository.py +185 -0
  294. snackbase/infrastructure/persistence/repositories/file_repository.py +52 -0
  295. snackbase/infrastructure/persistence/repositories/function_repository.py +304 -0
  296. snackbase/infrastructure/persistence/repositories/group_repository.py +221 -0
  297. snackbase/infrastructure/persistence/repositories/hook_execution_repository.py +81 -0
  298. snackbase/infrastructure/persistence/repositories/hook_repository.py +260 -0
  299. snackbase/infrastructure/persistence/repositories/invitation_repository.py +211 -0
  300. snackbase/infrastructure/persistence/repositories/job_repository.py +318 -0
  301. snackbase/infrastructure/persistence/repositories/macro_repository.py +160 -0
  302. snackbase/infrastructure/persistence/repositories/oauth_state_repository.py +93 -0
  303. snackbase/infrastructure/persistence/repositories/password_reset_repository.py +191 -0
  304. snackbase/infrastructure/persistence/repositories/record_repository.py +1198 -0
  305. snackbase/infrastructure/persistence/repositories/refresh_token_repository.py +137 -0
  306. snackbase/infrastructure/persistence/repositories/role_repository.py +113 -0
  307. snackbase/infrastructure/persistence/repositories/sync_audit_log_repository.py +89 -0
  308. snackbase/infrastructure/persistence/repositories/user_repository.py +449 -0
  309. snackbase/infrastructure/persistence/repositories/webhook_repository.py +235 -0
  310. snackbase/infrastructure/persistence/repositories/workflow_instance_repository.py +63 -0
  311. snackbase/infrastructure/persistence/repositories/workflow_repository.py +127 -0
  312. snackbase/infrastructure/persistence/repositories/workflow_step_log_repository.py +42 -0
  313. snackbase/infrastructure/persistence/table_builder.py +434 -0
  314. snackbase/infrastructure/realtime/__init__.py +0 -0
  315. snackbase/infrastructure/realtime/event_broadcaster.py +52 -0
  316. snackbase/infrastructure/realtime/realtime_auth.py +88 -0
  317. snackbase/infrastructure/realtime/realtime_manager.py +135 -0
  318. snackbase/infrastructure/realtime/realtime_policy.py +215 -0
  319. snackbase/infrastructure/security/__init__.py +45 -0
  320. snackbase/infrastructure/security/encryption.py +335 -0
  321. snackbase/infrastructure/security/field_encryption.py +224 -0
  322. snackbase/infrastructure/security/redaction.py +152 -0
  323. snackbase/infrastructure/security/scopes.py +67 -0
  324. snackbase/infrastructure/services/email/__init__.py +5 -0
  325. snackbase/infrastructure/services/email/aws_ses_provider.py +222 -0
  326. snackbase/infrastructure/services/email/email_provider.py +54 -0
  327. snackbase/infrastructure/services/email/resend_provider.py +198 -0
  328. snackbase/infrastructure/services/email/smtp_provider.py +129 -0
  329. snackbase/infrastructure/services/email/template_renderer.py +71 -0
  330. snackbase/infrastructure/services/email_service.py +550 -0
  331. snackbase/infrastructure/services/job_service.py +510 -0
  332. snackbase/infrastructure/services/scheduler_service.py +179 -0
  333. snackbase/infrastructure/services/token_service.py +39 -0
  334. snackbase/infrastructure/storage/__init__.py +18 -0
  335. snackbase/infrastructure/storage/base.py +49 -0
  336. snackbase/infrastructure/storage/local_storage_provider.py +54 -0
  337. snackbase/infrastructure/storage/s3_storage_provider.py +188 -0
  338. snackbase/infrastructure/storage/storage_service.py +140 -0
  339. snackbase/infrastructure/webhooks/__init__.py +17 -0
  340. snackbase/infrastructure/webhooks/webhook_service.py +572 -0
  341. snackbase/infrastructure/workflows/__init__.py +1 -0
  342. snackbase/infrastructure/workflows/workflow_executor.py +594 -0
  343. snackbase/infrastructure/workflows/workflow_trigger.py +226 -0
  344. snackbase/py.typed +0 -0
  345. snackbase/testing.py +76 -0
  346. snackbase-0.12.1.dist-info/METADATA +849 -0
  347. snackbase-0.12.1.dist-info/RECORD +350 -0
  348. snackbase-0.12.1.dist-info/WHEEL +4 -0
  349. snackbase-0.12.1.dist-info/entry_points.txt +2 -0
  350. snackbase-0.12.1.dist-info/licenses/LICENSE +633 -0
snackbase/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ """SnackBase - Open-source Backend-as-a-Service.
2
+
3
+ A self-hosted alternative to PocketBase with multi-tenancy,
4
+ row-level security, and GxP-compliant audit logging.
5
+ """
6
+
7
+ __version__ = "0.12.1"
8
+
9
+ from snackbase.infrastructure.api.app import app
10
+
11
+ __all__ = ["app", "__version__"]
snackbase/__main__.py ADDED
@@ -0,0 +1,10 @@
1
+ """Entry point for 'python -m snackbase' command.
2
+
3
+ This module allows the SnackBase CLI to be invoked using
4
+ 'python -m snackbase' or 'python -m SnackBase serve'.
5
+ """
6
+
7
+ from snackbase.cli import main
8
+
9
+ if __name__ == "__main__":
10
+ main()
@@ -0,0 +1 @@
1
+ Generic single-database configuration with an async dbapi.
@@ -0,0 +1,123 @@
1
+ import asyncio
2
+ from logging.config import fileConfig
3
+
4
+ from alembic import context
5
+ from sqlalchemy import pool
6
+ from sqlalchemy.engine import Connection
7
+ from sqlalchemy.ext.asyncio import async_engine_from_config
8
+
9
+ # Import SnackBase settings and models
10
+ from snackbase.core.config import get_settings
11
+ from snackbase.infrastructure.persistence import models # noqa: F401
12
+ from snackbase.infrastructure.persistence.database import Base
13
+
14
+ # this is the Alembic Config object, which provides
15
+ # access to the values within the .ini file in use.
16
+ config = context.config
17
+
18
+ # Interpret the config file for Python logging.
19
+ # This line sets up loggers basically.
20
+ if config.config_file_name is not None:
21
+ fileConfig(config.config_file_name)
22
+
23
+ # Set the target metadata for autogenerate support
24
+ target_metadata = Base.metadata
25
+
26
+ # Set the database URL from settings if not provided in config
27
+ # IMPORTANT: Always use the async URL here because async_engine_from_config needs it
28
+ # Alembic handles the async/sync conversion internally
29
+ settings = get_settings()
30
+ if not config.get_main_option("sqlalchemy.url"):
31
+ config.set_main_option("sqlalchemy.url", settings.database_url)
32
+
33
+
34
+ def run_migrations_offline() -> None:
35
+ """Run migrations in 'offline' mode.
36
+
37
+ This configures the context with just a URL
38
+ and not an Engine, though an Engine is acceptable
39
+ here as well. By skipping the Engine creation
40
+ we don't even need a DBAPI to be available.
41
+
42
+ Calls to context.execute() here emit the given string to the
43
+ script output.
44
+
45
+ """
46
+ url = config.get_main_option("sqlalchemy.url")
47
+ context.configure(
48
+ url=url,
49
+ target_metadata=target_metadata,
50
+ literal_binds=True,
51
+ dialect_opts={"paramstyle": "named"},
52
+ render_as_batch=True if url and url.startswith("sqlite") else False,
53
+ )
54
+
55
+ with context.begin_transaction():
56
+ context.run_migrations()
57
+
58
+
59
+ def do_run_migrations(connection: Connection) -> None:
60
+ url = config.get_main_option("sqlalchemy.url")
61
+ context.configure(
62
+ connection=connection,
63
+ target_metadata=target_metadata,
64
+ render_as_batch=True if url and url.startswith("sqlite") else False,
65
+ )
66
+
67
+ # Use existing transaction if connection is provided, otherwise start a new one
68
+ if config.attributes.get("connection"):
69
+ context.run_migrations()
70
+ else:
71
+ with context.begin_transaction():
72
+ context.run_migrations()
73
+
74
+
75
+ async def run_async_migrations() -> None:
76
+ """In this scenario we need to create an Engine
77
+ and associate a connection with the context.
78
+
79
+ """
80
+
81
+ connectable = async_engine_from_config(
82
+ config.get_section(config.config_ini_section, {}),
83
+ prefix="sqlalchemy.",
84
+ poolclass=pool.NullPool,
85
+ )
86
+
87
+ async with connectable.connect() as connection:
88
+ await connection.run_sync(do_run_migrations)
89
+
90
+ await connectable.dispose()
91
+
92
+
93
+ def run_migrations_online() -> None:
94
+ """Run migrations in 'online' mode."""
95
+
96
+ # Check if a connection was passed in the configuration attributes
97
+ # This is used by the application and tests to share a connection
98
+ connection = config.attributes.get("connection")
99
+
100
+ if connection is not None:
101
+ do_run_migrations(connection)
102
+ return
103
+
104
+ # For async support, we need to handle the event loop differently
105
+ # if it's already running (e.g. when called from application code)
106
+ try:
107
+ loop = asyncio.get_running_loop()
108
+ except RuntimeError:
109
+ loop = None
110
+
111
+ if loop and loop.is_running():
112
+ # This is used for CLI use when an event loop is already running
113
+ # (e.g. when calling from pytest-asyncio or a long-running app)
114
+ # Note: In production, it's better to run migrations before the loop starts
115
+ asyncio.ensure_future(run_async_migrations())
116
+ else:
117
+ asyncio.run(run_async_migrations())
118
+
119
+
120
+ if context.is_offline_mode():
121
+ run_migrations_offline()
122
+ else:
123
+ run_migrations_online()
@@ -0,0 +1,28 @@
1
+ """${message}
2
+
3
+ Revision ID: ${up_revision}
4
+ Revises: ${down_revision | comma,n}
5
+ Create Date: ${create_date}
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+ ${imports if imports else ""}
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = ${repr(up_revision)}
16
+ down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
17
+ branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
18
+ depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
19
+
20
+
21
+ def upgrade() -> None:
22
+ """Upgrade schema."""
23
+ ${upgrades if upgrades else "pass"}
24
+
25
+
26
+ def downgrade() -> None:
27
+ """Downgrade schema."""
28
+ ${downgrades if downgrades else "pass"}
@@ -0,0 +1,272 @@
1
+ """enhance password reset template
2
+
3
+ Revision ID: 038a7194dd07
4
+ Revises: bb38e4d658cd
5
+ Create Date: 2026-01-10 18:05:52.469527
6
+
7
+ """
8
+ from collections.abc import Sequence
9
+
10
+ import sqlalchemy as sa
11
+ from alembic import op
12
+
13
+ # revision identifiers, used by Alembic.
14
+ revision: str = '038a7194dd07'
15
+ down_revision: str | Sequence[str] | None = 'bb38e4d658cd'
16
+ branch_labels: str | Sequence[str] | None = None
17
+ depends_on: str | Sequence[str] | None = None
18
+
19
+
20
+ SYSTEM_ACCOUNT_ID = "00000000-0000-0000-0000-000000000000"
21
+
22
+ NEW_SUBJECT = "Reset your {{ app_name }} password"
23
+ NEW_HTML_BODY = """
24
+ <!DOCTYPE html>
25
+ <html>
26
+ <head>
27
+ <meta charset="UTF-8">
28
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
29
+ <title>Reset Your Password</title>
30
+ <style>
31
+ @media only screen and (max-width: 600px) {
32
+ .container { padding: 10px !important; }
33
+ .button { padding: 10px 20px !important; font-size: 14px !important; }
34
+ h1 { font-size: 24px !important; }
35
+ }
36
+ </style>
37
+ </head>
38
+ <body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f4f7fa;">
39
+ <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color: #f4f7fa; padding: 40px 20px;">
40
+ <tr>
41
+ <td align="center">
42
+ <table role="presentation" class="container" width="600" cellpadding="0" cellspacing="0" style="background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); padding: 40px;">
43
+ <!-- Header -->
44
+ <tr>
45
+ <td style="text-align: center; padding-bottom: 30px;">
46
+ <h1 style="margin: 0; color: #2c3e50; font-size: 28px; font-weight: 600;">{{ app_name }}</h1>
47
+ </td>
48
+ </tr>
49
+ <!-- Content -->
50
+ <tr>
51
+ <td>
52
+ <h2 style="margin: 0 0 20px 0; color: #2c3e50; font-size: 24px; font-weight: 600;">Reset Your Password</h2>
53
+ <p style="margin: 0 0 16px 0; color: #4a5568; font-size: 16px; line-height: 1.6;">Hello{% if user_name %} {{ user_name }}{% endif %},</p>
54
+ <p style="margin: 0 0 16px 0; color: #4a5568; font-size: 16px; line-height: 1.6;">We received a request to reset the password for your {{ app_name }} account{% if user_email %} ({{ user_email }}){% endif %}. Click the button below to create a new password:</p>
55
+ </td>
56
+ </tr>
57
+ <!-- Button -->
58
+ <tr>
59
+ <td style="text-align: center; padding: 30px 0;">
60
+ <a href="{{ reset_url }}" class="button" style="display: inline-block; background-color: #e74c3c; color: #ffffff; text-decoration: none; padding: 14px 40px; border-radius: 6px; font-size: 16px; font-weight: 600; box-shadow: 0 2px 4px rgba(231,76,60,0.3);">Reset Password</a>
61
+ </td>
62
+ </tr>
63
+ <!-- Alternative Link -->
64
+ <tr>
65
+ <td>
66
+ <p style="margin: 0 0 8px 0; color: #718096; font-size: 14px;">Or copy and paste this link into your browser:</p>
67
+ <p style="margin: 0 0 24px 0; word-break: break-all; color: #e74c3c; font-size: 14px;">{{ reset_url }}</p>
68
+ </td>
69
+ </tr>
70
+ <!-- Token Info -->
71
+ {% if token %}
72
+ <tr>
73
+ <td style="background-color: #fef5f5; padding: 16px; border-radius: 6px; border-left: 4px solid #e74c3c;">
74
+ <p style="margin: 0 0 8px 0; color: #4a5568; font-size: 14px; font-weight: 600;">Reset Code:</p>
75
+ <p style="margin: 0; color: #2d3748; font-size: 16px; font-family: 'Courier New', monospace; letter-spacing: 2px;">{{ token }}</p>
76
+ </td>
77
+ </tr>
78
+ {% endif %}
79
+ <!-- Expiration -->
80
+ {% if expires_at %}
81
+ <tr>
82
+ <td style="padding-top: 20px;">
83
+ <p style="margin: 0; color: #718096; font-size: 14px;">⏰ This password reset link will expire on {{ expires_at }}.</p>
84
+ </td>
85
+ </tr>
86
+ {% endif %}
87
+ <!-- Security Warning -->
88
+ <tr>
89
+ <td style="padding-top: 20px;">
90
+ <div style="background-color: #fff5f5; border-left: 4px solid #fc8181; padding: 16px; border-radius: 6px;">
91
+ <p style="margin: 0 0 8px 0; color: #c53030; font-size: 14px; font-weight: 600;">🔒 Security Notice</p>
92
+ <p style="margin: 0; color: #742a2a; font-size: 13px; line-height: 1.5;">If you didn't request a password reset, please ignore this email. Your password will remain unchanged.{% if ip_address %} This request was initiated from IP address: <strong>{{ ip_address }}</strong>.{% endif %} For security, we recommend changing your password if you suspect unauthorized access to your account.</p>
93
+ </div>
94
+ </td>
95
+ </tr>
96
+ <!-- Footer -->
97
+ <tr>
98
+ <td style="padding-top: 30px; border-top: 1px solid #e2e8f0; margin-top: 30px;">
99
+ <p style="margin: 0 0 8px 0; color: #a0aec0; font-size: 13px; line-height: 1.5;">This password reset was requested from your {{ app_name }} account.</p>
100
+ <p style="margin: 0; color: #a0aec0; font-size: 13px;">Need help? Visit <a href="{{ app_url }}" style="color: #3498db; text-decoration: none;">{{ app_name }}</a></p>
101
+ </td>
102
+ </tr>
103
+ </table>
104
+ </td>
105
+ </tr>
106
+ </table>
107
+ </body>
108
+ </html>
109
+ """.strip()
110
+
111
+ NEW_TEXT_BODY = """
112
+ {{ app_name }} - Reset Your Password
113
+
114
+ Hello{% if user_name %} {{ user_name }}{% endif %},
115
+
116
+ We received a request to reset the password for your {{ app_name }} account{% if user_email %} ({{ user_email }}){% endif %}. To create a new password, visit the following link:
117
+
118
+ {{ reset_url }}
119
+
120
+ {% if token %}Reset Code: {{ token }}{% endif %}
121
+
122
+ {% if expires_at %}This password reset link will expire on {{ expires_at }}.{% endif %}
123
+
124
+ SECURITY NOTICE:
125
+ If you didn't request a password reset, please ignore this email. Your password will remain unchanged.{% if ip_address %} This request was initiated from IP address: {{ ip_address }}.{% endif %} For security, we recommend changing your password if you suspect unauthorized access to your account.
126
+
127
+ This password reset was requested from your {{ app_name }} account.
128
+
129
+ Need help? Visit {{ app_url }}
130
+ """.strip()
131
+
132
+ OLD_SUBJECT = "Reset your password for {{ app_name }}"
133
+ OLD_HTML_BODY = """
134
+ <!DOCTYPE html>
135
+ <html>
136
+ <head>
137
+ <meta charset="UTF-8">
138
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
139
+ <title>Reset Your Password</title>
140
+ <style>
141
+ @media only screen and (max-width: 600px) {
142
+ .container { padding: 10px !important; }
143
+ .button { padding: 10px 20px !important; font-size: 14px !important; }
144
+ h1 { font-size: 24px !important; }
145
+ }
146
+ </style>
147
+ </head>
148
+ <body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f4f7fa;">
149
+ <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color: #f4f7fa; padding: 40px 20px;">
150
+ <tr>
151
+ <td align="center">
152
+ <table role="presentation" class="container" width="600" cellpadding="0" cellspacing="0" style="background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); padding: 40px;">
153
+ <!-- Header -->
154
+ <tr>
155
+ <td style="text-align: center; padding-bottom: 30px;">
156
+ <h1 style="margin: 0; color: #2c3e50; font-size: 28px; font-weight: 600;">{{ app_name }}</h1>
157
+ </td>
158
+ </tr>
159
+ <!-- Content -->
160
+ <tr>
161
+ <td>
162
+ <h2 style="margin: 0 0 20px 0; color: #2c3e50; font-size: 24px; font-weight: 600;">Reset Your Password</h2>
163
+ <p style="margin: 0 0 16px 0; color: #4a5568; font-size: 16px; line-height: 1.6;">Hello{% if user_name %} {{ user_name }}{% endif %},</p>
164
+ <p style="margin: 0 0 16px 0; color: #4a5568; font-size: 16px; line-height: 1.6;">We received a request to reset the password for your {{ app_name }} account{% if user_email %} ({{ user_email }}){% endif %}. Click the button below to create a new password:</p>
165
+ </td>
166
+ </tr>
167
+ <!-- Button -->
168
+ <tr>
169
+ <td style="text-align: center; padding: 30px 0;">
170
+ <a href="{{ reset_url }}" class="button" style="display: inline-block; background-color: #e74c3c; color: #ffffff; text-decoration: none; padding: 14px 40px; border-radius: 6px; font-size: 16px; font-weight: 600; box-shadow: 0 2px 4px rgba(231,76,60,0.3);">Reset Password</a>
171
+ </td>
172
+ </tr>
173
+ <!-- Alternative Link -->
174
+ <tr>
175
+ <td>
176
+ <p style="margin: 0 0 8px 0; color: #718096; font-size: 14px;">Or copy and paste this link into your browser:</p>
177
+ <p style="margin: 0 0 24px 0; word-break: break-all; color: #e74c3c; font-size: 14px;">{{ reset_url }}</p>
178
+ </td>
179
+ </tr>
180
+ <!-- Token Info -->
181
+ {% if token %}
182
+ <tr>
183
+ <td style="background-color: #fef5f5; padding: 16px; border-radius: 6px; border-left: 4px solid #e74c3c;">
184
+ <p style="margin: 0 0 8px 0; color: #4a5568; font-size: 14px; font-weight: 600;">Reset Code:</p>
185
+ <p style="margin: 0; color: #2d3748; font-size: 16px; font-family: 'Courier New', monospace; letter-spacing: 2px;">{{ token }}</p>
186
+ </td>
187
+ </tr>
188
+ {% endif %}
189
+ <!-- Expiration -->
190
+ {% if expires_at %}
191
+ <tr>
192
+ <td style="padding-top: 20px;">
193
+ <p style="margin: 0; color: #718096; font-size: 14px;">⏰ This password reset link will expire on {{ expires_at }}.</p>
194
+ </td>
195
+ </tr>
196
+ {% endif %}
197
+ <!-- Security Warning -->
198
+ <tr>
199
+ <td style="padding-top: 20px;">
200
+ <div style="background-color: #fff5f5; border-left: 4px solid #fc8181; padding: 16px; border-radius: 6px;">
201
+ <p style="margin: 0 0 8px 0; color: #c53030; font-size: 14px; font-weight: 600;">🔒 Security Notice</p>
202
+ <p style="margin: 0; color: #742a2a; font-size: 13px; line-height: 1.5;">If you didn't request a password reset, please ignore this email. Your password will remain unchanged. For security, we recommend changing your password if you suspect unauthorized access to your account.</p>
203
+ </div>
204
+ </td>
205
+ </tr>
206
+ <!-- Footer -->
207
+ <tr>
208
+ <td style="padding-top: 30px; border-top: 1px solid #e2e8f0; margin-top: 30px;">
209
+ <p style="margin: 0 0 8px 0; color: #a0aec0; font-size: 13px; line-height: 1.5;">This password reset was requested from your {{ app_name }} account.</p>
210
+ <p style="margin: 0; color: #a0aec0; font-size: 13px;">Need help? Visit <a href="{{ app_url }}" style="color: #3498db; text-decoration: none;">{{ app_name }}</a></p>
211
+ </td>
212
+ </tr>
213
+ </table>
214
+ </td>
215
+ </tr>
216
+ </table>
217
+ </body>
218
+ </html>
219
+ """.strip()
220
+
221
+ OLD_TEXT_BODY = """
222
+ {{ app_name }} - Reset Your Password
223
+
224
+ Hello{% if user_name %} {{ user_name }}{% endif %},
225
+
226
+ We received a request to reset the password for your {{ app_name }} account{% if user_email %} ({{ user_email }}){% endif %}. To create a new password, visit the following link:
227
+
228
+ {{ reset_url }}
229
+
230
+ {% if token %}Reset Code: {{ token }}{% endif %}
231
+
232
+ {% if expires_at %}This password reset link will expire on {{ expires_at }}.{% endif %}
233
+
234
+ SECURITY NOTICE:
235
+ If you didn't request a password reset, please ignore this email. Your password will remain unchanged. For security, we recommend changing your password if you suspect unauthorized access to your account.
236
+
237
+ This password reset was requested from your {{ app_name }} account.
238
+
239
+ Need help? Visit {{ app_url }}
240
+ """.strip()
241
+
242
+
243
+ def upgrade() -> None:
244
+ """Upgrade schema."""
245
+ op.execute(
246
+ sa.text(
247
+ "UPDATE email_templates "
248
+ "SET subject = :subject, html_body = :html_body, text_body = :text_body, updated_at = CURRENT_TIMESTAMP "
249
+ "WHERE account_id = :account_id AND template_type = 'password_reset'"
250
+ ).bindparams(
251
+ subject=NEW_SUBJECT,
252
+ html_body=NEW_HTML_BODY,
253
+ text_body=NEW_TEXT_BODY,
254
+ account_id=SYSTEM_ACCOUNT_ID
255
+ )
256
+ )
257
+
258
+
259
+ def downgrade() -> None:
260
+ """Downgrade schema."""
261
+ op.execute(
262
+ sa.text(
263
+ "UPDATE email_templates "
264
+ "SET subject = :subject, html_body = :html_body, text_body = :text_body, updated_at = CURRENT_TIMESTAMP "
265
+ "WHERE account_id = :account_id AND template_type = 'password_reset'"
266
+ ).bindparams(
267
+ subject=OLD_SUBJECT,
268
+ html_body=OLD_HTML_BODY,
269
+ text_body=OLD_TEXT_BODY,
270
+ account_id=SYSTEM_ACCOUNT_ID
271
+ )
272
+ )
@@ -0,0 +1,131 @@
1
+ """create_collection_rules_table
2
+
3
+ Revision ID: 084e50e30ae8
4
+ Revises: 7bccaa56da18
5
+ Create Date: 2026-01-19 00:19:12.172897
6
+
7
+ """
8
+
9
+ from collections.abc import Sequence
10
+
11
+ import sqlalchemy as sa
12
+ from alembic import op
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = "084e50e30ae8"
16
+ down_revision: str | Sequence[str] | None = "7bccaa56da18"
17
+ branch_labels: str | Sequence[str] | None = None
18
+ depends_on: str | Sequence[str] | None = None
19
+
20
+
21
+ def upgrade() -> None:
22
+ """Create collection_rules table."""
23
+ op.create_table(
24
+ "collection_rules",
25
+ sa.Column(
26
+ "id", sa.String(length=36), nullable=False, comment="Collection rule ID (UUID)"
27
+ ),
28
+ sa.Column(
29
+ "collection_id",
30
+ sa.String(length=36),
31
+ nullable=False,
32
+ comment="Foreign key to collections table",
33
+ ),
34
+ # 5 operation rules (NULL = locked, "" = public, "expr" = filter)
35
+ sa.Column(
36
+ "list_rule",
37
+ sa.Text(),
38
+ nullable=True,
39
+ comment="Filter expression for listing records",
40
+ ),
41
+ sa.Column(
42
+ "view_rule",
43
+ sa.Text(),
44
+ nullable=True,
45
+ comment="Filter expression for viewing single record",
46
+ ),
47
+ sa.Column(
48
+ "create_rule",
49
+ sa.Text(),
50
+ nullable=True,
51
+ comment="Validation expression for creating records",
52
+ ),
53
+ sa.Column(
54
+ "update_rule",
55
+ sa.Text(),
56
+ nullable=True,
57
+ comment="Filter/validation expression for updates",
58
+ ),
59
+ sa.Column(
60
+ "delete_rule",
61
+ sa.Text(),
62
+ nullable=True,
63
+ comment="Filter expression for deletions",
64
+ ),
65
+ # Field-level permissions per operation
66
+ sa.Column(
67
+ "list_fields",
68
+ sa.Text(),
69
+ nullable=False,
70
+ server_default="*",
71
+ comment="Fields visible in list operations (JSON array or '*')",
72
+ ),
73
+ sa.Column(
74
+ "view_fields",
75
+ sa.Text(),
76
+ nullable=False,
77
+ server_default="*",
78
+ comment="Fields visible in view operations (JSON array or '*')",
79
+ ),
80
+ sa.Column(
81
+ "create_fields",
82
+ sa.Text(),
83
+ nullable=False,
84
+ server_default="*",
85
+ comment="Fields allowed in create requests (JSON array or '*')",
86
+ ),
87
+ sa.Column(
88
+ "update_fields",
89
+ sa.Text(),
90
+ nullable=False,
91
+ server_default="*",
92
+ comment="Fields allowed in update requests (JSON array or '*')",
93
+ ),
94
+ # Timestamps
95
+ sa.Column(
96
+ "created_at",
97
+ sa.DateTime(),
98
+ nullable=False,
99
+ server_default=sa.text("(CURRENT_TIMESTAMP)"),
100
+ ),
101
+ sa.Column(
102
+ "updated_at",
103
+ sa.DateTime(),
104
+ nullable=False,
105
+ server_default=sa.text("(CURRENT_TIMESTAMP)"),
106
+ ),
107
+ # Constraints
108
+ sa.ForeignKeyConstraint(
109
+ ["collection_id"],
110
+ ["collections.id"],
111
+ ondelete="CASCADE",
112
+ ),
113
+ sa.PrimaryKeyConstraint("id"),
114
+ sa.UniqueConstraint("collection_id", name="uq_collection_rules_collection_id"),
115
+ )
116
+
117
+ # Create index on collection_id for performance
118
+ with op.batch_alter_table("collection_rules", schema=None) as batch_op:
119
+ batch_op.create_index(
120
+ batch_op.f("ix_collection_rules_collection_id"),
121
+ ["collection_id"],
122
+ unique=False,
123
+ )
124
+
125
+
126
+ def downgrade() -> None:
127
+ """Drop collection_rules table."""
128
+ with op.batch_alter_table("collection_rules", schema=None) as batch_op:
129
+ batch_op.drop_index(batch_op.f("ix_collection_rules_collection_id"))
130
+
131
+ op.drop_table("collection_rules")
@@ -0,0 +1,39 @@
1
+ """add_email_sent_to_invitations
2
+
3
+ Revision ID: 0ea276bf29bc
4
+ Revises: 038a7194dd07
5
+ Create Date: 2026-01-10 18:58:33.816670
6
+
7
+ """
8
+ from collections.abc import Sequence
9
+
10
+ import sqlalchemy as sa
11
+ from alembic import op
12
+
13
+ # revision identifiers, used by Alembic.
14
+ revision: str = '0ea276bf29bc'
15
+ down_revision: str | Sequence[str] | None = '038a7194dd07'
16
+ branch_labels: str | Sequence[str] | None = None
17
+ depends_on: str | Sequence[str] | None = None
18
+
19
+
20
+ def upgrade() -> None:
21
+ """Upgrade schema."""
22
+ # ### commands auto generated by Alembic - please adjust! ###
23
+ with op.batch_alter_table('invitations', schema=None) as batch_op:
24
+ batch_op.add_column(sa.Column('email_sent', sa.Boolean(), nullable=False, server_default=sa.text('false'), comment='Whether the invitation email has been sent'))
25
+ batch_op.add_column(sa.Column('email_sent_at', sa.DateTime(), nullable=True, comment='Timestamp when the email was sent'))
26
+ batch_op.create_index(batch_op.f('ix_invitations_email_sent'), ['email_sent'], unique=False)
27
+
28
+ # ### end Alembic commands ###
29
+
30
+
31
+ def downgrade() -> None:
32
+ """Downgrade schema."""
33
+ # ### commands auto generated by Alembic - please adjust! ###
34
+ with op.batch_alter_table('invitations', schema=None) as batch_op:
35
+ batch_op.drop_index(batch_op.f('ix_invitations_email_sent'))
36
+ batch_op.drop_column('email_sent_at')
37
+ batch_op.drop_column('email_sent')
38
+
39
+ # ### end Alembic commands ###