@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.
- package/README.md +21 -2
- package/dist/index.d.ts +58 -0
- package/dist/index.js +209 -0
- package/package.json +29 -4
- package/prisma/migrations/0000_init/migration.sql +220 -0
- package/prisma/migrations/0001_tenant_isolation/migration.sql +43 -0
- package/prisma/migrations/0002_app_runtime_role/migration.sql +14 -0
- package/prisma/migrations/0003_skill_taxonomy/migration.sql +30 -0
- package/prisma/migrations/0004_extended_models/migration.sql +215 -0
- package/prisma/migrations/0005_enhanced_test_artifacts/migration.sql +24 -0
- package/prisma/migrations/0006_tenant_settings/migration.sql +28 -0
- package/prisma/migrations/0007_project_slug/migration.sql +11 -0
- package/prisma/migrations/0008_project_crawl_config/migration.sql +3 -0
- package/prisma/migrations/0009_tenant_slug/migration.sql +11 -0
- package/prisma/migrations/0010_user_password/migration.sql +3 -0
- package/prisma/migrations/0011_oauth_connections/migration.sql +78 -0
- package/prisma/migrations/0012_agent_webhooks/migration.sql +46 -0
- package/prisma/migrations/0013_project_artifacts/migration.sql +27 -0
- package/prisma/migrations/0014_knowledge_chunks/migration.sql +37 -0
- package/prisma/migrations/20260626022056_platform_job_migration/migration.sql +44 -0
- package/prisma/migrations/20260626053339_kag_schema_update/migration.sql +27 -0
- package/prisma/migrations/20260626053340_restore_bm25_index/migration.sql +13 -0
- package/prisma/migrations/20260626063704_token_optimisation/migration.sql +86 -0
- package/prisma/migrations/20260626080000_job_name/migration.sql +2 -0
- package/prisma/migrations/20260626090000_rls_missing_tables/migration.sql +39 -0
- package/prisma/migrations/20260626100000_token_budget/migration.sql +21 -0
- package/prisma/migrations/20260626110000_screen_groups/migration.sql +42 -0
- package/prisma/migrations/20260626120000_artifact_intelligence/migration.sql +5 -0
- package/prisma/migrations/20260626130000_notifications/migration.sql +34 -0
- package/prisma/migrations/20260627100000_element_viewport/migration.sql +2 -0
- package/prisma/migrations/20260627100000_screenshot_cloud_url/migration.sql +2 -0
- package/prisma/migrations/20260627110000_screenshot_index/migration.sql +2 -0
- package/prisma/migrations/20260627140000_artifact_storage_key/migration.sql +3 -0
- package/prisma/migrations/20260627150000_screen_lifecycle/migration.sql +5 -0
- package/prisma/migrations/20260627160000_native_app_support/migration.sql +41 -0
- package/prisma/migrations/20260627200000_rls_missing_tables/migration.sql +57 -0
- package/prisma/migrations/20260627210000_llmcalllog_tasktype/migration.sql +1 -0
- package/prisma/migrations/20260628100000_element_enrichment/migration.sql +7 -0
- package/prisma/migrations/20260628110000_element_hash/migration.sql +6 -0
- package/prisma/migrations/20260628120000_screen_diff/migration.sql +20 -0
- package/prisma/migrations/20260628130000_probe_observations/migration.sql +19 -0
- package/prisma/migrations/20260629100000_visual_baseline_masking/migration.sql +1 -0
- package/prisma/migrations/20260629110000_testcase_viewports/migration.sql +1 -0
- package/prisma/migrations/20260630100000_visual_baseline_element_snapshot/migration.sql +1 -0
- package/prisma/migrations/20260630110000_git_branched_baselines/migration.sql +11 -0
- package/prisma/migrations/20260630120000_testcase_viewport_constraint/migration.sql +1 -0
- package/prisma/migrations/20260630130000_test_case_runs/migration.sql +22 -0
- package/prisma/migrations/20260630140000_perf_env_comments/migration.sql +68 -0
- package/prisma/migrations/20260630150000_testcase_gen_at/migration.sql +2 -0
- package/prisma/migrations/20260630160000_rls_catchup_late_tables/migration.sql +41 -0
- package/prisma/migrations/20260701100000_skill_file_diff_approval/migration.sql +9 -0
- package/prisma/migrations/20260701200000_workspace_connector/migration.sql +20 -0
- package/prisma/migrations/20260701220000_screen_a11y_security/migration.sql +2 -0
- package/prisma/migrations/20260702100000_ci_threshold_root_cause/migration.sql +2 -0
- package/prisma/migrations/20260702120000_medium_priority_gaps/migration.sql +28 -0
- package/prisma/migrations/20260702140000_testcase_v2_technique/migration.sql +7 -0
- package/prisma/migrations/20260702150000_add_app_field/migration.sql +40 -0
- package/prisma/migrations/20260702160000_add_requirement_model/migration.sql +62 -0
- package/prisma/migrations/20260702180000_add_invitation_model/migration.sql +53 -0
- package/prisma/migrations/20260702190000_suite_hierarchy_artifact_trace/migration.sql +28 -0
- package/prisma/migrations/20260702200000_project_member_access/migration.sql +31 -0
- package/prisma/migrations/20260703000000_add_system_config/migration.sql +11 -0
- package/prisma/migrations/20260703000000_custom_roles/migration.sql +29 -0
- package/prisma/migrations/20260703100000_platform_admin/migration.sql +11 -0
- package/prisma/migrations/20260703110000_add_extension_models/migration.sql +106 -0
- package/prisma/migrations/20260703120000_project_restricted_column/migration.sql +1 -0
- package/prisma/migrations/20260703200000_system_config_category_index/migration.sql +1 -0
- package/prisma/migrations/20260703210000_workspace_layer/migration.sql +24 -0
- package/prisma/migrations/20260703230000_workspace_agent_tokens/migration.sql +2 -0
- package/prisma/migrations/20260704000000_requirement_rls_policy/migration.sql +35 -0
- package/prisma/migrations/20260704100000_fix_oauth_auditlog_cascade/migration.sql +9 -0
- package/prisma/migrations/20260704110000_skillfile_approval_status/migration.sql +2 -0
- package/prisma/migrations/20260704120000_scenario_suspect_workspace/migration.sql +35 -0
- package/prisma/migrations/20260704130000_testcase_traceto_suspect_indexes/migration.sql +5 -0
- package/prisma/migrations/20260704140000_workspace_rls/migration.sql +19 -0
- package/prisma/migrations/20260704150000_project_cascade_deletes/migration.sql +125 -0
- package/prisma/migrations/20260705100000_platform_job_worker_id/migration.sql +12 -0
- package/prisma/migrations/20260705110000_screen_discovery_method/migration.sql +7 -0
- package/prisma/migrations/20260705120000_webhook_delivery/migration.sql +32 -0
- package/prisma/migrations/20260705130000_tenant_feature_flags/migration.sql +23 -0
- package/prisma/migrations/20260705140000_auth_sessions/migration.sql +30 -0
- package/prisma/migrations/20260705150000_fix_rls_wrong_session_var/migration.sql +51 -0
- package/prisma/migrations/20260705160000_testcase_flaky_quarantine/migration.sql +14 -0
- package/prisma/migrations/20260705170000_testcaserun_browser/migration.sql +6 -0
- package/prisma/migrations/20260705180000_allow_custom_roles/migration.sql +22 -0
- package/prisma/migrations/20260706050000_screen_markdown_extraction/migration.sql +8 -0
- package/prisma/migrations/20260706090000_add_lighthouse_to_performance_snapshot/migration.sql +4 -0
- package/prisma/migrations/20260706100000_add_lighthouse_bp_and_job_index/migration.sql +5 -0
- package/prisma/migrations/20260706110000_add_visual_diff_ai_summary/migration.sql +2 -0
- package/prisma/migrations/20260707120000_script_bundles/migration.sql +25 -0
- package/prisma/migrations/20260707130000_requirement_source_section/migration.sql +4 -0
- package/prisma/migrations/20260707140000_structured_skill_metadata/migration.sql +8 -0
- package/prisma/migrations/20260707150000_crawler_platform_primitives/migration.sql +112 -0
- package/prisma/migrations/20260707160000_screen_group_versioning/migration.sql +9 -0
- package/prisma/migrations/20260707170000_add_mfa_fields/migration.sql +24 -0
- package/prisma/migrations/20260707180000_add_missing_tables/migration.sql +158 -0
- package/prisma/migrations/20260707190000_add_missing_columns/migration.sql +15 -0
- package/prisma/migrations/20260707200000_add_plan_sessions/migration.sql +1 -0
- package/prisma/migrations/20260708100000_missing_columns_comprehensive/migration.sql +15 -0
- package/prisma/migrations/20260708200000_testcase_dataset_column/migration.sql +16 -0
- package/prisma/migrations/20260709100000_add_filename_to_native_app_build/migration.sql +1 -0
- package/prisma/migrations/20260709200000_cloud_url_visual_artifacts/migration.sql +6 -0
- package/prisma/migrations/20260714200000_add_cached_scenarios_to_artifact/migration.sql +4 -0
- package/prisma/migrations/20260715200000_screen_markdown_content/migration.sql +2 -0
- package/prisma/migrations/20260716100000_github_repo_coverage_tables/migration.sql +114 -0
- package/prisma/migrations/20260716200000_github_pr_script_validation/migration.sql +48 -0
- package/prisma/migrations/20260716210000_s3_content_keys/migration.sql +20 -0
- package/prisma/migrations/20260717220000_ide_push_token/migration.sql +21 -0
- package/prisma/migrations/20260717230000_ide_push_script_ids/migration.sql +2 -0
- package/prisma/migrations/20260730040000_add_step_evidence_and_validation/migration.sql +93 -0
- package/prisma/migrations/20260803000000_add_tenantId_to_test_data_row/migration.sql +40 -0
- package/prisma/migrations/20260804000000_add_project_restricted/migration.sql +6 -0
- package/prisma/migrations/20260804000000_add_user_avatar_url/migration.sql +4 -0
- package/prisma/migrations/20260805000000_add_auditlog_ipaddress/migration.sql +2 -0
- package/prisma/migrations/20260807000000_add_test_data_classification/migration.sql +18 -0
- package/prisma/migrations/20260807000001_add_test_case_notes/migration.sql +5 -0
- package/prisma/migrations/20260807000002_add_visual_baseline_browser_structured_steps/migration.sql +11 -0
- package/prisma/migrations/20260807100000_add_tenant_plan_config/migration.sql +14 -0
- package/prisma/migrations/20260808000000_agent_token_missing_columns/migration.sql +22 -0
- package/prisma/migrations/20260808000001_visual_diff_image_and_threshold/migration.sql +8 -0
- package/prisma/migrations/20260808000002_ssoconfig_email_domains/migration.sql +7 -0
- package/prisma/migrations/20260808000003_project_status_description_updatedat/migration.sql +7 -0
- package/prisma/migrations/20260814000000_workspace_require_mfa/migration.sql +2 -0
- package/prisma/migrations/20260814000001_add_screenshot_dpr/migration.sql +2 -0
- package/prisma/migrations/20260818000000_add_content_hash_to_test_case/migration.sql +6 -0
- package/prisma/migrations/20260818000001_add_grounding_hash_to_test_case/migration.sql +3 -0
- package/prisma/migrations/20260819000000_coverage_mapping_cross_encoder_fields/migration.sql +3 -0
- package/prisma/migrations/20260820000000_add_locators_json_to_test_case/migration.sql +6 -0
- package/prisma/migrations/20260820000001_add_test_case_version/migration.sql +27 -0
- package/prisma/migrations/20260826000000_add_capture_step_screenshots/migration.sql +1 -0
- package/prisma/migrations/20260903000000_add_platform_job_dlq/migration.sql +10 -0
- package/prisma/migrations/20260903000002_add_versioning_tables/migration.sql +38 -0
- package/prisma/migrations/20260903000003_add_learning_models/migration.sql +71 -0
- package/prisma/migrations/20260903000004_add_workspace_agent_prompts/migration.sql +33 -0
- package/prisma/migrations/20260903000005_add_config_history/migration.sql +70 -0
- package/prisma/migrations/20260903000006_add_skill_file_config/migration.sql +24 -0
- package/prisma/migrations/20260904000000_add_agent_prompt_platform_table/migration.sql +23 -0
- package/prisma/migrations/20260904180424_rbac_role_configs_admin_roles/migration.sql +606 -0
- package/prisma/migrations/20260907000000_fix_testcase_timestamps/migration.sql +13 -0
- package/prisma/migrations/20260907000001_add_artifact_imported_suite_id/migration.sql +1 -0
- package/prisma/migrations/20260907000002_add_user_login_tracking/migration.sql +9 -0
- package/prisma/migrations/20260907000003_add_artifact_import_tracking/migration.sql +9 -0
- package/prisma/migrations/20260907000004_add_artifact_version_chain/migration.sql +18 -0
- package/prisma/migrations/20260908000000_add_project_vsa_enabled/migration.sql +2 -0
- package/prisma/migrations/20260908000001_rbac_migration_catchup/migration.sql +314 -0
- package/prisma/migrations/20260908000002_fix_role_configs_empty_permissions/migration.sql +22 -0
- package/prisma/migrations/20260908000003_sync_builtin_role_permissions/migration.sql +38 -0
- package/prisma/migrations/20260909000000_add_filepath_to_testscript/migration.sql +5 -0
- package/prisma/migrations/20260909000001_add_alignment_result_table/migration.sql +31 -0
- package/prisma/migrations/20260910000000_add_admin_mfa/migration.sql +35 -0
- package/prisma/migrations/20260910000000_add_dom_snapshot_cloud_url/migration.sql +2 -0
- package/prisma/migrations/20260910000001_add_requires_auth_to_screen/migration.sql +1 -0
- package/prisma/migrations/20260910000002_add_screen_requires_auth/migration.sql +2 -0
- package/prisma/migrations/20260910000003_add_admin_auth_sessions/migration.sql +17 -0
- package/prisma/migrations/20260916000001_add_admin_last_login/migration.sql +1 -0
- package/prisma/migrations/20260916000002_add_admin_failed_login/migration.sql +2 -0
- package/prisma/migrations/20260916000003_seed_system_tenant/migration.sql +6 -0
- package/prisma/migrations/20260918000000_fix_permissions_comprehensive/migration.sql +63 -0
- package/prisma/migrations/20260924000000_add_screen_role_access/migration.sql +2 -0
- package/prisma/migrations/20260928000000_add_navigation_graph/migration.sql +41 -0
- package/prisma/migrations/20260929000000_artifact_content_hash_suite_unique/migration.sql +25 -0
- package/prisma/migrations/20260929000000_navigation_graph_unique_constraint/migration.sql +5 -0
- package/prisma/migrations/20260929000001_add_membership_status/migration.sql +7 -0
- package/prisma/migrations/20260929000002_add_sso_config_scim_token_hash/migration.sql +8 -0
- package/prisma/migrations/20261005000001_jobs_workers_hardening/migration.sql +63 -0
- package/prisma/migrations/20261009000001_add_suite_category/migration.sql +2 -0
- package/prisma/migrations/20261009000002_add_testcase_approved_fields/migration.sql +3 -0
- package/prisma/migrations/20261012_auto_version_history/migration.sql +111 -0
- package/prisma/migrations/20261015000001_traceability_alignment_gaps/migration.sql +11 -0
- package/prisma/migrations/20261028000001_context_intelligence_p1_p7/migration.sql +76 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +2901 -0
- package/prisma/seed.ts +1262 -0
package/README.md
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @detiq/database — The Data Model & Zeta Core
|
|
2
2
|
|
|
3
|
-
This
|
|
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
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -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": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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.
|