@taodigital/database 0.0.0-stage → 1.2.0

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 (173) hide show
  1. package/README.md +21 -2
  2. package/dist/index.d.ts +58 -0
  3. package/dist/index.js +209 -0
  4. package/package.json +29 -4
  5. package/prisma/migrations/0000_init/migration.sql +220 -0
  6. package/prisma/migrations/0001_tenant_isolation/migration.sql +43 -0
  7. package/prisma/migrations/0002_app_runtime_role/migration.sql +14 -0
  8. package/prisma/migrations/0003_skill_taxonomy/migration.sql +30 -0
  9. package/prisma/migrations/0004_extended_models/migration.sql +215 -0
  10. package/prisma/migrations/0005_enhanced_test_artifacts/migration.sql +24 -0
  11. package/prisma/migrations/0006_tenant_settings/migration.sql +28 -0
  12. package/prisma/migrations/0007_project_slug/migration.sql +11 -0
  13. package/prisma/migrations/0008_project_crawl_config/migration.sql +3 -0
  14. package/prisma/migrations/0009_tenant_slug/migration.sql +11 -0
  15. package/prisma/migrations/0010_user_password/migration.sql +3 -0
  16. package/prisma/migrations/0011_oauth_connections/migration.sql +78 -0
  17. package/prisma/migrations/0012_agent_webhooks/migration.sql +46 -0
  18. package/prisma/migrations/0013_project_artifacts/migration.sql +27 -0
  19. package/prisma/migrations/0014_knowledge_chunks/migration.sql +37 -0
  20. package/prisma/migrations/20260626022056_platform_job_migration/migration.sql +44 -0
  21. package/prisma/migrations/20260626053339_kag_schema_update/migration.sql +27 -0
  22. package/prisma/migrations/20260626053340_restore_bm25_index/migration.sql +13 -0
  23. package/prisma/migrations/20260626063704_token_optimisation/migration.sql +86 -0
  24. package/prisma/migrations/20260626080000_job_name/migration.sql +2 -0
  25. package/prisma/migrations/20260626090000_rls_missing_tables/migration.sql +39 -0
  26. package/prisma/migrations/20260626100000_token_budget/migration.sql +21 -0
  27. package/prisma/migrations/20260626110000_screen_groups/migration.sql +42 -0
  28. package/prisma/migrations/20260626120000_artifact_intelligence/migration.sql +5 -0
  29. package/prisma/migrations/20260626130000_notifications/migration.sql +34 -0
  30. package/prisma/migrations/20260627100000_element_viewport/migration.sql +2 -0
  31. package/prisma/migrations/20260627100000_screenshot_cloud_url/migration.sql +2 -0
  32. package/prisma/migrations/20260627110000_screenshot_index/migration.sql +2 -0
  33. package/prisma/migrations/20260627140000_artifact_storage_key/migration.sql +3 -0
  34. package/prisma/migrations/20260627150000_screen_lifecycle/migration.sql +5 -0
  35. package/prisma/migrations/20260627160000_native_app_support/migration.sql +41 -0
  36. package/prisma/migrations/20260627200000_rls_missing_tables/migration.sql +57 -0
  37. package/prisma/migrations/20260627210000_llmcalllog_tasktype/migration.sql +1 -0
  38. package/prisma/migrations/20260628100000_element_enrichment/migration.sql +7 -0
  39. package/prisma/migrations/20260628110000_element_hash/migration.sql +6 -0
  40. package/prisma/migrations/20260628120000_screen_diff/migration.sql +20 -0
  41. package/prisma/migrations/20260628130000_probe_observations/migration.sql +19 -0
  42. package/prisma/migrations/20260629100000_visual_baseline_masking/migration.sql +1 -0
  43. package/prisma/migrations/20260629110000_testcase_viewports/migration.sql +1 -0
  44. package/prisma/migrations/20260630100000_visual_baseline_element_snapshot/migration.sql +1 -0
  45. package/prisma/migrations/20260630110000_git_branched_baselines/migration.sql +11 -0
  46. package/prisma/migrations/20260630120000_testcase_viewport_constraint/migration.sql +1 -0
  47. package/prisma/migrations/20260630130000_test_case_runs/migration.sql +22 -0
  48. package/prisma/migrations/20260630140000_perf_env_comments/migration.sql +68 -0
  49. package/prisma/migrations/20260630150000_testcase_gen_at/migration.sql +2 -0
  50. package/prisma/migrations/20260630160000_rls_catchup_late_tables/migration.sql +41 -0
  51. package/prisma/migrations/20260701100000_skill_file_diff_approval/migration.sql +9 -0
  52. package/prisma/migrations/20260701200000_workspace_connector/migration.sql +20 -0
  53. package/prisma/migrations/20260701220000_screen_a11y_security/migration.sql +2 -0
  54. package/prisma/migrations/20260702100000_ci_threshold_root_cause/migration.sql +2 -0
  55. package/prisma/migrations/20260702120000_medium_priority_gaps/migration.sql +28 -0
  56. package/prisma/migrations/20260702140000_testcase_v2_technique/migration.sql +7 -0
  57. package/prisma/migrations/20260702150000_add_app_field/migration.sql +40 -0
  58. package/prisma/migrations/20260702160000_add_requirement_model/migration.sql +62 -0
  59. package/prisma/migrations/20260702180000_add_invitation_model/migration.sql +53 -0
  60. package/prisma/migrations/20260702190000_suite_hierarchy_artifact_trace/migration.sql +28 -0
  61. package/prisma/migrations/20260702200000_project_member_access/migration.sql +31 -0
  62. package/prisma/migrations/20260703000000_add_system_config/migration.sql +11 -0
  63. package/prisma/migrations/20260703000000_custom_roles/migration.sql +29 -0
  64. package/prisma/migrations/20260703100000_platform_admin/migration.sql +11 -0
  65. package/prisma/migrations/20260703110000_add_extension_models/migration.sql +106 -0
  66. package/prisma/migrations/20260703120000_project_restricted_column/migration.sql +1 -0
  67. package/prisma/migrations/20260703200000_system_config_category_index/migration.sql +1 -0
  68. package/prisma/migrations/20260703210000_workspace_layer/migration.sql +24 -0
  69. package/prisma/migrations/20260703230000_workspace_agent_tokens/migration.sql +2 -0
  70. package/prisma/migrations/20260704000000_requirement_rls_policy/migration.sql +35 -0
  71. package/prisma/migrations/20260704100000_fix_oauth_auditlog_cascade/migration.sql +9 -0
  72. package/prisma/migrations/20260704110000_skillfile_approval_status/migration.sql +2 -0
  73. package/prisma/migrations/20260704120000_scenario_suspect_workspace/migration.sql +35 -0
  74. package/prisma/migrations/20260704130000_testcase_traceto_suspect_indexes/migration.sql +5 -0
  75. package/prisma/migrations/20260704140000_workspace_rls/migration.sql +19 -0
  76. package/prisma/migrations/20260704150000_project_cascade_deletes/migration.sql +125 -0
  77. package/prisma/migrations/20260705100000_platform_job_worker_id/migration.sql +12 -0
  78. package/prisma/migrations/20260705110000_screen_discovery_method/migration.sql +7 -0
  79. package/prisma/migrations/20260705120000_webhook_delivery/migration.sql +32 -0
  80. package/prisma/migrations/20260705130000_tenant_feature_flags/migration.sql +23 -0
  81. package/prisma/migrations/20260705140000_auth_sessions/migration.sql +30 -0
  82. package/prisma/migrations/20260705150000_fix_rls_wrong_session_var/migration.sql +51 -0
  83. package/prisma/migrations/20260705160000_testcase_flaky_quarantine/migration.sql +14 -0
  84. package/prisma/migrations/20260705170000_testcaserun_browser/migration.sql +6 -0
  85. package/prisma/migrations/20260705180000_allow_custom_roles/migration.sql +22 -0
  86. package/prisma/migrations/20260706050000_screen_markdown_extraction/migration.sql +8 -0
  87. package/prisma/migrations/20260706090000_add_lighthouse_to_performance_snapshot/migration.sql +4 -0
  88. package/prisma/migrations/20260706100000_add_lighthouse_bp_and_job_index/migration.sql +5 -0
  89. package/prisma/migrations/20260706110000_add_visual_diff_ai_summary/migration.sql +2 -0
  90. package/prisma/migrations/20260707120000_script_bundles/migration.sql +25 -0
  91. package/prisma/migrations/20260707130000_requirement_source_section/migration.sql +4 -0
  92. package/prisma/migrations/20260707140000_structured_skill_metadata/migration.sql +8 -0
  93. package/prisma/migrations/20260707150000_crawler_platform_primitives/migration.sql +112 -0
  94. package/prisma/migrations/20260707160000_screen_group_versioning/migration.sql +9 -0
  95. package/prisma/migrations/20260707170000_add_mfa_fields/migration.sql +24 -0
  96. package/prisma/migrations/20260707180000_add_missing_tables/migration.sql +158 -0
  97. package/prisma/migrations/20260707190000_add_missing_columns/migration.sql +15 -0
  98. package/prisma/migrations/20260707200000_add_plan_sessions/migration.sql +1 -0
  99. package/prisma/migrations/20260708100000_missing_columns_comprehensive/migration.sql +15 -0
  100. package/prisma/migrations/20260708200000_testcase_dataset_column/migration.sql +16 -0
  101. package/prisma/migrations/20260709100000_add_filename_to_native_app_build/migration.sql +1 -0
  102. package/prisma/migrations/20260709200000_cloud_url_visual_artifacts/migration.sql +6 -0
  103. package/prisma/migrations/20260714200000_add_cached_scenarios_to_artifact/migration.sql +4 -0
  104. package/prisma/migrations/20260715200000_screen_markdown_content/migration.sql +2 -0
  105. package/prisma/migrations/20260716100000_github_repo_coverage_tables/migration.sql +114 -0
  106. package/prisma/migrations/20260716200000_github_pr_script_validation/migration.sql +48 -0
  107. package/prisma/migrations/20260716210000_s3_content_keys/migration.sql +20 -0
  108. package/prisma/migrations/20260717220000_ide_push_token/migration.sql +21 -0
  109. package/prisma/migrations/20260717230000_ide_push_script_ids/migration.sql +2 -0
  110. package/prisma/migrations/20260730040000_add_step_evidence_and_validation/migration.sql +93 -0
  111. package/prisma/migrations/20260803000000_add_tenantId_to_test_data_row/migration.sql +40 -0
  112. package/prisma/migrations/20260804000000_add_project_restricted/migration.sql +6 -0
  113. package/prisma/migrations/20260804000000_add_user_avatar_url/migration.sql +4 -0
  114. package/prisma/migrations/20260805000000_add_auditlog_ipaddress/migration.sql +2 -0
  115. package/prisma/migrations/20260807000000_add_test_data_classification/migration.sql +18 -0
  116. package/prisma/migrations/20260807000001_add_test_case_notes/migration.sql +5 -0
  117. package/prisma/migrations/20260807000002_add_visual_baseline_browser_structured_steps/migration.sql +11 -0
  118. package/prisma/migrations/20260807100000_add_tenant_plan_config/migration.sql +14 -0
  119. package/prisma/migrations/20260808000000_agent_token_missing_columns/migration.sql +22 -0
  120. package/prisma/migrations/20260808000001_visual_diff_image_and_threshold/migration.sql +8 -0
  121. package/prisma/migrations/20260808000002_ssoconfig_email_domains/migration.sql +7 -0
  122. package/prisma/migrations/20260808000003_project_status_description_updatedat/migration.sql +7 -0
  123. package/prisma/migrations/20260814000000_workspace_require_mfa/migration.sql +2 -0
  124. package/prisma/migrations/20260814000001_add_screenshot_dpr/migration.sql +2 -0
  125. package/prisma/migrations/20260818000000_add_content_hash_to_test_case/migration.sql +6 -0
  126. package/prisma/migrations/20260818000001_add_grounding_hash_to_test_case/migration.sql +3 -0
  127. package/prisma/migrations/20260819000000_coverage_mapping_cross_encoder_fields/migration.sql +3 -0
  128. package/prisma/migrations/20260820000000_add_locators_json_to_test_case/migration.sql +6 -0
  129. package/prisma/migrations/20260820000001_add_test_case_version/migration.sql +27 -0
  130. package/prisma/migrations/20260826000000_add_capture_step_screenshots/migration.sql +1 -0
  131. package/prisma/migrations/20260903000000_add_platform_job_dlq/migration.sql +10 -0
  132. package/prisma/migrations/20260903000002_add_versioning_tables/migration.sql +38 -0
  133. package/prisma/migrations/20260903000003_add_learning_models/migration.sql +71 -0
  134. package/prisma/migrations/20260903000004_add_workspace_agent_prompts/migration.sql +33 -0
  135. package/prisma/migrations/20260903000005_add_config_history/migration.sql +70 -0
  136. package/prisma/migrations/20260903000006_add_skill_file_config/migration.sql +24 -0
  137. package/prisma/migrations/20260904000000_add_agent_prompt_platform_table/migration.sql +23 -0
  138. package/prisma/migrations/20260904180424_rbac_role_configs_admin_roles/migration.sql +606 -0
  139. package/prisma/migrations/20260907000000_fix_testcase_timestamps/migration.sql +13 -0
  140. package/prisma/migrations/20260907000001_add_artifact_imported_suite_id/migration.sql +1 -0
  141. package/prisma/migrations/20260907000002_add_user_login_tracking/migration.sql +9 -0
  142. package/prisma/migrations/20260907000003_add_artifact_import_tracking/migration.sql +9 -0
  143. package/prisma/migrations/20260907000004_add_artifact_version_chain/migration.sql +18 -0
  144. package/prisma/migrations/20260908000000_add_project_vsa_enabled/migration.sql +2 -0
  145. package/prisma/migrations/20260908000001_rbac_migration_catchup/migration.sql +314 -0
  146. package/prisma/migrations/20260908000002_fix_role_configs_empty_permissions/migration.sql +22 -0
  147. package/prisma/migrations/20260908000003_sync_builtin_role_permissions/migration.sql +38 -0
  148. package/prisma/migrations/20260909000000_add_filepath_to_testscript/migration.sql +5 -0
  149. package/prisma/migrations/20260909000001_add_alignment_result_table/migration.sql +31 -0
  150. package/prisma/migrations/20260910000000_add_admin_mfa/migration.sql +35 -0
  151. package/prisma/migrations/20260910000000_add_dom_snapshot_cloud_url/migration.sql +2 -0
  152. package/prisma/migrations/20260910000001_add_requires_auth_to_screen/migration.sql +1 -0
  153. package/prisma/migrations/20260910000002_add_screen_requires_auth/migration.sql +2 -0
  154. package/prisma/migrations/20260910000003_add_admin_auth_sessions/migration.sql +17 -0
  155. package/prisma/migrations/20260916000001_add_admin_last_login/migration.sql +1 -0
  156. package/prisma/migrations/20260916000002_add_admin_failed_login/migration.sql +2 -0
  157. package/prisma/migrations/20260916000003_seed_system_tenant/migration.sql +6 -0
  158. package/prisma/migrations/20260918000000_fix_permissions_comprehensive/migration.sql +63 -0
  159. package/prisma/migrations/20260924000000_add_screen_role_access/migration.sql +2 -0
  160. package/prisma/migrations/20260928000000_add_navigation_graph/migration.sql +41 -0
  161. package/prisma/migrations/20260929000000_artifact_content_hash_suite_unique/migration.sql +25 -0
  162. package/prisma/migrations/20260929000000_navigation_graph_unique_constraint/migration.sql +5 -0
  163. package/prisma/migrations/20260929000001_add_membership_status/migration.sql +7 -0
  164. package/prisma/migrations/20260929000002_add_sso_config_scim_token_hash/migration.sql +8 -0
  165. package/prisma/migrations/20261005000001_jobs_workers_hardening/migration.sql +63 -0
  166. package/prisma/migrations/20261009000001_add_suite_category/migration.sql +2 -0
  167. package/prisma/migrations/20261009000002_add_testcase_approved_fields/migration.sql +3 -0
  168. package/prisma/migrations/20261012_auto_version_history/migration.sql +111 -0
  169. package/prisma/migrations/20261015000001_traceability_alignment_gaps/migration.sql +11 -0
  170. package/prisma/migrations/20261028000001_context_intelligence_p1_p7/migration.sql +76 -0
  171. package/prisma/migrations/migration_lock.toml +3 -0
  172. package/prisma/schema.prisma +2901 -0
  173. package/prisma/seed.ts +1262 -0
