@intelligo-dev/core 1.0.0-beta.1 → 1.0.0-beta.14
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/NOTICE +6 -0
- package/README.md +102 -0
- package/dist/attachments/errors.d.ts +7 -0
- package/dist/attachments/errors.d.ts.map +1 -0
- package/dist/attachments/errors.js +11 -0
- package/dist/attachments/errors.js.map +1 -0
- package/dist/attachments/index.d.ts +8 -0
- package/dist/attachments/index.d.ts.map +1 -0
- package/dist/attachments/index.js +7 -0
- package/dist/attachments/index.js.map +1 -0
- package/dist/attachments/service.d.ts +45 -0
- package/dist/attachments/service.d.ts.map +1 -0
- package/dist/attachments/service.js +100 -0
- package/dist/attachments/service.js.map +1 -0
- package/dist/attachments/types.d.ts +6 -0
- package/dist/attachments/types.d.ts.map +1 -0
- package/dist/attachments/types.js +2 -0
- package/dist/attachments/types.js.map +1 -0
- package/dist/conversations/errors.d.ts +2 -8
- package/dist/conversations/errors.d.ts.map +1 -1
- package/dist/conversations/errors.js +2 -8
- package/dist/conversations/errors.js.map +1 -1
- package/dist/conversations/index.d.ts +4 -12
- package/dist/conversations/index.d.ts.map +1 -1
- package/dist/conversations/index.js +2 -11
- package/dist/conversations/index.js.map +1 -1
- package/dist/conversations/service.d.ts +49 -61
- package/dist/conversations/service.d.ts.map +1 -1
- package/dist/conversations/service.js +110 -83
- package/dist/conversations/service.js.map +1 -1
- package/dist/conversations/types.d.ts +1 -4
- package/dist/conversations/types.d.ts.map +1 -1
- package/dist/conversations/types.js +0 -3
- package/dist/conversations/types.js.map +1 -1
- package/dist/db/client.d.ts +21 -10
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +43 -18
- package/dist/db/client.js.map +1 -1
- package/dist/db/index.d.ts +3 -3
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +3 -7
- package/dist/db/index.js.map +1 -1
- package/dist/db/schema/agents.d.ts +14 -51
- package/dist/db/schema/agents.d.ts.map +1 -1
- package/dist/db/schema/agents.js +9 -21
- package/dist/db/schema/agents.js.map +1 -1
- package/dist/db/schema/ai.d.ts +86 -1556
- package/dist/db/schema/ai.d.ts.map +1 -1
- package/dist/db/schema/ai.js +32 -236
- package/dist/db/schema/ai.js.map +1 -1
- package/dist/db/schema/auth.d.ts +5 -34
- package/dist/db/schema/auth.d.ts.map +1 -1
- package/dist/db/schema/auth.js +25 -56
- package/dist/db/schema/auth.js.map +1 -1
- package/dist/db/schema/billing.d.ts +339 -105
- package/dist/db/schema/billing.d.ts.map +1 -1
- package/dist/db/schema/billing.js +99 -58
- package/dist/db/schema/billing.js.map +1 -1
- package/dist/db/schema/feature-flags.d.ts +3 -12
- package/dist/db/schema/feature-flags.d.ts.map +1 -1
- package/dist/db/schema/feature-flags.js +3 -12
- package/dist/db/schema/feature-flags.js.map +1 -1
- package/dist/db/schema/identity.d.ts +12 -274
- package/dist/db/schema/identity.d.ts.map +1 -1
- package/dist/db/schema/identity.js +19 -104
- package/dist/db/schema/identity.js.map +1 -1
- package/dist/db/schema/index.d.ts +8 -15
- package/dist/db/schema/index.d.ts.map +1 -1
- package/dist/db/schema/index.js +12 -42
- package/dist/db/schema/index.js.map +1 -1
- package/dist/db/schema/notifications.d.ts +3 -17
- package/dist/db/schema/notifications.d.ts.map +1 -1
- package/dist/db/schema/notifications.js +3 -17
- package/dist/db/schema/notifications.js.map +1 -1
- package/dist/db/schema/usage.d.ts +94 -77
- package/dist/db/schema/usage.d.ts.map +1 -1
- package/dist/db/schema/usage.js +59 -89
- package/dist/db/schema/usage.js.map +1 -1
- package/dist/db/validate.d.ts +2 -11
- package/dist/db/validate.d.ts.map +1 -1
- package/dist/db/validate.js +6 -17
- package/dist/db/validate.js.map +1 -1
- package/dist/db/workspace-queries.d.ts +5 -29
- package/dist/db/workspace-queries.d.ts.map +1 -1
- package/dist/db/workspace-queries.js +5 -29
- package/dist/db/workspace-queries.js.map +1 -1
- package/dist/documents/classifier.d.ts +5 -12
- package/dist/documents/classifier.d.ts.map +1 -1
- package/dist/documents/classifier.js +16 -16
- package/dist/documents/classifier.js.map +1 -1
- package/dist/documents/errors.d.ts +2 -10
- package/dist/documents/errors.d.ts.map +1 -1
- package/dist/documents/errors.js +2 -10
- package/dist/documents/errors.js.map +1 -1
- package/dist/documents/index.d.ts +4 -13
- package/dist/documents/index.d.ts.map +1 -1
- package/dist/documents/index.js +3 -12
- package/dist/documents/index.js.map +1 -1
- package/dist/documents/service.d.ts +19 -26
- package/dist/documents/service.d.ts.map +1 -1
- package/dist/documents/service.js +44 -38
- package/dist/documents/service.js.map +1 -1
- package/dist/documents/types.d.ts +6 -10
- package/dist/documents/types.d.ts.map +1 -1
- package/dist/documents/types.js +0 -3
- package/dist/documents/types.js.map +1 -1
- package/dist/email/brand.d.ts +17 -0
- package/dist/email/brand.d.ts.map +1 -0
- package/dist/email/brand.js +9 -0
- package/dist/email/brand.js.map +1 -0
- package/dist/email/index.d.ts +4 -4
- package/dist/email/index.js +4 -4
- package/dist/email/provider.d.ts +1 -7
- package/dist/email/provider.d.ts.map +1 -1
- package/dist/email/provider.js +10 -19
- package/dist/email/provider.js.map +1 -1
- package/dist/email/send.d.ts +12 -12
- package/dist/email/send.d.ts.map +1 -1
- package/dist/email/send.js +5 -19
- package/dist/email/send.js.map +1 -1
- package/dist/email/senders.d.ts +17 -30
- package/dist/email/senders.d.ts.map +1 -1
- package/dist/email/senders.js +33 -47
- package/dist/email/senders.js.map +1 -1
- package/dist/email/templates/base-layout.d.ts +1 -1
- package/dist/email/templates/base-layout.d.ts.map +1 -1
- package/dist/email/templates/base-layout.js +4 -6
- package/dist/email/templates/base-layout.js.map +1 -1
- package/dist/email/templates/index.d.ts +10 -10
- package/dist/email/templates/index.d.ts.map +1 -1
- package/dist/email/templates/index.js +10 -16
- package/dist/email/templates/index.js.map +1 -1
- package/dist/email/templates/password-reset.d.ts +2 -2
- package/dist/email/templates/password-reset.d.ts.map +1 -1
- package/dist/email/templates/password-reset.js +10 -22
- package/dist/email/templates/password-reset.js.map +1 -1
- package/dist/email/templates/payment-failed.d.ts +2 -2
- package/dist/email/templates/payment-failed.d.ts.map +1 -1
- package/dist/email/templates/payment-failed.js +11 -24
- package/dist/email/templates/payment-failed.js.map +1 -1
- package/dist/email/templates/quota-warning.d.ts +4 -4
- package/dist/email/templates/quota-warning.d.ts.map +1 -1
- package/dist/email/templates/quota-warning.js +19 -39
- package/dist/email/templates/quota-warning.js.map +1 -1
- package/dist/email/templates/subscription-confirmed.d.ts +2 -2
- package/dist/email/templates/subscription-confirmed.d.ts.map +1 -1
- package/dist/email/templates/subscription-confirmed.js +13 -28
- package/dist/email/templates/subscription-confirmed.js.map +1 -1
- package/dist/email/templates/trial-expiry.d.ts +2 -2
- package/dist/email/templates/trial-expiry.d.ts.map +1 -1
- package/dist/email/templates/trial-expiry.js +12 -36
- package/dist/email/templates/trial-expiry.js.map +1 -1
- package/dist/email/templates/trial-warning.d.ts +2 -2
- package/dist/email/templates/trial-warning.d.ts.map +1 -1
- package/dist/email/templates/trial-warning.js +15 -32
- package/dist/email/templates/trial-warning.js.map +1 -1
- package/dist/email/templates/verify-email.d.ts +2 -2
- package/dist/email/templates/verify-email.d.ts.map +1 -1
- package/dist/email/templates/verify-email.js +10 -22
- package/dist/email/templates/verify-email.js.map +1 -1
- package/dist/email/templates/welcome.d.ts +2 -2
- package/dist/email/templates/welcome.d.ts.map +1 -1
- package/dist/email/templates/welcome.js +13 -20
- package/dist/email/templates/welcome.js.map +1 -1
- package/dist/email/templates/workspace-invitation.d.ts +2 -2
- package/dist/email/templates/workspace-invitation.d.ts.map +1 -1
- package/dist/email/templates/workspace-invitation.js +12 -26
- package/dist/email/templates/workspace-invitation.js.map +1 -1
- package/dist/env.d.ts +5 -10
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +41 -31
- package/dist/env.js.map +1 -1
- package/dist/identity/audit.d.ts +11 -24
- package/dist/identity/audit.d.ts.map +1 -1
- package/dist/identity/audit.js +13 -27
- package/dist/identity/audit.js.map +1 -1
- package/dist/identity/errors.d.ts +4 -12
- package/dist/identity/errors.d.ts.map +1 -1
- package/dist/identity/errors.js +2 -8
- package/dist/identity/errors.js.map +1 -1
- package/dist/identity/index.d.ts +6 -13
- package/dist/identity/index.d.ts.map +1 -1
- package/dist/identity/index.js +5 -12
- package/dist/identity/index.js.map +1 -1
- package/dist/identity/service.d.ts +29 -47
- package/dist/identity/service.d.ts.map +1 -1
- package/dist/identity/service.js +107 -79
- package/dist/identity/service.js.map +1 -1
- package/dist/identity/types.d.ts +28 -4
- package/dist/identity/types.d.ts.map +1 -1
- package/dist/identity/types.js +0 -3
- package/dist/identity/types.js.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -10
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +7 -18
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +45 -49
- package/dist/logger.js.map +1 -1
- package/dist/money.d.ts +95 -0
- package/dist/money.d.ts.map +1 -0
- package/dist/money.js +208 -0
- package/dist/money.js.map +1 -0
- package/dist/notifications/index.d.ts +9 -43
- package/dist/notifications/index.d.ts.map +1 -1
- package/dist/notifications/index.js +9 -49
- package/dist/notifications/index.js.map +1 -1
- package/dist/notifications/triggers.d.ts +9 -32
- package/dist/notifications/triggers.d.ts.map +1 -1
- package/dist/notifications/triggers.js +21 -58
- package/dist/notifications/triggers.js.map +1 -1
- package/dist/notifications/types.d.ts +6 -8
- package/dist/notifications/types.d.ts.map +1 -1
- package/dist/notifications/types.js +0 -9
- package/dist/notifications/types.js.map +1 -1
- package/dist/prompt.d.ts +38 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +118 -0
- package/dist/prompt.js.map +1 -0
- package/dist/registry.d.ts +32 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +60 -0
- package/dist/registry.js.map +1 -0
- package/dist/request-context.d.ts +49 -0
- package/dist/request-context.d.ts.map +1 -0
- package/dist/request-context.js +100 -0
- package/dist/request-context.js.map +1 -0
- package/dist/storage/index.d.ts +65 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +109 -0
- package/dist/storage/index.js.map +1 -0
- package/package.json +74 -24
- package/src/attachments/errors.ts +18 -0
- package/src/attachments/index.ts +22 -0
- package/src/attachments/service.ts +178 -0
- package/src/attachments/types.ts +6 -0
- package/src/conversations/errors.ts +45 -0
- package/src/conversations/index.ts +39 -0
- package/src/conversations/service.ts +594 -0
- package/src/conversations/types.ts +15 -0
- package/src/db/client.ts +91 -0
- package/src/db/index.ts +3 -0
- package/src/db/migrations/0000_baseline.sql +652 -0
- package/src/db/migrations/0001_reconcile_chain_built_databases.sql +53 -0
- package/src/db/migrations/0002_webhook_claim_lease.sql +1 -0
- package/src/db/migrations/0003_local_payments.sql +18 -0
- package/src/db/migrations/README.md +103 -36
- package/src/db/migrations/legacy-chain.json +206 -0
- package/src/db/migrations/meta/0000_snapshot.json +4182 -157
- package/src/db/migrations/meta/0002_snapshot.json +2478 -368
- package/src/db/migrations/meta/0003_snapshot.json +2567 -554
- package/src/db/migrations/meta/_journal.json +8 -274
- package/src/db/schema/agents.ts +57 -0
- package/src/db/schema/ai.ts +174 -0
- package/src/db/schema/auth.ts +183 -0
- package/src/db/schema/billing.ts +245 -0
- package/src/db/schema/feature-flags.ts +20 -0
- package/src/db/schema/identity.ts +250 -0
- package/src/db/schema/index.ts +12 -0
- package/src/db/schema/notifications.ts +31 -0
- package/src/db/schema/usage.ts +269 -0
- package/src/db/validate.ts +63 -0
- package/src/db/workspace-queries.ts +64 -0
- package/src/documents/classifier.ts +61 -0
- package/src/documents/errors.ts +44 -0
- package/src/documents/index.ts +24 -0
- package/src/documents/service.ts +256 -0
- package/src/documents/types.ts +24 -0
- package/src/email/brand.ts +24 -0
- package/src/email/index.ts +19 -0
- package/src/email/provider.ts +278 -0
- package/src/email/send.ts +169 -0
- package/src/email/senders.ts +292 -0
- package/src/email/templates/base-layout.tsx +145 -0
- package/src/email/templates/index.ts +28 -0
- package/src/email/templates/password-reset.tsx +72 -0
- package/src/email/templates/payment-failed.tsx +77 -0
- package/src/email/templates/quota-warning.tsx +118 -0
- package/src/email/templates/subscription-confirmed.tsx +91 -0
- package/src/email/templates/trial-expiry.tsx +124 -0
- package/src/email/templates/trial-warning.tsx +104 -0
- package/src/email/templates/verify-email.tsx +72 -0
- package/src/email/templates/welcome.tsx +64 -0
- package/src/email/templates/workspace-invitation.tsx +105 -0
- package/src/env.ts +180 -0
- package/src/identity/audit.ts +65 -0
- package/src/identity/errors.ts +44 -0
- package/src/identity/index.ts +29 -0
- package/src/identity/service.ts +303 -0
- package/src/identity/types.ts +58 -0
- package/src/index.ts +4 -0
- package/src/logger.ts +198 -0
- package/src/money.ts +270 -0
- package/src/notifications/index.ts +112 -0
- package/src/notifications/triggers.ts +158 -0
- package/src/notifications/types.ts +26 -0
- package/src/prompt.ts +142 -0
- package/src/registry.ts +75 -0
- package/src/request-context.ts +137 -0
- package/src/storage/index.ts +153 -0
- package/dist/db/schema/rag.d.ts +0 -171
- package/dist/db/schema/rag.d.ts.map +0 -1
- package/dist/db/schema/rag.js +0 -42
- package/dist/db/schema/rag.js.map +0 -1
- package/src/db/migrations/0000_sharp_night_nurse.sql +0 -52
- package/src/db/migrations/0001_hesitant_vapor.sql +0 -40
- package/src/db/migrations/0002_numerous_lady_vermin.sql +0 -235
- package/src/db/migrations/0003_mighty_jean_grey.sql +0 -37
- package/src/db/migrations/0004_cynical_grandmaster.sql +0 -2
- package/src/db/migrations/0005_careful_switch.sql +0 -8
- package/src/db/migrations/0006_tense_blue_marvel.sql +0 -16
- package/src/db/migrations/0007_next_squadron_sinister.sql +0 -1
- package/src/db/migrations/0008_powerful_scream.sql +0 -39
- package/src/db/migrations/0009_sad_ink.sql +0 -1
- package/src/db/migrations/0010_shocking_omega_flight.sql +0 -2
- package/src/db/migrations/0011_ambiguous_otto_octavius.sql +0 -151
- package/src/db/migrations/0011_hnsw_vector_index.sql +0 -17
- package/src/db/migrations/0012_user_profiles.sql +0 -30
- package/src/db/migrations/0013_trending_seasonal_columns.sql +0 -9
- package/src/db/migrations/0014_add_product_type_to_agents.sql +0 -6
- package/src/db/migrations/0015_new_v1_tables.sql +0 -149
- package/src/db/migrations/0016_document_types.sql +0 -30
- package/src/db/migrations/0017_document_metadata.sql +0 -2
- package/src/db/migrations/0018_identity_graph.sql +0 -200
- package/src/db/migrations/0019_db_driven_prompts.sql +0 -20
- package/src/db/migrations/0020_pending_extractions.sql +0 -35
- package/src/db/migrations/0021_rag_documents.sql +0 -39
- package/src/db/migrations/0022_user_quotas_usage_jsonb.sql +0 -70
- package/src/db/migrations/0023_user_facts_user_scoped_unique.sql +0 -17
- package/src/db/migrations/0024_user_memories_hnsw.sql +0 -23
- package/src/db/migrations/0025_audit_trigger_errcode.sql +0 -21
- package/src/db/migrations/0026_user_profile_snapshots_composite_pk.sql +0 -17
- package/src/db/migrations/0027_billing_settings_mnt_columns.sql +0 -54
- package/src/db/migrations/0028_trial_credits_normalized_email.sql +0 -37
- package/src/db/migrations/0029_usage_records_conversation_idx.sql +0 -13
- package/src/db/migrations/0030_pending_extractions_composite_idx.sql +0 -11
- package/src/db/migrations/0031_rag_documents_hnsw.sql +0 -16
- package/src/db/migrations/0032_rate_limit_unique_idx.sql +0 -16
- package/src/db/migrations/0033_pending_extractions_partial_idx.sql +0 -18
- package/src/db/migrations/0034_rate_limit_count.sql +0 -9
- package/src/db/migrations/0035_credit_reservations.sql +0 -24
- package/src/db/migrations/0036_executions_audit_jobs.sql +0 -96
- package/src/db/migrations/0037_rate_limit_minute_bucket.sql +0 -46
- package/src/db/migrations/0038_user_quotas_generic_usage.sql +0 -25
- package/src/db/migrations/0039_platform_admin_impersonation.sql +0 -27
- package/src/db/migrations/0040_ee_audit_chain.sql +0 -23
- package/src/db/migrations/meta/0001_snapshot.json +0 -631
- package/src/db/migrations/meta/0004_snapshot.json +0 -2500
- package/src/db/migrations/meta/0005_snapshot.json +0 -2548
- package/src/db/migrations/meta/0006_snapshot.json +0 -2650
- package/src/db/migrations/meta/0007_snapshot.json +0 -2657
- package/src/db/migrations/meta/0008_snapshot.json +0 -2719
- package/src/db/migrations/meta/0009_snapshot.json +0 -2726
- package/src/db/migrations/meta/0010_snapshot.json +0 -2739
- package/src/db/migrations/meta/0011_snapshot.json +0 -3886
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The user identity graph: a user's facts, episodic memories and audit
|
|
3
|
+
* trail, synthesized into one profile snapshot an agent session reads in
|
|
4
|
+
* one query.
|
|
5
|
+
*
|
|
6
|
+
* - `user_facts`: the structured source of truth, queryable by category
|
|
7
|
+
* and weighted by confidence and importance.
|
|
8
|
+
* - `user_memories`: unstructured episodic recall via pgvector search.
|
|
9
|
+
* - `user_profile_snapshots`: the cached synthesis, since voice agents
|
|
10
|
+
* cannot rebuild the profile inside a turn's latency budget.
|
|
11
|
+
* - `user_memory_audit`: append-only log of every write, so a user can see
|
|
12
|
+
* where each fact came from.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
pgTable,
|
|
17
|
+
primaryKey,
|
|
18
|
+
text,
|
|
19
|
+
timestamp,
|
|
20
|
+
integer,
|
|
21
|
+
index,
|
|
22
|
+
uniqueIndex,
|
|
23
|
+
jsonb,
|
|
24
|
+
doublePrecision,
|
|
25
|
+
} from "drizzle-orm/pg-core";
|
|
26
|
+
import { vector } from "drizzle-orm/pg-core/columns/vector_extension/vector";
|
|
27
|
+
import { organization, users } from "./auth";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A fact's category, the filter axis for synthesis and display. A closed
|
|
31
|
+
* list on purpose: a new category is a decision, not a free-form string.
|
|
32
|
+
*/
|
|
33
|
+
export const USER_FACT_CATEGORIES = [
|
|
34
|
+
"interest",
|
|
35
|
+
"skill",
|
|
36
|
+
"goal",
|
|
37
|
+
"constraint",
|
|
38
|
+
"trait",
|
|
39
|
+
"value",
|
|
40
|
+
"preference",
|
|
41
|
+
"context",
|
|
42
|
+
"background",
|
|
43
|
+
] as const;
|
|
44
|
+
|
|
45
|
+
export type UserFactCategory = (typeof USER_FACT_CATEGORIES)[number];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* How a fact entered the system. "explicit_user_input" facts are
|
|
49
|
+
* considered ground truth and survive conflict resolution against
|
|
50
|
+
* lower-confidence LLM extractions.
|
|
51
|
+
*/
|
|
52
|
+
export const USER_FACT_EXTRACTION_METHODS = [
|
|
53
|
+
"llm_extraction",
|
|
54
|
+
"explicit_user_input",
|
|
55
|
+
"inline_tool",
|
|
56
|
+
"onboarding_form",
|
|
57
|
+
] as const;
|
|
58
|
+
|
|
59
|
+
export type UserFactExtractionMethod =
|
|
60
|
+
(typeof USER_FACT_EXTRACTION_METHODS)[number];
|
|
61
|
+
|
|
62
|
+
export const userFacts = pgTable(
|
|
63
|
+
"user_facts",
|
|
64
|
+
{
|
|
65
|
+
id: text("id").primaryKey(),
|
|
66
|
+
userId: text("user_id")
|
|
67
|
+
.notNull()
|
|
68
|
+
.references(() => users.id, { onDelete: "cascade" }),
|
|
69
|
+
workspaceId: text("workspace_id")
|
|
70
|
+
.notNull()
|
|
71
|
+
.references(() => organization.id, { onDelete: "cascade" }),
|
|
72
|
+
|
|
73
|
+
category: text("category").$type<UserFactCategory>().notNull(),
|
|
74
|
+
key: text("key").notNull(),
|
|
75
|
+
value: jsonb("value").notNull(),
|
|
76
|
+
confidence: doublePrecision("confidence").notNull(),
|
|
77
|
+
importance: integer("importance").notNull().default(5),
|
|
78
|
+
|
|
79
|
+
sourceProduct: text("source_product"),
|
|
80
|
+
sourceSessionId: text("source_session_id"),
|
|
81
|
+
sourceMessageId: text("source_message_id"),
|
|
82
|
+
extractionMethod:
|
|
83
|
+
text("extraction_method").$type<UserFactExtractionMethod>(),
|
|
84
|
+
|
|
85
|
+
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
86
|
+
lastConfirmedAt: timestamp("last_confirmed_at"),
|
|
87
|
+
expiresAt: timestamp("expires_at"),
|
|
88
|
+
},
|
|
89
|
+
(table) => [
|
|
90
|
+
index("user_facts_user_idx").on(table.userId),
|
|
91
|
+
index("user_facts_workspace_idx").on(table.workspaceId),
|
|
92
|
+
index("user_facts_category_idx").on(table.workspaceId, table.category),
|
|
93
|
+
// Scoped to (user, workspace, category, key) so members of a shared
|
|
94
|
+
// workspace can hold the same fact key without collision.
|
|
95
|
+
uniqueIndex("user_facts_user_workspace_category_key_uniq").on(
|
|
96
|
+
table.userId,
|
|
97
|
+
table.workspaceId,
|
|
98
|
+
table.category,
|
|
99
|
+
table.key
|
|
100
|
+
),
|
|
101
|
+
]
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
export const USER_MEMORY_KINDS = [
|
|
105
|
+
"conversation_summary",
|
|
106
|
+
"event",
|
|
107
|
+
"insight",
|
|
108
|
+
"preference_statement",
|
|
109
|
+
"decision",
|
|
110
|
+
"struggle",
|
|
111
|
+
"success",
|
|
112
|
+
] as const;
|
|
113
|
+
|
|
114
|
+
export type UserMemoryKind = (typeof USER_MEMORY_KINDS)[number];
|
|
115
|
+
|
|
116
|
+
export const userMemories = pgTable(
|
|
117
|
+
"user_memories",
|
|
118
|
+
{
|
|
119
|
+
id: text("id").primaryKey(),
|
|
120
|
+
userId: text("user_id")
|
|
121
|
+
.notNull()
|
|
122
|
+
.references(() => users.id, { onDelete: "cascade" }),
|
|
123
|
+
workspaceId: text("workspace_id")
|
|
124
|
+
.notNull()
|
|
125
|
+
.references(() => organization.id, { onDelete: "cascade" }),
|
|
126
|
+
|
|
127
|
+
kind: text("kind").$type<UserMemoryKind>().notNull(),
|
|
128
|
+
content: text("content").notNull(),
|
|
129
|
+
embedding: vector({ dimensions: 1536 }),
|
|
130
|
+
|
|
131
|
+
sourceProduct: text("source_product"),
|
|
132
|
+
sourceSessionId: text("source_session_id"),
|
|
133
|
+
sourceMessageIds: text("source_message_ids").array(),
|
|
134
|
+
|
|
135
|
+
importance: integer("importance").notNull().default(5),
|
|
136
|
+
decayFactor: doublePrecision("decay_factor").notNull().default(1.0),
|
|
137
|
+
metadata: jsonb("metadata"),
|
|
138
|
+
|
|
139
|
+
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
140
|
+
},
|
|
141
|
+
(table) => [
|
|
142
|
+
index("user_memories_user_idx").on(table.userId),
|
|
143
|
+
index("user_memories_workspace_idx").on(table.workspaceId),
|
|
144
|
+
index("user_memories_kind_idx").on(table.workspaceId, table.kind),
|
|
145
|
+
// The pgvector index is created in migration SQL: drizzle-orm cannot
|
|
146
|
+
// express it.
|
|
147
|
+
]
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
export const SYNTHESIS_TRIGGER_REASONS = [
|
|
151
|
+
"fact_threshold",
|
|
152
|
+
"session_close",
|
|
153
|
+
"daily_cron",
|
|
154
|
+
"manual",
|
|
155
|
+
] as const;
|
|
156
|
+
|
|
157
|
+
export type SynthesisTriggerReason = (typeof SYNTHESIS_TRIGGER_REASONS)[number];
|
|
158
|
+
|
|
159
|
+
export const userProfileSnapshots = pgTable(
|
|
160
|
+
"user_profile_snapshots",
|
|
161
|
+
{
|
|
162
|
+
userId: text("user_id")
|
|
163
|
+
.notNull()
|
|
164
|
+
.references(() => users.id, { onDelete: "cascade" }),
|
|
165
|
+
workspaceId: text("workspace_id")
|
|
166
|
+
.notNull()
|
|
167
|
+
.references(() => organization.id, { onDelete: "cascade" }),
|
|
168
|
+
version: integer("version").notNull(),
|
|
169
|
+
|
|
170
|
+
summary: text("summary").notNull(),
|
|
171
|
+
summaryEn: text("summary_en"),
|
|
172
|
+
summaryMn: text("summary_mn"),
|
|
173
|
+
|
|
174
|
+
factsDigest: jsonb("facts_digest").notNull(),
|
|
175
|
+
activeGoals: jsonb("active_goals"),
|
|
176
|
+
personalitySignals: jsonb("personality_signals"),
|
|
177
|
+
relationshipNotes: jsonb("relationship_notes"),
|
|
178
|
+
|
|
179
|
+
synthesizedByModel: text("synthesized_by_model"),
|
|
180
|
+
synthesizedAt: timestamp("synthesized_at").notNull().defaultNow(),
|
|
181
|
+
nextSynthesisAt: timestamp("next_synthesis_at"),
|
|
182
|
+
triggerReason: text("trigger_reason").$type<SynthesisTriggerReason>(),
|
|
183
|
+
},
|
|
184
|
+
// One snapshot per user per workspace: a user in two workspaces gets two
|
|
185
|
+
// distinct profiles.
|
|
186
|
+
(table) => [
|
|
187
|
+
primaryKey({ columns: [table.userId, table.workspaceId] }),
|
|
188
|
+
index("user_profile_snapshots_workspace_idx").on(table.workspaceId),
|
|
189
|
+
]
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
export const AUDIT_TARGET_KINDS = ["fact", "memory", "snapshot"] as const;
|
|
193
|
+
export type AuditTargetKind = (typeof AUDIT_TARGET_KINDS)[number];
|
|
194
|
+
|
|
195
|
+
export const AUDIT_ACTIONS = [
|
|
196
|
+
"create",
|
|
197
|
+
"update",
|
|
198
|
+
"delete",
|
|
199
|
+
"view",
|
|
200
|
+
"export",
|
|
201
|
+
] as const;
|
|
202
|
+
export type AuditAction = (typeof AUDIT_ACTIONS)[number];
|
|
203
|
+
|
|
204
|
+
export const AUDIT_ACTOR_KINDS = [
|
|
205
|
+
"agent",
|
|
206
|
+
"user",
|
|
207
|
+
"system_job",
|
|
208
|
+
"admin",
|
|
209
|
+
] as const;
|
|
210
|
+
export type AuditActorKind = (typeof AUDIT_ACTOR_KINDS)[number];
|
|
211
|
+
|
|
212
|
+
export const userMemoryAudit = pgTable(
|
|
213
|
+
"user_memory_audit",
|
|
214
|
+
{
|
|
215
|
+
id: text("id").primaryKey(),
|
|
216
|
+
userId: text("user_id")
|
|
217
|
+
.notNull()
|
|
218
|
+
.references(() => users.id, { onDelete: "cascade" }),
|
|
219
|
+
workspaceId: text("workspace_id")
|
|
220
|
+
.notNull()
|
|
221
|
+
.references(() => organization.id, { onDelete: "cascade" }),
|
|
222
|
+
|
|
223
|
+
targetKind: text("target_kind").$type<AuditTargetKind>().notNull(),
|
|
224
|
+
targetId: text("target_id").notNull(),
|
|
225
|
+
action: text("action").$type<AuditAction>().notNull(),
|
|
226
|
+
actorKind: text("actor_kind").$type<AuditActorKind>().notNull(),
|
|
227
|
+
actorId: text("actor_id"),
|
|
228
|
+
|
|
229
|
+
beforeValue: jsonb("before_value"),
|
|
230
|
+
afterValue: jsonb("after_value"),
|
|
231
|
+
reason: text("reason"),
|
|
232
|
+
|
|
233
|
+
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
234
|
+
},
|
|
235
|
+
(table) => [
|
|
236
|
+
index("user_memory_audit_user_idx").on(table.userId),
|
|
237
|
+
index("user_memory_audit_workspace_idx").on(table.workspaceId),
|
|
238
|
+
index("user_memory_audit_target_idx").on(table.targetKind, table.targetId),
|
|
239
|
+
]
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
export type UserFact = typeof userFacts.$inferSelect;
|
|
243
|
+
export type InsertUserFact = typeof userFacts.$inferInsert;
|
|
244
|
+
export type UserMemory = typeof userMemories.$inferSelect;
|
|
245
|
+
export type InsertUserMemory = typeof userMemories.$inferInsert;
|
|
246
|
+
export type UserProfileSnapshot = typeof userProfileSnapshots.$inferSelect;
|
|
247
|
+
export type InsertUserProfileSnapshot =
|
|
248
|
+
typeof userProfileSnapshots.$inferInsert;
|
|
249
|
+
export type UserMemoryAuditRow = typeof userMemoryAudit.$inferSelect;
|
|
250
|
+
export type InsertUserMemoryAudit = typeof userMemoryAudit.$inferInsert;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Better-Auth's tables, including organization, member and invitation.
|
|
2
|
+
// Product code goes through the Better-Auth API rather than querying the
|
|
3
|
+
// organization tables directly, so membership and role rules stay in one
|
|
4
|
+
// place.
|
|
5
|
+
export * from "./auth";
|
|
6
|
+
export * from "./billing";
|
|
7
|
+
export * from "./usage";
|
|
8
|
+
export * from "./notifications";
|
|
9
|
+
export * from "./feature-flags";
|
|
10
|
+
export * from "./ai";
|
|
11
|
+
export * from "./agents";
|
|
12
|
+
export * from "./identity";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { pgTable, text, timestamp, boolean, index } from "drizzle-orm/pg-core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* User-scoped, not workspace-scoped, so a user sees their notifications from
|
|
5
|
+
* every workspace in one place. `userId` references the users table by
|
|
6
|
+
* convention (no FK); `workspaceId` is null for user-level notifications.
|
|
7
|
+
*/
|
|
8
|
+
export const notifications = pgTable(
|
|
9
|
+
"notifications",
|
|
10
|
+
{
|
|
11
|
+
id: text("id")
|
|
12
|
+
.primaryKey()
|
|
13
|
+
.$defaultFn(() => crypto.randomUUID()),
|
|
14
|
+
userId: text("user_id").notNull(),
|
|
15
|
+
workspaceId: text("workspace_id"),
|
|
16
|
+
type: text("type").notNull(),
|
|
17
|
+
title: text("title").notNull(),
|
|
18
|
+
message: text("message").notNull(),
|
|
19
|
+
isRead: boolean("is_read").notNull().default(false),
|
|
20
|
+
metadata: text("metadata"),
|
|
21
|
+
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
22
|
+
},
|
|
23
|
+
(table) => [
|
|
24
|
+
index("notifications_user_id_idx").on(table.userId),
|
|
25
|
+
index("notifications_user_id_is_read_idx").on(table.userId, table.isRead),
|
|
26
|
+
index("notifications_created_at_idx").on(table.createdAt),
|
|
27
|
+
]
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
export type Notification = typeof notifications.$inferSelect;
|
|
31
|
+
export type InsertNotification = typeof notifications.$inferInsert;
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token consumption per request and aggregated per period, which the quota
|
|
3
|
+
* engine checks before AI requests and records into after them.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
pgTable,
|
|
8
|
+
text,
|
|
9
|
+
timestamp,
|
|
10
|
+
integer,
|
|
11
|
+
bigint,
|
|
12
|
+
real,
|
|
13
|
+
index,
|
|
14
|
+
unique,
|
|
15
|
+
jsonb,
|
|
16
|
+
} from "drizzle-orm/pg-core";
|
|
17
|
+
import { organization, users } from "./auth";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* One row per AI request: tokens, model and agent, for per-model and
|
|
21
|
+
* per-agent reporting.
|
|
22
|
+
*/
|
|
23
|
+
export const usageRecords = pgTable(
|
|
24
|
+
"usage_records",
|
|
25
|
+
{
|
|
26
|
+
id: text("id").primaryKey(),
|
|
27
|
+
workspaceId: text("workspace_id")
|
|
28
|
+
.notNull()
|
|
29
|
+
.references(() => organization.id, { onDelete: "cascade" }),
|
|
30
|
+
userId: text("user_id")
|
|
31
|
+
.notNull()
|
|
32
|
+
.references(() => users.id, { onDelete: "cascade" }),
|
|
33
|
+
type: text("type").notNull(), // "ai_tokens", "api_call"
|
|
34
|
+
model: text("model"), // "gpt-4o", "gpt-4o-mini"
|
|
35
|
+
agent: text("agent"), // the product's agent slug
|
|
36
|
+
inputTokens: integer("input_tokens").notNull().default(0),
|
|
37
|
+
outputTokens: integer("output_tokens").notNull().default(0),
|
|
38
|
+
totalTokens: integer("total_tokens").notNull().default(0),
|
|
39
|
+
/**
|
|
40
|
+
* What the provider charged, in USD micros. Integer on purpose: float
|
|
41
|
+
* arithmetic accumulates sub-cent error over many records.
|
|
42
|
+
*/
|
|
43
|
+
providerCostMicros: bigint("provider_cost_micros", { mode: "number" })
|
|
44
|
+
.notNull()
|
|
45
|
+
.default(0),
|
|
46
|
+
/** Snapshots of the rate and margin this charge was computed with. */
|
|
47
|
+
marginBp: integer("margin_bp").notNull().default(0),
|
|
48
|
+
usdRateMicros: bigint("usd_rate_micros", { mode: "number" })
|
|
49
|
+
.notNull()
|
|
50
|
+
.default(0),
|
|
51
|
+
/** Authoritative charge, in micros of `currency`. */
|
|
52
|
+
chargedMicros: bigint("charged_micros", { mode: "number" })
|
|
53
|
+
.notNull()
|
|
54
|
+
.default(0),
|
|
55
|
+
currency: text("currency").notNull().default("USD"),
|
|
56
|
+
/** Request correlation id for log tracing */
|
|
57
|
+
requestId: text("request_id"),
|
|
58
|
+
/**
|
|
59
|
+
* Owning execution (@intelligo-dev/executions). No FK: the executions
|
|
60
|
+
* table is owned by another package and this column is written by
|
|
61
|
+
* the settlement path, which may run before or after the execution
|
|
62
|
+
* row's terminal update.
|
|
63
|
+
*/
|
|
64
|
+
executionId: text("execution_id"),
|
|
65
|
+
/** Conversation/chat id for grouping in usage dashboards */
|
|
66
|
+
conversationId: text("conversation_id"),
|
|
67
|
+
metadata: text("metadata"), // JSON string for additional data
|
|
68
|
+
recordedAt: timestamp("recorded_at").notNull().defaultNow(),
|
|
69
|
+
},
|
|
70
|
+
(table) => [
|
|
71
|
+
index("usage_records_workspace_id_idx").on(table.workspaceId),
|
|
72
|
+
index("usage_records_workspace_recorded_at_idx").on(
|
|
73
|
+
table.workspaceId,
|
|
74
|
+
table.recordedAt
|
|
75
|
+
),
|
|
76
|
+
index("usage_records_workspace_type_idx").on(table.workspaceId, table.type),
|
|
77
|
+
index("usage_records_conversation_id_idx").on(table.conversationId),
|
|
78
|
+
index("usage_records_execution_id_idx").on(table.executionId),
|
|
79
|
+
]
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* One row per workspace per billing period, incremented atomically, so a
|
|
84
|
+
* quota check is O(1) instead of a SUM over usage_records.
|
|
85
|
+
*/
|
|
86
|
+
export const monthlyUsage = pgTable(
|
|
87
|
+
"monthly_usage",
|
|
88
|
+
{
|
|
89
|
+
id: text("id").primaryKey(),
|
|
90
|
+
workspaceId: text("workspace_id")
|
|
91
|
+
.notNull()
|
|
92
|
+
.references(() => organization.id, { onDelete: "cascade" }),
|
|
93
|
+
periodStart: timestamp("period_start").notNull(),
|
|
94
|
+
periodEnd: timestamp("period_end").notNull(),
|
|
95
|
+
tokensUsed: integer("tokens_used").notNull().default(0),
|
|
96
|
+
/** The plan allowance spent this period, in micros of `currency`. */
|
|
97
|
+
allowanceUsedMicros: bigint("allowance_used_micros", { mode: "number" })
|
|
98
|
+
.notNull()
|
|
99
|
+
.default(0),
|
|
100
|
+
currency: text("currency").notNull().default("USD"),
|
|
101
|
+
requestCount: integer("request_count").notNull().default(0),
|
|
102
|
+
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
|
103
|
+
},
|
|
104
|
+
(table) => [
|
|
105
|
+
unique("monthly_usage_workspace_period_unique").on(
|
|
106
|
+
table.workspaceId,
|
|
107
|
+
table.periodStart
|
|
108
|
+
),
|
|
109
|
+
index("monthly_usage_workspace_id_idx").on(table.workspaceId),
|
|
110
|
+
]
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* One-time trial grant, one per workspace (unique constraint). Separate from
|
|
115
|
+
* purchased credits; used when the plan quota is exceeded or the credit
|
|
116
|
+
* balance is zero. Status: active → depleted, converted or expired.
|
|
117
|
+
*/
|
|
118
|
+
export const trialCredits = pgTable(
|
|
119
|
+
"trial_credits",
|
|
120
|
+
{
|
|
121
|
+
id: text("id").primaryKey(),
|
|
122
|
+
workspaceId: text("workspace_id")
|
|
123
|
+
.notNull()
|
|
124
|
+
.unique()
|
|
125
|
+
.references(() => organization.id, { onDelete: "cascade" }),
|
|
126
|
+
initialCredits: integer("initial_credits").notNull().default(100000),
|
|
127
|
+
creditsUsed: integer("credits_used").notNull().default(0),
|
|
128
|
+
creditsRemaining: integer("credits_remaining").notNull().default(100000),
|
|
129
|
+
/** The grant, in micros of `currency`. */
|
|
130
|
+
initialMicros: bigint("initial_micros", { mode: "number" })
|
|
131
|
+
.notNull()
|
|
132
|
+
.default(0),
|
|
133
|
+
usedMicros: bigint("used_micros", { mode: "number" }).notNull().default(0),
|
|
134
|
+
remainingMicros: bigint("remaining_micros", { mode: "number" })
|
|
135
|
+
.notNull()
|
|
136
|
+
.default(0),
|
|
137
|
+
currency: text("currency").notNull().default("USD"),
|
|
138
|
+
status: text("status").notNull().default("active"), // active|depleted|converted|expired
|
|
139
|
+
provisionedAt: timestamp("provisioned_at").notNull().defaultNow(),
|
|
140
|
+
trialEndDate: timestamp("trial_end_date"), // 14 days from provisioning
|
|
141
|
+
depletedAt: timestamp("depleted_at"),
|
|
142
|
+
convertedAt: timestamp("converted_at"),
|
|
143
|
+
createdByIp: text("created_by_ip"),
|
|
144
|
+
createdByEmail: text("created_by_email"),
|
|
145
|
+
/**
|
|
146
|
+
* Canonical form of createdByEmail for the per-email abuse check,
|
|
147
|
+
* stored so the count can use an index instead of a full-table scan.
|
|
148
|
+
*/
|
|
149
|
+
normalizedEmail: text("normalized_email"),
|
|
150
|
+
},
|
|
151
|
+
(table) => [
|
|
152
|
+
index("trial_credits_expiry_idx").on(table.status, table.trialEndDate),
|
|
153
|
+
index("trial_credits_normalized_email_idx").on(table.normalizedEmail),
|
|
154
|
+
]
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* One row per (workspace, endpoint, minute bucket). Each request upserts the
|
|
159
|
+
* row and atomically increments `count`, so N concurrent requests observe
|
|
160
|
+
* 1..N and exactly `limit` are admitted. Old buckets are deleted by the
|
|
161
|
+
* billing-maintenance job.
|
|
162
|
+
*/
|
|
163
|
+
export const rateLimitEntries = pgTable(
|
|
164
|
+
"rate_limit_entries",
|
|
165
|
+
{
|
|
166
|
+
id: text("id").primaryKey(),
|
|
167
|
+
workspaceId: text("workspace_id")
|
|
168
|
+
.notNull()
|
|
169
|
+
.references(() => organization.id, { onDelete: "cascade" }),
|
|
170
|
+
endpoint: text("endpoint").notNull().default("chat"),
|
|
171
|
+
requestedAt: timestamp("requested_at").notNull().defaultNow(),
|
|
172
|
+
/** Floored minute bucket for unique constraint enforcement */
|
|
173
|
+
minuteBucket: timestamp("minute_bucket").notNull(),
|
|
174
|
+
/** Requests observed in this bucket — incremented via upsert */
|
|
175
|
+
count: integer("count").notNull().default(1),
|
|
176
|
+
},
|
|
177
|
+
(table) => [
|
|
178
|
+
index("rate_limit_entries_window_idx").on(
|
|
179
|
+
table.workspaceId,
|
|
180
|
+
table.endpoint,
|
|
181
|
+
table.requestedAt
|
|
182
|
+
),
|
|
183
|
+
unique("rate_limit_entries_unique_bucket_idx").on(
|
|
184
|
+
table.workspaceId,
|
|
185
|
+
table.endpoint,
|
|
186
|
+
table.minuteBucket
|
|
187
|
+
),
|
|
188
|
+
]
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Sent quota and trial notifications. The unique constraint on
|
|
193
|
+
* (workspaceId, type, periodKey) keeps one notification from being sent
|
|
194
|
+
* twice in a billing period.
|
|
195
|
+
*/
|
|
196
|
+
export const notificationHistory = pgTable(
|
|
197
|
+
"notification_history",
|
|
198
|
+
{
|
|
199
|
+
id: text("id").primaryKey(),
|
|
200
|
+
workspaceId: text("workspace_id")
|
|
201
|
+
.notNull()
|
|
202
|
+
.references(() => organization.id, { onDelete: "cascade" }),
|
|
203
|
+
type: text("type").notNull(), // "quota_warning_80", "quota_warning_100", "trial_warning_20", "trial_depleted"
|
|
204
|
+
periodKey: text("period_key").notNull(), // "2026-02" for monthly, "trial" for one-time
|
|
205
|
+
sentAt: timestamp("sent_at").notNull().defaultNow(),
|
|
206
|
+
channel: text("channel").notNull().default("console"), // "console" | "email"
|
|
207
|
+
metadata: text("metadata"), // JSON string for additional context
|
|
208
|
+
},
|
|
209
|
+
(table) => [
|
|
210
|
+
unique("notification_history_dedup_unique").on(
|
|
211
|
+
table.workspaceId,
|
|
212
|
+
table.type,
|
|
213
|
+
table.periodKey
|
|
214
|
+
),
|
|
215
|
+
index("notification_history_workspace_id_idx").on(table.workspaceId),
|
|
216
|
+
]
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Per-user, per-action usage counters, one row per user and workspace (not
|
|
221
|
+
* per period). Counters live in the `usage` JSONB map and increment
|
|
222
|
+
* atomically in SQL, so a quota check is O(1).
|
|
223
|
+
*/
|
|
224
|
+
export const userQuotas = pgTable(
|
|
225
|
+
"user_quotas",
|
|
226
|
+
{
|
|
227
|
+
id: text("id").primaryKey(),
|
|
228
|
+
userId: text("user_id")
|
|
229
|
+
.notNull()
|
|
230
|
+
.references(() => users.id, { onDelete: "cascade" }),
|
|
231
|
+
workspaceId: text("workspace_id")
|
|
232
|
+
.notNull()
|
|
233
|
+
.references(() => organization.id, { onDelete: "cascade" }),
|
|
234
|
+
plan: text("plan").notNull().default("free"), // free | standard | pro
|
|
235
|
+
/**
|
|
236
|
+
* Per-action counters keyed by the product's own action slugs
|
|
237
|
+
* (`usage["chat"] = 42`), so a product adds a counter without a
|
|
238
|
+
* migration.
|
|
239
|
+
*/
|
|
240
|
+
usage: jsonb("usage").notNull().default({}),
|
|
241
|
+
totalCostUsd: real("total_cost_usd").notNull().default(0),
|
|
242
|
+
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
243
|
+
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
|
244
|
+
},
|
|
245
|
+
(table) => [
|
|
246
|
+
index("user_quotas_user_id_idx").on(table.userId),
|
|
247
|
+
index("user_quotas_workspace_id_idx").on(table.workspaceId),
|
|
248
|
+
// Unique per (user, workspace), not per user: a user in a paid and a
|
|
249
|
+
// free workspace must not share one plan and one set of counters.
|
|
250
|
+
unique("user_quotas_user_workspace_unique").on(
|
|
251
|
+
table.userId,
|
|
252
|
+
table.workspaceId
|
|
253
|
+
),
|
|
254
|
+
]
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
export type UserQuota = typeof userQuotas.$inferSelect;
|
|
258
|
+
export type InsertUserQuota = typeof userQuotas.$inferInsert;
|
|
259
|
+
export type UsageRecord = typeof usageRecords.$inferSelect;
|
|
260
|
+
export type InsertUsageRecord = typeof usageRecords.$inferInsert;
|
|
261
|
+
export type MonthlyUsage = typeof monthlyUsage.$inferSelect;
|
|
262
|
+
export type InsertMonthlyUsage = typeof monthlyUsage.$inferInsert;
|
|
263
|
+
export type TrialCredit = typeof trialCredits.$inferSelect;
|
|
264
|
+
export type InsertTrialCredit = typeof trialCredits.$inferInsert;
|
|
265
|
+
export type RateLimitEntry = typeof rateLimitEntries.$inferSelect;
|
|
266
|
+
export type InsertRateLimitEntry = typeof rateLimitEntries.$inferInsert;
|
|
267
|
+
export type NotificationHistoryRecord = typeof notificationHistory.$inferSelect;
|
|
268
|
+
export type InsertNotificationHistoryRecord =
|
|
269
|
+
typeof notificationHistory.$inferInsert;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { db } from "./client";
|
|
2
|
+
import { sql } from "drizzle-orm";
|
|
3
|
+
|
|
4
|
+
export interface DatabaseValidationResult {
|
|
5
|
+
isConnected: boolean;
|
|
6
|
+
tablesExist: boolean;
|
|
7
|
+
error?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Checks that the database answers and the four auth tables exist. */
|
|
11
|
+
export async function validateDatabaseConnection(): Promise<DatabaseValidationResult> {
|
|
12
|
+
try {
|
|
13
|
+
await db.execute(sql`SELECT 1`);
|
|
14
|
+
|
|
15
|
+
const result = await db.execute(sql`
|
|
16
|
+
SELECT COUNT(*) as table_count
|
|
17
|
+
FROM information_schema.tables
|
|
18
|
+
WHERE table_schema = 'public'
|
|
19
|
+
AND table_name IN ('users', 'sessions', 'accounts', 'verifications')
|
|
20
|
+
`);
|
|
21
|
+
|
|
22
|
+
const tableCount = Number(result.rows[0]?.table_count ?? 0);
|
|
23
|
+
const tablesExist = tableCount === 4;
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
isConnected: true,
|
|
27
|
+
tablesExist,
|
|
28
|
+
error: tablesExist
|
|
29
|
+
? undefined
|
|
30
|
+
: "Auth tables do not exist. Run 'intelligo migrate' to apply the framework's migration chain.",
|
|
31
|
+
};
|
|
32
|
+
} catch (error) {
|
|
33
|
+
return {
|
|
34
|
+
isConnected: false,
|
|
35
|
+
tablesExist: false,
|
|
36
|
+
error: error instanceof Error ? error.message : "Unknown database error",
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Startup check: throws with setup instructions when validation fails. */
|
|
42
|
+
export async function requireDatabaseConnection(): Promise<void> {
|
|
43
|
+
const result = await validateDatabaseConnection();
|
|
44
|
+
|
|
45
|
+
if (!result.isConnected) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`❌ Database connection failed: ${result.error}\n\n` +
|
|
48
|
+
`Make sure DATABASE_URL is configured in .env.local\n` +
|
|
49
|
+
`Example: DATABASE_URL=postgres://user:pass@host/db`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!result.tablesExist) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`❌ Database tables do not exist.\n\n` +
|
|
56
|
+
`Run the following commands to set up the database:\n` +
|
|
57
|
+
` 1. intelligo migrate (applies the framework's migration chain)\n` +
|
|
58
|
+
` 2. drizzle-kit migrate (your own tables, if any)`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
console.log("✓ Database connection validated");
|
|
63
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace scoping for tables with a `workspaceId` column. Every query on
|
|
3
|
+
* such a table filters by the workspace resolved from `requireWorkspace()`.
|
|
4
|
+
* Organization, member and invitation rows go through the Better-Auth API,
|
|
5
|
+
* which scopes them to the caller's session.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { eq, and, type SQL } from "drizzle-orm";
|
|
9
|
+
import type { PgColumn } from "drizzle-orm/pg-core";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Creates a workspace-scoped WHERE condition for any table with a workspaceId column.
|
|
13
|
+
* Use it in every custom Drizzle query to enforce workspace isolation.
|
|
14
|
+
*
|
|
15
|
+
* @param workspaceIdColumn - The workspaceId column from the table schema
|
|
16
|
+
* @param workspaceId - The active workspace ID from requireWorkspace()
|
|
17
|
+
* @returns SQL condition: workspaceId = ?
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* const { workspace } = await requireWorkspace();
|
|
22
|
+
* const conversations = await db
|
|
23
|
+
* .select()
|
|
24
|
+
* .from(conversationsTable)
|
|
25
|
+
* .where(workspaceEq(conversationsTable.workspaceId, workspace.id));
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export function workspaceEq(
|
|
29
|
+
workspaceIdColumn: PgColumn,
|
|
30
|
+
workspaceId: string
|
|
31
|
+
): SQL {
|
|
32
|
+
return eq(workspaceIdColumn, workspaceId);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Combines a workspace scope condition with additional WHERE conditions.
|
|
37
|
+
*
|
|
38
|
+
* @param workspaceIdColumn - The workspaceId column from the table schema
|
|
39
|
+
* @param workspaceId - The active workspace ID from requireWorkspace()
|
|
40
|
+
* @param additionalConditions - Extra WHERE conditions to AND together
|
|
41
|
+
* @returns Combined SQL condition: workspaceId = ? AND ...
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```typescript
|
|
45
|
+
* const { workspace } = await requireWorkspace();
|
|
46
|
+
* const activeConversations = await db
|
|
47
|
+
* .select()
|
|
48
|
+
* .from(conversationsTable)
|
|
49
|
+
* .where(
|
|
50
|
+
* withWorkspaceFilter(
|
|
51
|
+
* conversationsTable.workspaceId,
|
|
52
|
+
* workspace.id,
|
|
53
|
+
* eq(conversationsTable.status, "active")
|
|
54
|
+
* )
|
|
55
|
+
* );
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export function withWorkspaceFilter(
|
|
59
|
+
workspaceIdColumn: PgColumn,
|
|
60
|
+
workspaceId: string,
|
|
61
|
+
...additionalConditions: SQL[]
|
|
62
|
+
): SQL {
|
|
63
|
+
return and(eq(workspaceIdColumn, workspaceId), ...additionalConditions)!;
|
|
64
|
+
}
|