package/README.md CHANGED
@@ -1,3 +1,22 @@
1
- # Temporary Holding Version
1
+ # @detiq/database — The Data Model & Zeta Core
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ This package defines the "sealed boxes" (Workspace -> Tenant -> Project) and
4
+ the **Zeta Core** (Screens, Elements, Flows, Skill Files, Test artifacts).
5
+
6
+ ## Why a normal database (not a vector database)?
7
+ Because the workspace is **closed and bounded**, we ground the AI by loading a
8
+ project's knowledge **directly** (see `@detiq/app-brain`). A normal Postgres
9
+ database is simpler, cheaper, precise, and easy to isolate per tenant. We add
10
+ vector search later **only** for projects flagged `oversized = true`.
11
+
12
+ ## Tenant isolation is enforced HERE, at the database layer
13
+ `migrations/0001_tenant_isolation/migration.sql` turns on Row-Level Security.
14
+ Every query must run inside `withTenant(tenantId, ...)`. If you forget, the
15
+ database returns nothing — the safe failure mode.
16
+
17
+ ## Commands
18
+ ```
19
+ npm run -w @detiq/database generate # build the Prisma client
20
+ npm run -w @detiq/database migrate # create/update tables
21
+ npm run -w @detiq/database studio # visual database browser
22
+ ```
@@ -0,0 +1,58 @@
1
+ import { Prisma, PrismaClient } from '@prisma/client';
2
+ /**
3
+ * A single shared Prisma client for the whole app.
4
+ * Runs as qip_app (subject to RLS — use withTenant() to set tenant context).
5
+ */
6
+ export declare const prisma: PrismaClient<{
7
+ datasources: {
8
+ db: {
9
+ url: string;
10
+ };
11
+ };
12
+ }, never, import("@prisma/client/runtime/library").DefaultArgs>;
13
+ /**
14
+ * DANGER: Bypasses Row-Level Security. Returns data across ALL tenants.
15
+ *
16
+ * Cross-tenant data leak if used in a tenant-scoped route handler.
17
+ * Approved use sites ONLY:
18
+ * - auth.ts — user lookup before tenant is known (login/signup/password)
19
+ * - admin.ts — platform admin routes
20
+ * - tenant.ts — token resolution before tenant context established
21
+ * - ci.ts — API key resolution before tenant known
22
+ * - session.service.ts — session management
23
+ *
24
+ * Everywhere else: use withTenant() + the RLS-scoped `prisma` client.
25
+ */
26
+ export declare const prismaAdmin: PrismaClient<{
27
+ datasources: {
28
+ db: {
29
+ url: string;
30
+ };
31
+ };
32
+ }, never, import("@prisma/client/runtime/library").DefaultArgs>;
33
+ /**
34
+ * Evict a SILO client from the in-process cache (e.g. after re-provisioning).
35
+ * Call from the admin provisioning endpoint so the next withTenant() picks up
36
+ * the new connection string.
37
+ */
38
+ export declare function evictSiloClient(tenantId: string): void;
39
+ /**
40
+ * Run database work "as a tenant".
41
+ *
42
+ * POOL path (default): sets the Postgres session variable `app.current_tenant`
43
+ * so Row-Level Security policies filter every query to that tenant only.
44
+ *
45
+ * SILO path (when TenantSiloConfig exists): uses a dedicated PrismaClient
46
+ * pointed at the tenant's own database — RLS is not needed there because the
47
+ * database itself contains only that tenant's data.
48
+ *
49
+ * If you forget to wrap a query in withTenant(), the RLS policy returns
50
+ * NOTHING — which is the safe failure mode.
51
+ *
52
+ * Usage:
53
+ * const screens = await withTenant(tenantId, (tx) =>
54
+ * tx.screen.findMany({ where: { projectId } })
55
+ * );
56
+ */
57
+ export declare function withTenant<T>(tenantId: string, work: (tx: Prisma.TransactionClient) => Promise<T>): Promise<T>;
58
+ export * from '@prisma/client';
package/dist/index.js ADDED
@@ -0,0 +1,209 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
36
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.prismaAdmin = exports.prisma = void 0;
40
+ exports.evictSiloClient = evictSiloClient;
41
+ exports.withTenant = withTenant;
42
+ const client_1 = require("@prisma/client");
43
+ const node_crypto_1 = __importStar(require("node:crypto"));
44
+ // Inline decrypt mirroring packages/core/src/secrets.ts — avoids a circular
45
+ // dependency between @taodigital/database and @taodigital/core.
46
+ function _decryptConnectionString(ciphertext) {
47
+ const raw = process.env.ENCRYPTION_KEY ?? '';
48
+ if (!raw)
49
+ throw new Error('ENCRYPTION_KEY is not set');
50
+ const salt = process.env.HKDF_SALT ? Buffer.from(process.env.HKDF_SALT, 'hex') : Buffer.alloc(32);
51
+ const key = Buffer.from((0, node_crypto_1.hkdfSync)('sha256', Buffer.from(raw, 'utf8'), salt, 'zeta-aes-256-gcm-v2', 32));
52
+ const buf = Buffer.from(ciphertext, 'base64');
53
+ const iv = buf.subarray(0, 12);
54
+ const tag = buf.subarray(12, 28);
55
+ const data = buf.subarray(28);
56
+ const decipher = node_crypto_1.default.createDecipheriv('aes-256-gcm', key, iv);
57
+ decipher.setAuthTag(tag);
58
+ return Buffer.concat([decipher.update(data), decipher.final()]).toString('utf8');
59
+ }
60
+ /**
61
+ * A single shared Prisma client for the whole app.
62
+ * Runs as qip_app (subject to RLS — use withTenant() to set tenant context).
63
+ */
64
+ // Neon's PgBouncer transaction-mode pooler limits concurrent interactive transactions.
65
+ // Default 25 is fine for API-only load but crawls issue ~4 writes/screen (saveScreen,
66
+ // saveElement×N, saveScreenshot, saveActionRecipe) — at 30+ pages that's 120+ concurrent
67
+ // writes. Raise via DATABASE_CONNECTION_LIMIT env var; 50 is safe for most Neon plans.
68
+ exports.prisma = new client_1.PrismaClient({
69
+ datasources: { db: { url: process.env.DATABASE_URL + (process.env.DATABASE_URL?.includes('?') ? '&' : '?') + `connection_limit=${process.env.DATABASE_CONNECTION_LIMIT ?? '50'}&pool_timeout=${process.env.DATABASE_POOL_TIMEOUT ?? '60'}` } },
70
+ });
71
+ /**
72
+ * DANGER: Bypasses Row-Level Security. Returns data across ALL tenants.
73
+ *
74
+ * Cross-tenant data leak if used in a tenant-scoped route handler.
75
+ * Approved use sites ONLY:
76
+ * - auth.ts — user lookup before tenant is known (login/signup/password)
77
+ * - admin.ts — platform admin routes
78
+ * - tenant.ts — token resolution before tenant context established
79
+ * - ci.ts — API key resolution before tenant known
80
+ * - session.service.ts — session management
81
+ *
82
+ * Everywhere else: use withTenant() + the RLS-scoped `prisma` client.
83
+ */
84
+ exports.prismaAdmin = new client_1.PrismaClient({
85
+ datasources: { db: { url: (() => { const base = process.env.MIGRATION_DATABASE_URL || process.env.DATABASE_URL || ''; return base + (base.includes('?') ? '&' : '?') + 'connection_limit=10&pool_timeout=30'; })() } },
86
+ });
87
+ // ---- SILO routing -----------------------------------------------------------
88
+ //
89
+ // When a tenant has a TenantSiloConfig row, withTenant() routes their queries
90
+ // to a dedicated Prisma client pointed at the tenant's own database instead of
91
+ // the shared POOL. The cache avoids a DB lookup on every request.
92
+ //
93
+ // The SILO path is purely additive — absence of a TenantSiloConfig row falls
94
+ // through to the existing POOL path unchanged. The connection string is stored
95
+ // encrypted; the admin provisioning endpoint decrypts it before writing to the
96
+ // cache here. See POST /admin/tenants/:tenantId/silo-provision.
97
+ const siloClientCache = new Map();
98
+ const SILO_CACHE_MAX = 50; // evict oldest when cap reached
99
+ async function getSiloClient(tenantId) {
100
+ if (siloClientCache.has(tenantId))
101
+ return siloClientCache.get(tenantId);
102
+ let config;
103
+ try {
104
+ config = await exports.prismaAdmin.tenantSiloConfig.findUnique({
105
+ where: { tenantId },
106
+ select: { connectionString_enc: true },
107
+ });
108
+ }
109
+ catch {
110
+ // TenantSiloConfig table may not exist yet (migration pending) — fall back to POOL path.
111
+ return null;
112
+ }
113
+ if (!config)
114
+ return null;
115
+ // Decrypt the connection string before use. The admin provisioning endpoint
116
+ // encrypts with @taodigital/core encrypt() (AES-256-GCM) before storing, so we
117
+ // must decrypt here. Falls back to the raw value for any unencrypted legacy rows.
118
+ let connectionUrl;
119
+ try {
120
+ connectionUrl = _decryptConnectionString(config.connectionString_enc);
121
+ }
122
+ catch {
123
+ // Not a valid ciphertext — treat as plain text (backwards compat for any
124
+ // rows written before encryption was enforced).
125
+ connectionUrl = config.connectionString_enc;
126
+ }
127
+ const client = new client_1.PrismaClient({
128
+ datasources: { db: { url: connectionUrl + (connectionUrl.includes('?') ? '&' : '?') + 'connection_limit=10&pool_timeout=30' } },
129
+ });
130
+ siloClientCache.set(tenantId, client);
131
+ if (siloClientCache.size > SILO_CACHE_MAX) {
132
+ const [oldestKey, oldestClient] = siloClientCache.entries().next().value;
133
+ siloClientCache.delete(oldestKey);
134
+ oldestClient.$disconnect().catch(() => { });
135
+ }
136
+ return client;
137
+ }
138
+ /**
139
+ * Evict a SILO client from the in-process cache (e.g. after re-provisioning).
140
+ * Call from the admin provisioning endpoint so the next withTenant() picks up
141
+ * the new connection string.
142
+ */
143
+ function evictSiloClient(tenantId) {
144
+ siloClientCache.delete(tenantId);
145
+ }
146
+ // ---- withTenant -------------------------------------------------------------
147
+ /**
148
+ * Run database work "as a tenant".
149
+ *
150
+ * POOL path (default): sets the Postgres session variable `app.current_tenant`
151
+ * so Row-Level Security policies filter every query to that tenant only.
152
+ *
153
+ * SILO path (when TenantSiloConfig exists): uses a dedicated PrismaClient
154
+ * pointed at the tenant's own database — RLS is not needed there because the
155
+ * database itself contains only that tenant's data.
156
+ *
157
+ * If you forget to wrap a query in withTenant(), the RLS policy returns
158
+ * NOTHING — which is the safe failure mode.
159
+ *
160
+ * Usage:
161
+ * const screens = await withTenant(tenantId, (tx) =>
162
+ * tx.screen.findMany({ where: { projectId } })
163
+ * );
164
+ */
165
+ async function withTenant(tenantId, work) {
166
+ // Check for a dedicated SILO database for this tenant.
167
+ const siloClient = await getSiloClient(tenantId);
168
+ if (siloClient) {
169
+ // SILO path: the dedicated DB has no RLS, so just wrap in a transaction.
170
+ return siloClient.$transaction((tx) => work(tx));
171
+ }
172
+ // POOL path — retry up to 3 times on Neon connection-reset errors.
173
+ // Neon's serverless pooler occasionally drops idle connections mid-transaction
174
+ // ("Transaction not found", "prepared statement does not exist"), requiring a retry.
175
+ const isRetryable = (e) => {
176
+ const msg = e?.message ?? '';
177
+ const code = e?.code ?? '';
178
+ return (code === 'P2028' || // pool timeout / tx start failed
179
+ msg.includes('Transaction not found') ||
180
+ msg.includes('Transaction ID is invalid') ||
181
+ msg.includes('Unable to start a transaction') ||
182
+ msg.includes('prepared statement') ||
183
+ msg.includes('Connection reset') ||
184
+ msg.includes('ECONNRESET'));
185
+ };
186
+ let lastError;
187
+ for (let attempt = 0; attempt < 4; attempt++) {
188
+ try {
189
+ return await exports.prisma.$transaction(async (tx) => {
190
+ await tx.$executeRawUnsafe(`SELECT set_config('app.current_tenant', $1, true)`, tenantId);
191
+ return work(tx);
192
+ }, {
193
+ maxWait: parseInt(process.env.DATABASE_TX_MAX_WAIT ?? '15000'),
194
+ timeout: parseInt(process.env.DATABASE_TX_TIMEOUT ?? '90000'),
195
+ });
196
+ }
197
+ catch (e) {
198
+ if (attempt < 3 && isRetryable(e)) {
199
+ lastError = e;
200
+ // Exponential backoff: 200ms, 400ms, 800ms
201
+ await new Promise((r) => setTimeout(r, 200 * Math.pow(2, attempt)));
202
+ continue;
203
+ }
204
+ throw e;
205
+ }
206
+ }
207
+ throw lastError;
208
+ }
209
+ __exportStar(require("@prisma/client"), exports);
package/package.json CHANGED
@@ -1,6 +1,31 @@
1
1
  {
2
2
  "name": "@taodigital/database",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.2.0",
4
+ "private": false,
5
+ "publishConfig": {
6
+ "access": "public",
7
+ "registry": "https://registry.npmjs.org/"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "prisma"
12
+ ],
13
+ "main": "dist/index.js",
14
+ "types": "dist/index.d.ts",
15
+ "prisma": {
16
+ "seed": "tsx prisma/seed.ts"
17
+ },
18
+ "scripts": {
19
+ "generate": "prisma generate",
20
+ "migrate": "prisma migrate dev",
21
+ "studio": "prisma studio",
22
+ "build": "prisma generate && tsc",
23
+ "test": "tsx --test src/**/*.test.ts"
24
+ },
25
+ "dependencies": {
26
+ "@prisma/client": "^5.18.0"
27
+ },
28
+ "devDependencies": {
29
+ "prisma": "^5.18.0"
30
+ }
31
+ }
@@ -0,0 +1,220 @@
1
+ -- Initial schema for the Quality Intelligence Platform.
2
+ -- Prisma generates cuid() ids in the client, so ids are TEXT primary keys.
3
+
4
+ CREATE TABLE "Workspace" (
5
+ "id" TEXT NOT NULL,
6
+ "name" TEXT NOT NULL,
7
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
8
+ CONSTRAINT "Workspace_pkey" PRIMARY KEY ("id")
9
+ );
10
+
11
+ CREATE TABLE "Tenant" (
12
+ "id" TEXT NOT NULL,
13
+ "workspaceId" TEXT NOT NULL,
14
+ "name" TEXT NOT NULL,
15
+ "isolation" TEXT NOT NULL DEFAULT 'POOL',
16
+ "region" TEXT NOT NULL DEFAULT 'GLOBAL',
17
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
18
+ CONSTRAINT "Tenant_pkey" PRIMARY KEY ("id")
19
+ );
20
+
21
+ CREATE TABLE "User" (
22
+ "id" TEXT NOT NULL,
23
+ "workspaceId" TEXT NOT NULL,
24
+ "email" TEXT NOT NULL,
25
+ "name" TEXT,
26
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
27
+ CONSTRAINT "User_pkey" PRIMARY KEY ("id")
28
+ );
29
+
30
+ CREATE TABLE "Membership" (
31
+ "id" TEXT NOT NULL,
32
+ "userId" TEXT NOT NULL,
33
+ "tenantId" TEXT NOT NULL,
34
+ "role" TEXT NOT NULL,
35
+ CONSTRAINT "Membership_pkey" PRIMARY KEY ("id")
36
+ );
37
+
38
+ CREATE TABLE "Project" (
39
+ "id" TEXT NOT NULL,
40
+ "tenantId" TEXT NOT NULL,
41
+ "name" TEXT NOT NULL,
42
+ "appUrl" TEXT,
43
+ "figmaUrl" TEXT,
44
+ "oversized" BOOLEAN NOT NULL DEFAULT false,
45
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
46
+ CONSTRAINT "Project_pkey" PRIMARY KEY ("id")
47
+ );
48
+
49
+ CREATE TABLE "Screen" (
50
+ "id" TEXT NOT NULL,
51
+ "tenantId" TEXT NOT NULL,
52
+ "projectId" TEXT NOT NULL,
53
+ "name" TEXT NOT NULL,
54
+ "url" TEXT,
55
+ "version" INTEGER NOT NULL DEFAULT 1,
56
+ "figmaMatch" INTEGER,
57
+ "a11yScore" INTEGER,
58
+ "lastSeenAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
59
+ CONSTRAINT "Screen_pkey" PRIMARY KEY ("id")
60
+ );
61
+
62
+ CREATE TABLE "Element" (
63
+ "id" TEXT NOT NULL,
64
+ "tenantId" TEXT NOT NULL,
65
+ "projectId" TEXT NOT NULL,
66
+ "screenId" TEXT NOT NULL,
67
+ "meaning" TEXT NOT NULL,
68
+ "role" TEXT NOT NULL,
69
+ "expectedData" TEXT,
70
+ "notes" TEXT,
71
+ CONSTRAINT "Element_pkey" PRIMARY KEY ("id")
72
+ );
73
+
74
+ CREATE TABLE "Flow" (
75
+ "id" TEXT NOT NULL,
76
+ "tenantId" TEXT NOT NULL,
77
+ "projectId" TEXT NOT NULL,
78
+ "name" TEXT NOT NULL,
79
+ "steps" JSONB NOT NULL,
80
+ CONSTRAINT "Flow_pkey" PRIMARY KEY ("id")
81
+ );
82
+
83
+ CREATE TABLE "Screenshot" (
84
+ "id" TEXT NOT NULL,
85
+ "tenantId" TEXT NOT NULL,
86
+ "projectId" TEXT NOT NULL,
87
+ "screenId" TEXT NOT NULL,
88
+ "viewport" TEXT NOT NULL DEFAULT 'DESKTOP',
89
+ "storagePath" TEXT NOT NULL,
90
+ "version" INTEGER NOT NULL DEFAULT 1,
91
+ "capturedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
92
+ CONSTRAINT "Screenshot_pkey" PRIMARY KEY ("id")
93
+ );
94
+
95
+ CREATE TABLE "SkillFile" (
96
+ "id" TEXT NOT NULL,
97
+ "tenantId" TEXT NOT NULL,
98
+ "projectId" TEXT NOT NULL,
99
+ "kind" TEXT NOT NULL,
100
+ "content" TEXT NOT NULL,
101
+ "version" INTEGER NOT NULL DEFAULT 1,
102
+ "approvedBy" TEXT,
103
+ "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
104
+ CONSTRAINT "SkillFile_pkey" PRIMARY KEY ("id")
105
+ );
106
+
107
+ CREATE TABLE "TestSuite" (
108
+ "id" TEXT NOT NULL,
109
+ "tenantId" TEXT NOT NULL,
110
+ "projectId" TEXT NOT NULL,
111
+ "name" TEXT NOT NULL,
112
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
113
+ CONSTRAINT "TestSuite_pkey" PRIMARY KEY ("id")
114
+ );
115
+
116
+ CREATE TABLE "TestCase" (
117
+ "id" TEXT NOT NULL,
118
+ "tenantId" TEXT NOT NULL,
119
+ "projectId" TEXT NOT NULL,
120
+ "suiteId" TEXT NOT NULL,
121
+ "title" TEXT NOT NULL,
122
+ "kind" TEXT NOT NULL,
123
+ "steps" JSONB NOT NULL,
124
+ "status" TEXT NOT NULL DEFAULT 'PENDING_REVIEW',
125
+ "groundedOn" JSONB NOT NULL,
126
+ CONSTRAINT "TestCase_pkey" PRIMARY KEY ("id")
127
+ );
128
+
129
+ CREATE TABLE "TestData" (
130
+ "id" TEXT NOT NULL,
131
+ "tenantId" TEXT NOT NULL,
132
+ "projectId" TEXT NOT NULL,
133
+ "testCaseId" TEXT NOT NULL,
134
+ "kind" TEXT NOT NULL,
135
+ "payload" JSONB NOT NULL,
136
+ CONSTRAINT "TestData_pkey" PRIMARY KEY ("id")
137
+ );
138
+
139
+ CREATE TABLE "TestScript" (
140
+ "id" TEXT NOT NULL,
141
+ "tenantId" TEXT NOT NULL,
142
+ "projectId" TEXT NOT NULL,
143
+ "testCaseId" TEXT NOT NULL,
144
+ "language" TEXT NOT NULL DEFAULT 'playwright-ts',
145
+ "code" TEXT NOT NULL,
146
+ CONSTRAINT "TestScript_pkey" PRIMARY KEY ("id")
147
+ );
148
+
149
+ CREATE TABLE "Connector" (
150
+ "id" TEXT NOT NULL,
151
+ "tenantId" TEXT NOT NULL,
152
+ "projectId" TEXT NOT NULL,
153
+ "kind" TEXT NOT NULL,
154
+ "secretRef" TEXT NOT NULL,
155
+ "config" JSONB,
156
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
157
+ CONSTRAINT "Connector_pkey" PRIMARY KEY ("id")
158
+ );
159
+
160
+ CREATE TABLE "AuditLog" (
161
+ "id" TEXT NOT NULL,
162
+ "tenantId" TEXT NOT NULL,
163
+ "projectId" TEXT,
164
+ "actorId" TEXT,
165
+ "action" TEXT NOT NULL,
166
+ "detail" JSONB,
167
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
168
+ CONSTRAINT "AuditLog_pkey" PRIMARY KEY ("id")
169
+ );
170
+
171
+ CREATE TABLE "ChangeLog" (
172
+ "id" TEXT NOT NULL,
173
+ "tenantId" TEXT NOT NULL,
174
+ "projectId" TEXT NOT NULL,
175
+ "changeType" TEXT NOT NULL,
176
+ "summary" TEXT NOT NULL,
177
+ "detail" JSONB,
178
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
179
+ CONSTRAINT "ChangeLog_pkey" PRIMARY KEY ("id")
180
+ );
181
+
182
+ CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
183
+ CREATE UNIQUE INDEX "Membership_userId_tenantId_key" ON "Membership"("userId", "tenantId");
184
+
185
+ CREATE INDEX "Project_tenantId_idx" ON "Project"("tenantId");
186
+ CREATE INDEX "Screen_tenantId_projectId_idx" ON "Screen"("tenantId", "projectId");
187
+ CREATE INDEX "Element_tenantId_projectId_idx" ON "Element"("tenantId", "projectId");
188
+ CREATE INDEX "Element_tenantId_screenId_idx" ON "Element"("tenantId", "screenId");
189
+ CREATE INDEX "Flow_tenantId_projectId_idx" ON "Flow"("tenantId", "projectId");
190
+ CREATE INDEX "Screenshot_tenantId_projectId_idx" ON "Screenshot"("tenantId", "projectId");
191
+ CREATE INDEX "Screenshot_tenantId_screenId_idx" ON "Screenshot"("tenantId", "screenId");
192
+ CREATE INDEX "SkillFile_tenantId_projectId_idx" ON "SkillFile"("tenantId", "projectId");
193
+ CREATE INDEX "TestSuite_tenantId_projectId_idx" ON "TestSuite"("tenantId", "projectId");
194
+ CREATE INDEX "TestCase_tenantId_projectId_idx" ON "TestCase"("tenantId", "projectId");
195
+ CREATE INDEX "TestCase_tenantId_suiteId_idx" ON "TestCase"("tenantId", "suiteId");
196
+ CREATE INDEX "TestData_tenantId_projectId_idx" ON "TestData"("tenantId", "projectId");
197
+ CREATE INDEX "TestData_tenantId_testCaseId_idx" ON "TestData"("tenantId", "testCaseId");
198
+ CREATE INDEX "TestScript_tenantId_projectId_idx" ON "TestScript"("tenantId", "projectId");
199
+ CREATE INDEX "TestScript_tenantId_testCaseId_idx" ON "TestScript"("tenantId", "testCaseId");
200
+ CREATE INDEX "Connector_tenantId_projectId_idx" ON "Connector"("tenantId", "projectId");
201
+ CREATE INDEX "AuditLog_tenantId_createdAt_idx" ON "AuditLog"("tenantId", "createdAt");
202
+ CREATE INDEX "ChangeLog_tenantId_projectId_idx" ON "ChangeLog"("tenantId", "projectId");
203
+
204
+ ALTER TABLE "Tenant" ADD CONSTRAINT "Tenant_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
205
+ ALTER TABLE "User" ADD CONSTRAINT "User_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
206
+ ALTER TABLE "Membership" ADD CONSTRAINT "Membership_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
207
+ ALTER TABLE "Membership" ADD CONSTRAINT "Membership_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
208
+ ALTER TABLE "Project" ADD CONSTRAINT "Project_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
209
+ ALTER TABLE "Screen" ADD CONSTRAINT "Screen_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
210
+ ALTER TABLE "Element" ADD CONSTRAINT "Element_screenId_fkey" FOREIGN KEY ("screenId") REFERENCES "Screen"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
211
+ ALTER TABLE "Flow" ADD CONSTRAINT "Flow_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
212
+ ALTER TABLE "Screenshot" ADD CONSTRAINT "Screenshot_screenId_fkey" FOREIGN KEY ("screenId") REFERENCES "Screen"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
213
+ ALTER TABLE "SkillFile" ADD CONSTRAINT "SkillFile_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
214
+ ALTER TABLE "TestSuite" ADD CONSTRAINT "TestSuite_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
215
+ ALTER TABLE "TestCase" ADD CONSTRAINT "TestCase_suiteId_fkey" FOREIGN KEY ("suiteId") REFERENCES "TestSuite"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
216
+ ALTER TABLE "TestData" ADD CONSTRAINT "TestData_testCaseId_fkey" FOREIGN KEY ("testCaseId") REFERENCES "TestCase"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
217
+ ALTER TABLE "TestScript" ADD CONSTRAINT "TestScript_testCaseId_fkey" FOREIGN KEY ("testCaseId") REFERENCES "TestCase"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
218
+ ALTER TABLE "Connector" ADD CONSTRAINT "Connector_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
219
+ ALTER TABLE "AuditLog" ADD CONSTRAINT "AuditLog_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE SET NULL ON UPDATE CASCADE;
220
+ ALTER TABLE "ChangeLog" ADD CONSTRAINT "ChangeLog_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1,43 @@
1
+ -- ============================================================================
2
+ -- TENANT ISOLATION AT THE DATABASE LAYER (Row-Level Security)
3
+ --
4
+ -- This is the "sacred" rule from the Project Constitution. Even if app code
5
+ -- has a bug, the database itself refuses to return another tenant's rows.
6
+ --
7
+ -- How it works:
8
+ -- 1. Every tenant-scoped table gets RLS enabled.
9
+ -- 2. A policy says: a row is only visible if its "tenantId" matches the
10
+ -- current request's tenant, set via SET app.current_tenant = '<id>'.
11
+ -- 3. The API sets app.current_tenant at the start of every request
12
+ -- (see packages/database/src/index.ts -> withTenant()).
13
+ --
14
+ -- Run this after the Prisma migration that creates the tables.
15
+ -- ============================================================================
16
+
17
+ DO $$
18
+ DECLARE
19
+ t text;
20
+ tenant_tables text[] := ARRAY[
21
+ 'Tenant','Membership','Project','Screen','Element','Flow','Screenshot','SkillFile',
22
+ 'TestSuite','TestCase','TestData','TestScript','Connector',
23
+ 'AuditLog','ChangeLog'
24
+ ];
25
+ BEGIN
26
+ FOREACH t IN ARRAY tenant_tables LOOP
27
+ EXECUTE format('ALTER TABLE "%s" ENABLE ROW LEVEL SECURITY;', t);
28
+ EXECUTE format('ALTER TABLE "%s" FORCE ROW LEVEL SECURITY;', t);
29
+ IF t = 'Tenant' THEN
30
+ EXECUTE format($f$
31
+ CREATE POLICY tenant_isolation ON "%s"
32
+ USING ("id" = current_setting('app.current_tenant', true))
33
+ WITH CHECK ("id" = current_setting('app.current_tenant', true));
34
+ $f$, t);
35
+ ELSE
36
+ EXECUTE format($f$
37
+ CREATE POLICY tenant_isolation ON "%s"
38
+ USING ("tenantId" = current_setting('app.current_tenant', true))
39
+ WITH CHECK ("tenantId" = current_setting('app.current_tenant', true));
40
+ $f$, t);
41
+ END IF;
42
+ END LOOP;
43
+ END $$;
@@ -0,0 +1,14 @@
1
+ -- Runtime role used by the application and tests.
2
+ -- The migration/admin role may be a superuser; the app role must not be,
3
+ -- otherwise Postgres Row-Level Security can be bypassed.
4
+
5
+ DO $$
6
+ BEGIN
7
+ IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'qip_app') THEN
8
+ CREATE ROLE qip_app LOGIN PASSWORD 'qip_app_password' NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT;
9
+ END IF;
10
+ END $$;
11
+
12
+ GRANT USAGE ON SCHEMA public TO qip_app;
13
+ GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO qip_app;
14
+ ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO qip_app;
@@ -0,0 +1,30 @@
1
+ -- Migration: 0003_skill_taxonomy
2
+ -- Extends SkillFile with Application Intelligence taxonomy fields.
3
+ -- New columns support the 6-level knowledge taxonomy (PLATFORM/FUNCTION/DESIGN/THEME/COMPONENT/ELEMENT)
4
+ -- and graduated retrieval (global vs selective vs vector-search).
5
+
6
+ -- Add scope column (PROJECT | WORKSPACE)
7
+ ALTER TABLE "SkillFile" ADD COLUMN IF NOT EXISTS "scope" TEXT NOT NULL DEFAULT 'PROJECT';
8
+
9
+ -- Add summary: short description for UI display and keyword search
10
+ ALTER TABLE "SkillFile" ADD COLUMN IF NOT EXISTS "summary" TEXT;
11
+
12
+ -- Add tags: JSON array of searchable labels e.g. ["login-form", "email-validation"]
13
+ ALTER TABLE "SkillFile" ADD COLUMN IF NOT EXISTS "tags" JSONB NOT NULL DEFAULT '[]';
14
+
15
+ -- Add screenIds: JSON array of screen IDs this COMPONENT/ELEMENT file covers ([] = all)
16
+ ALTER TABLE "SkillFile" ADD COLUMN IF NOT EXISTS "screenIds" JSONB NOT NULL DEFAULT '[]';
17
+
18
+ -- Add source: AUTO | MANUAL | SEEDED
19
+ ALTER TABLE "SkillFile" ADD COLUMN IF NOT EXISTS "source" TEXT NOT NULL DEFAULT 'AUTO';
20
+
21
+ -- Unique constraint: one skill file per (project, kind, scope) — upsert key
22
+ ALTER TABLE "SkillFile" DROP CONSTRAINT IF EXISTS "SkillFile_projectId_kind_scope_key";
23
+ ALTER TABLE "SkillFile" ADD CONSTRAINT "SkillFile_projectId_kind_scope_key"
24
+ UNIQUE ("projectId", "kind", "scope");
25
+
26
+ -- Index for kind-based retrieval (global tier lookup)
27
+ CREATE INDEX IF NOT EXISTS "SkillFile_tenantId_kind_idx" ON "SkillFile" ("tenantId", "kind");
28
+
29
+ -- RLS: SkillFile already covered by the tenant isolation policy on all tables.
30
+ -- No additional policy needed — existing "SkillFile_tenantId" rows remain valid.