@tangen/db 0.0.0-stage → 0.1.55
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 +73 -2
- package/drizzle/0000_flimsy_shotgun.sql +339 -0
- package/drizzle/0001_app_data.sql +11 -0
- package/drizzle/0002_add_indexes.sql +36 -0
- package/drizzle/0003_extension_data_rename.sql +3 -0
- package/drizzle/0004_extension_data_published_globals.sql +12 -0
- package/drizzle/0005_auth.sql +51 -0
- package/drizzle/0006_auth_identities.sql +13 -0
- package/drizzle/0007_ai.sql +18 -0
- package/drizzle/0008_ai_chat_threads.sql +5 -0
- package/drizzle/0009_forms_source_slug.sql +11 -0
- package/drizzle/0010_rbac_roles_status.sql +14 -0
- package/drizzle/0011_jobs_v2.sql +67 -0
- package/drizzle/0012_search_platform.sql +36 -0
- package/drizzle/0013_search_synonym_label.sql +2 -0
- package/drizzle/0014_object_types_disabled.sql +2 -0
- package/drizzle/0015_playground_collections.sql +25 -0
- package/drizzle/0016_catchup_activity_promotion.sql +36 -0
- package/drizzle/0017_fields_disabled.sql +3 -0
- package/drizzle/0018_object_types_plural.sql +1 -0
- package/drizzle/0019_member_profile.sql +2 -0
- package/drizzle/0020_paused_jobs.sql +7 -0
- package/drizzle/0021_form_submission_notes.sql +2 -0
- package/drizzle/0022_paused_jobs_env_reason.sql +4 -0
- package/drizzle/0023_scale_indexes.sql +3 -0
- package/drizzle/0024_migration_runs.sql +24 -0
- package/drizzle/0025_porter_transfer.sql +30 -0
- package/drizzle/0026_cron_timezone.sql +1 -0
- package/drizzle/0027_rate_limit_counters.sql +6 -0
- package/drizzle/0028_realtime_events.sql +10 -0
- package/drizzle/0030_delivery_artifacts.sql +10 -0
- package/drizzle/0031_realtime_presence.sql +8 -0
- package/drizzle/0032_schema_pushes.sql +15 -0
- package/drizzle/0033_studio_design_capability.sql +37 -0
- package/drizzle/0034_object_types_provided_by.sql +12 -0
- package/drizzle/0035_custom_table_owners.sql +24 -0
- package/drizzle/0036_revision_version_unique.sql +42 -0
- package/drizzle/0037_job_queue_dedupe.sql +20 -0
- package/drizzle/0038_search_documents.sql +87 -0
- package/drizzle/0039_job_prune_indexes.sql +25 -0
- package/drizzle/0040_extension_data_object_idx.sql +10 -0
- package/drizzle/0041_knowledge.sql +79 -0
- package/drizzle/0042_schema_pushes_checksum.sql +7 -0
- package/drizzle/0043_guard_adoption.sql +12 -0
- package/drizzle/0044_retention_indexes.sql +7 -0
- package/drizzle/0045_extension_install_per_env.sql +40 -0
- package/drizzle/0046_extension_migrations.sql +18 -0
- package/drizzle/0047_environment_role.sql +25 -0
- package/drizzle/0048_environment_role_adoption.sql +30 -0
- package/drizzle/0049_release_lifecycle.sql +55 -0
- package/drizzle/0050_member_env_membership.sql +21 -0
- package/drizzle/0051_remove_environments.sql +350 -0
- package/drizzle/0052_release_actor_columns.sql +9 -0
- package/drizzle/0053_kernel_event_record.sql +26 -0
- package/drizzle/0054_kernel_run_identity.sql +13 -0
- package/drizzle/0055_kernel_identities.sql +19 -0
- package/drizzle/0056_forms_leave_core.sql +7 -0
- package/drizzle/0057_auth_leave_core.sql +8 -0
- package/drizzle/0058_member_grants.sql +11 -0
- package/drizzle/0059_extension_granted_grants.sql +7 -0
- package/drizzle/0060_connections.sql +21 -0
- package/drizzle/meta/0000_snapshot.json +1692 -0
- package/drizzle/meta/0002_snapshot.json +1935 -0
- package/drizzle/meta/0003_snapshot.json +1935 -0
- package/drizzle/meta/_journal.json +426 -0
- package/package.json +85 -4
- package/src/cache-keys.ts +43 -0
- package/src/client.ts +136 -0
- package/src/content-ddl.ts +70 -0
- package/src/content-tables.ts +110 -0
- package/src/custom-tables-loader.ts +65 -0
- package/src/ensure-system-schema.ts +195 -0
- package/src/extension-install-state.ts +220 -0
- package/src/extension-migrations.ts +270 -0
- package/src/generated/content-tables.ts +271 -0
- package/src/generated/custom-tables.ts +18 -0
- package/src/index.ts +24 -0
- package/src/instrument.ts +139 -0
- package/src/knowledge-schema.ts +232 -0
- package/src/migrate.ts +67 -0
- package/src/prepare-policy.ts +118 -0
- package/src/reconcile.ts +115 -0
- package/src/schema/ai-chats.ts +36 -0
- package/src/schema/ai-settings.ts +24 -0
- package/src/schema/api-keys.ts +37 -0
- package/src/schema/audit-log.ts +54 -0
- package/src/schema/connections.ts +35 -0
- package/src/schema/custom-table-owners.ts +32 -0
- package/src/schema/delivery-artifacts.ts +33 -0
- package/src/schema/entity-tags.ts +33 -0
- package/src/schema/extension-data.ts +38 -0
- package/src/schema/extension-migrations.ts +42 -0
- package/src/schema/fields.ts +37 -0
- package/src/schema/globals.ts +35 -0
- package/src/schema/identities.ts +27 -0
- package/src/schema/index.ts +51 -0
- package/src/schema/jobs.ts +104 -0
- package/src/schema/knowledge.ts +85 -0
- package/src/schema/media.ts +52 -0
- package/src/schema/member-grants.ts +34 -0
- package/src/schema/members.ts +49 -0
- package/src/schema/migration-runs.ts +46 -0
- package/src/schema/object-types.ts +41 -0
- package/src/schema/objects.ts +38 -0
- package/src/schema/paused-jobs.ts +25 -0
- package/src/schema/playground.ts +43 -0
- package/src/schema/porter-transfer.ts +47 -0
- package/src/schema/realtime.ts +45 -0
- package/src/schema/releases.ts +95 -0
- package/src/schema/roles.ts +27 -0
- package/src/schema/saved-views.ts +33 -0
- package/src/schema/schema-pushes.ts +43 -0
- package/src/schema/search-documents.ts +47 -0
- package/src/schema/search-query-log.ts +24 -0
- package/src/schema/search-recommendations.ts +27 -0
- package/src/schema/search-synonyms.ts +23 -0
- package/src/schema/webhook-deliveries.ts +37 -0
- package/src/schema/webhooks.ts +36 -0
- package/src/schema/workspace-activity.ts +35 -0
- package/src/schema/workspace-domains.ts +24 -0
- package/src/schema/workspace-installed-apps.ts +48 -0
- package/src/schema/workspaces.ts +24 -0
- package/src/schema-kit.ts +64 -0
- package/src/sealed.ts +84 -0
- package/src/seed/skeleton.ts +77 -0
- package/src/sql/apply-gin-indexes.ts +31 -0
- package/src/sql/gin-indexes.sql +11 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Name: knowledge.ts
|
|
3
|
+
* Purpose: The Knowledge (RAG) tables, declared where every other system
|
|
4
|
+
* table is declared. `knowledge_meta` pins the vector space of one
|
|
5
|
+
* workspace x environment (provider, model, dimensions);
|
|
6
|
+
* `knowledge_chunks` holds the embedded chunks; `knowledge_qa` the
|
|
7
|
+
* curated question/answer pairs; `knowledge_queries` the retrieval
|
|
8
|
+
* log the Insights screen reads.
|
|
9
|
+
*
|
|
10
|
+
* Author: Raghunath J
|
|
11
|
+
* License: MIT
|
|
12
|
+
*
|
|
13
|
+
* Dependencies/Notes: drizzle-orm/pg-core. Migration 0041 creates these; the
|
|
14
|
+
* canonical DDL text lives in ../knowledge-schema.ts and the test
|
|
15
|
+
* proves the migration carries it verbatim. `embedding` is
|
|
16
|
+
* pgvector's `vector` type; its dimensions are NOT part of the
|
|
17
|
+
* declaration because they come from the configured embedding model
|
|
18
|
+
* - `pinKnowledgeVectorSpace` types the column when the space is
|
|
19
|
+
* pinned. `@tangen/knowledge` itself declares and creates nothing.
|
|
20
|
+
*/
|
|
21
|
+
import { pgTable, text, integer, real, boolean, bigserial, jsonb, timestamp, primaryKey, index, customType } from 'drizzle-orm/pg-core'
|
|
22
|
+
|
|
23
|
+
/** pgvector's `vector` column type; dimensions are applied by the vector-space pin, not the declaration. */
|
|
24
|
+
export const vector = customType<{ data: number[]; driverData: string }>({
|
|
25
|
+
dataType() { return 'vector' },
|
|
26
|
+
toDriver(value: number[]) { return `[${value.join(',')}]` },
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
export const knowledgeMeta = pgTable('knowledge_meta', {
|
|
30
|
+
workspaceId: text('workspace_id').notNull(),
|
|
31
|
+
provider: text('provider').notNull(),
|
|
32
|
+
model: text('model').notNull(),
|
|
33
|
+
dimensions: integer('dimensions').notNull(),
|
|
34
|
+
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
35
|
+
}, (t) => ({
|
|
36
|
+
pk: primaryKey({ columns: [t.workspaceId] }),
|
|
37
|
+
}))
|
|
38
|
+
|
|
39
|
+
export const knowledgeChunks = pgTable('knowledge_chunks', {
|
|
40
|
+
id: bigserial('id', { mode: 'number' }).primaryKey(),
|
|
41
|
+
workspaceId: text('workspace_id').notNull(),
|
|
42
|
+
sourceKind: text('source_kind').notNull(),
|
|
43
|
+
sourceId: text('source_id').notNull(),
|
|
44
|
+
sourceGroup: text('source_group').notNull().default(''),
|
|
45
|
+
field: text('field').notNull().default(''),
|
|
46
|
+
locale: text('locale').notNull().default(''),
|
|
47
|
+
audience: text('audience').notNull().default('public'),
|
|
48
|
+
chunkIndex: integer('chunk_index').notNull().default(0),
|
|
49
|
+
content: text('content').notNull(),
|
|
50
|
+
contentHash: text('content_hash').notNull(),
|
|
51
|
+
metadata: jsonb('metadata'),
|
|
52
|
+
embedding: vector('embedding').notNull(),
|
|
53
|
+
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
54
|
+
}, (t) => ({
|
|
55
|
+
scopeIdx: index('knowledge_chunks_scope_idx').on(t.workspaceId, t.sourceKind, t.sourceId),
|
|
56
|
+
}))
|
|
57
|
+
|
|
58
|
+
export const knowledgeQa = pgTable('knowledge_qa', {
|
|
59
|
+
id: text('id').primaryKey(),
|
|
60
|
+
workspaceId: text('workspace_id').notNull(),
|
|
61
|
+
question: text('question').notNull(),
|
|
62
|
+
answer: text('answer').notNull(),
|
|
63
|
+
audience: text('audience').notNull().default('public'),
|
|
64
|
+
enabled: boolean('enabled').notNull().default(true),
|
|
65
|
+
updatedBy: text('updated_by'),
|
|
66
|
+
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
67
|
+
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
68
|
+
}, (t) => ({
|
|
69
|
+
scopeIdx: index('knowledge_qa_scope_idx').on(t.workspaceId),
|
|
70
|
+
}))
|
|
71
|
+
|
|
72
|
+
export const knowledgeQueries = pgTable('knowledge_queries', {
|
|
73
|
+
id: bigserial('id', { mode: 'number' }).primaryKey(),
|
|
74
|
+
workspaceId: text('workspace_id').notNull(),
|
|
75
|
+
query: text('query').notNull(),
|
|
76
|
+
source: text('source').notNull(),
|
|
77
|
+
audience: text('audience').notNull().default('public'),
|
|
78
|
+
locale: text('locale').notNull().default(''),
|
|
79
|
+
topScore: real('top_score'),
|
|
80
|
+
hitCount: integer('hit_count').notNull().default(0),
|
|
81
|
+
answered: boolean('answered').notNull().default(false),
|
|
82
|
+
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
83
|
+
}, (t) => ({
|
|
84
|
+
scopeIdx: index('knowledge_queries_scope_idx').on(t.workspaceId, t.createdAt),
|
|
85
|
+
}))
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Name: media.ts
|
|
3
|
+
* Purpose: Drizzle schema for the `media_folders` and `media_assets` tables, which
|
|
4
|
+
* together model a hierarchical media library for uploaded files and images.
|
|
5
|
+
*
|
|
6
|
+
* Author: Raghunath J
|
|
7
|
+
* License: MIT
|
|
8
|
+
*
|
|
9
|
+
* Dependencies/Notes: mediaFolders supports nesting via self-referential parentId.
|
|
10
|
+
* mediaAssets references mediaFolders for folder placement.
|
|
11
|
+
* storagePath is the provider-specific path (S3 key, local path, etc.).
|
|
12
|
+
*/
|
|
13
|
+
import { pgTable, text, timestamp, integer, bigint, jsonb, index } from 'drizzle-orm/pg-core'
|
|
14
|
+
import { sql } from 'drizzle-orm'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Stores hierarchical folder nodes for organising media assets; parentId is nullable
|
|
18
|
+
* to represent root-level folders.
|
|
19
|
+
*/
|
|
20
|
+
export const mediaFolders = pgTable('media_folders', {
|
|
21
|
+
id: text('id').primaryKey(),
|
|
22
|
+
workspaceId: text('workspace_id').notNull(),
|
|
23
|
+
name: text('name').notNull(),
|
|
24
|
+
parentId: text('parent_id'),
|
|
25
|
+
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Stores uploaded media file records including MIME type, dimensions, alt text, tags,
|
|
30
|
+
* storage path, and an optional thumbnail path for image previews.
|
|
31
|
+
*/
|
|
32
|
+
export const mediaAssets = pgTable('media_assets', {
|
|
33
|
+
id: text('id').primaryKey(),
|
|
34
|
+
workspaceId: text('workspace_id').notNull(),
|
|
35
|
+
folderId: text('folder_id').references(() => mediaFolders.id),
|
|
36
|
+
filename: text('filename').notNull(),
|
|
37
|
+
originalName: text('original_name').notNull(),
|
|
38
|
+
mimeType: text('mime_type').notNull(),
|
|
39
|
+
sizeBytes: bigint('size_bytes', { mode: 'number' }).notNull(),
|
|
40
|
+
width: integer('width'),
|
|
41
|
+
height: integer('height'),
|
|
42
|
+
altText: text('alt_text'),
|
|
43
|
+
tags: text('tags').array().notNull().default(sql`'{}'`),
|
|
44
|
+
metadata: jsonb('metadata').notNull().default(sql`'{}'`),
|
|
45
|
+
storagePath: text('storage_path').notNull(),
|
|
46
|
+
thumbnailPath: text('thumbnail_path'),
|
|
47
|
+
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
48
|
+
createdBy: text('created_by'),
|
|
49
|
+
}, (t) => ({
|
|
50
|
+
workspaceIdx: index('media_assets_workspace_idx').on(t.workspaceId),
|
|
51
|
+
folderIdx: index('media_assets_folder_idx').on(t.folderId),
|
|
52
|
+
}))
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Name: member-grants.ts
|
|
3
|
+
* Purpose: Drizzle schema for `member_grants` - per-member ADDITIVE permission
|
|
4
|
+
* grants layered on top of a member's role (extra admin capabilities
|
|
5
|
+
* and extra extension-page access). One row per member; a row in this
|
|
6
|
+
* database is a grant in THIS deployment (environments are removed, so
|
|
7
|
+
* the database boundary is the environment boundary).
|
|
8
|
+
*
|
|
9
|
+
* Author: Raghunath J
|
|
10
|
+
* License: MIT
|
|
11
|
+
*
|
|
12
|
+
* Dependencies/Notes: Grants only ever ADD - they never remove a role right
|
|
13
|
+
* (see tasks/plan-per-user-grants.md). `capabilities` and `extensions`
|
|
14
|
+
* are string arrays (Capability[] / ExtensionId[]). `expires_at` is an
|
|
15
|
+
* optional time-box; an expired grant is ignored at read time.
|
|
16
|
+
*/
|
|
17
|
+
import { pgTable, text, timestamp, jsonb, primaryKey } from 'drizzle-orm/pg-core'
|
|
18
|
+
|
|
19
|
+
export const memberGrants = pgTable('member_grants', {
|
|
20
|
+
workspaceId: text('workspace_id').notNull(),
|
|
21
|
+
memberId: text('member_id').notNull(),
|
|
22
|
+
/** Extra admin-area capabilities (Capability[]). */
|
|
23
|
+
capabilities: jsonb('capabilities').notNull().default([]),
|
|
24
|
+
/** Extra extension-page ids this member may open (ExtensionId[]). */
|
|
25
|
+
extensions: jsonb('extensions').notNull().default([]),
|
|
26
|
+
/** Member id of whoever granted it, for provenance. */
|
|
27
|
+
grantedBy: text('granted_by'),
|
|
28
|
+
reason: text('reason'),
|
|
29
|
+
/** Optional expiry; a past value means the grant no longer applies. */
|
|
30
|
+
expiresAt: timestamp('expires_at', { withTimezone: true }),
|
|
31
|
+
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
32
|
+
}, (t) => ({
|
|
33
|
+
pk: primaryKey({ columns: [t.workspaceId, t.memberId] }),
|
|
34
|
+
}))
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Name: members.ts
|
|
3
|
+
* Purpose: Drizzle schema for the `members` and `refresh_tokens` tables, which store
|
|
4
|
+
* workspace admin accounts and their session refresh tokens respectively.
|
|
5
|
+
*
|
|
6
|
+
* Author: Raghunath J
|
|
7
|
+
* License: MIT
|
|
8
|
+
*
|
|
9
|
+
* Dependencies/Notes: Members are the human operators of the CMS admin UI, distinct
|
|
10
|
+
* from end-users stored in auth_users. passwordHash uses argon2id
|
|
11
|
+
* (see seed/skeleton.ts). refresh_tokens supports revocation via
|
|
12
|
+
* revokedAt; expired/revoked tokens should be pruned periodically.
|
|
13
|
+
*/
|
|
14
|
+
import { pgTable, text, timestamp, unique, index } from 'drizzle-orm/pg-core'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Stores workspace admin member accounts with email/password credentials and role;
|
|
18
|
+
* email must be unique within a workspace.
|
|
19
|
+
*/
|
|
20
|
+
export const members = pgTable('members', {
|
|
21
|
+
id: text('id').primaryKey(),
|
|
22
|
+
workspaceId: text('workspace_id').notNull(),
|
|
23
|
+
email: text('email').notNull(),
|
|
24
|
+
name: text('name'),
|
|
25
|
+
/** Job title shown on the profile / member list (self-editable). */
|
|
26
|
+
designation: text('designation'),
|
|
27
|
+
/** Avatar image URL (self-editable). Members are workspace-scoped, so a plain
|
|
28
|
+
* URL avoids the env-scoped media coupling. */
|
|
29
|
+
avatar: text('avatar'),
|
|
30
|
+
passwordHash: text('password_hash').notNull(),
|
|
31
|
+
role: text('role').notNull().default('editor'),
|
|
32
|
+
/** 'active' | 'suspended' — suspended members cannot log in or refresh. */
|
|
33
|
+
status: text('status').notNull().default('active'),
|
|
34
|
+
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
35
|
+
lastLoginAt: timestamp('last_login_at', { withTimezone: true }),
|
|
36
|
+
}, (t) => ({
|
|
37
|
+
uniqWorkspaceEmail: unique().on(t.workspaceId, t.email),
|
|
38
|
+
}))
|
|
39
|
+
|
|
40
|
+
export const refreshTokens = pgTable('refresh_tokens', {
|
|
41
|
+
id: text('id').primaryKey(),
|
|
42
|
+
memberId: text('member_id').notNull().references(() => members.id),
|
|
43
|
+
tokenHash: text('token_hash').notNull(),
|
|
44
|
+
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
|
|
45
|
+
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
46
|
+
revokedAt: timestamp('revoked_at', { withTimezone: true }),
|
|
47
|
+
}, (t) => ({
|
|
48
|
+
memberIdx: index('refresh_tokens_member_idx').on(t.memberId),
|
|
49
|
+
}))
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Name: migration-runs.ts
|
|
3
|
+
* Purpose: Durable state for Porter data-movement runs (promote, export,
|
|
4
|
+
* import, remote transfer). The row IS the worker's state: claimable,
|
|
5
|
+
* heartbeat while alive, per-outcome counters, cancellable, and a pointer
|
|
6
|
+
* to the parts-based JSONL results ledger in storage - so a crash or
|
|
7
|
+
* redeploy loses nothing and "resume" is just reading the row back.
|
|
8
|
+
*
|
|
9
|
+
* Author: Raghunath J
|
|
10
|
+
* License: MIT
|
|
11
|
+
*
|
|
12
|
+
* Dependencies/Notes: drizzle-orm/pg-core. Results live OUTSIDE the row
|
|
13
|
+
* (storage adapter, one JSONL file per flush part) - a 100k-item report
|
|
14
|
+
* does not belong in a jsonb column.
|
|
15
|
+
*/
|
|
16
|
+
import { pgTable, text, timestamp, integer, boolean, jsonb, index } from 'drizzle-orm/pg-core'
|
|
17
|
+
import { sql } from 'drizzle-orm'
|
|
18
|
+
|
|
19
|
+
export const migrationRuns = pgTable('migration_runs', {
|
|
20
|
+
id: text('id').primaryKey(),
|
|
21
|
+
workspaceId: text('workspace_id').notNull(),
|
|
22
|
+
/** promote | export | import | remote_send | remote_receive */
|
|
23
|
+
kind: text('kind').notNull(),
|
|
24
|
+
/** Envs are nullable per kind: export has no target, import no source. */
|
|
25
|
+
/** pending | running | completed | failed | cancelled */
|
|
26
|
+
status: text('status').notNull().default('pending'),
|
|
27
|
+
/** What the run covers: type/objectIds/labels/bundle info - kind-specific. */
|
|
28
|
+
scope: jsonb('scope').notNull().default(sql`'{}'`),
|
|
29
|
+
/** Per-outcome counters: { created, overwritten, unchanged, replaced, failed, skipped }. */
|
|
30
|
+
counts: jsonb('counts').notNull().default(sql`'{}'`),
|
|
31
|
+
totalItems: integer('total_items').notNull().default(0),
|
|
32
|
+
processedItems: integer('processed_items').notNull().default(0),
|
|
33
|
+
/** Number of JSONL result parts written to storage (porter/runs/<id>/results-N.jsonl). */
|
|
34
|
+
resultsParts: integer('results_parts').notNull().default(0),
|
|
35
|
+
error: text('error'),
|
|
36
|
+
/** Durable cancel flag - workers poll it between chunks. */
|
|
37
|
+
cancelRequested: boolean('cancel_requested').notNull().default(false),
|
|
38
|
+
createdBy: text('created_by'),
|
|
39
|
+
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
40
|
+
startedAt: timestamp('started_at', { withTimezone: true }),
|
|
41
|
+
finishedAt: timestamp('finished_at', { withTimezone: true }),
|
|
42
|
+
heartbeatAt: timestamp('heartbeat_at', { withTimezone: true }),
|
|
43
|
+
}, (t) => ({
|
|
44
|
+
workspaceIdx: index('migration_runs_workspace_idx').on(t.workspaceId, t.createdAt),
|
|
45
|
+
statusIdx: index('migration_runs_status_idx').on(t.status),
|
|
46
|
+
}))
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Name: object-types.ts
|
|
3
|
+
* Purpose: Defines the `object_types` table, which stores content-type definitions
|
|
4
|
+
* (schema blueprints) registered in each environment of a workspace.
|
|
5
|
+
*
|
|
6
|
+
* Author: Raghunath J
|
|
7
|
+
* License: MIT
|
|
8
|
+
*
|
|
9
|
+
* Dependencies/Notes: drizzle-orm/pg-core; one registry per database: a type name is unique in the deployment.
|
|
10
|
+
*/
|
|
11
|
+
import { pgTable, text, timestamp, boolean, integer, jsonb, unique } from 'drizzle-orm/pg-core'
|
|
12
|
+
import { sql } from 'drizzle-orm'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Stores the schema definition for a single content type within an environment.
|
|
16
|
+
* Each row describes one content type (e.g. "post", "author") including its field
|
|
17
|
+
* schema version, singleton flag, and admin UI configuration.
|
|
18
|
+
*/
|
|
19
|
+
export const objectTypes = pgTable('object_types', {
|
|
20
|
+
id: text('id').primaryKey(),
|
|
21
|
+
name: text('name').notNull(),
|
|
22
|
+
label: text('label').notNull(),
|
|
23
|
+
/** Explicit plural word (nullable — inflection derives one from `name` when absent). */
|
|
24
|
+
plural: text('plural'),
|
|
25
|
+
singleton: boolean('singleton').notNull().default(false),
|
|
26
|
+
disabled: boolean('disabled').notNull().default(false),
|
|
27
|
+
/** Type-level localization opt-in — title (and any localized fields) get a
|
|
28
|
+
* <type>_i18n side table; drives whether the admin offers translation. */
|
|
29
|
+
localized: boolean('localized').notNull().default(false),
|
|
30
|
+
/** The extension that declares this type, or NULL when the site's own code
|
|
31
|
+
* does. A row with a value here is never dropped by a push: an extension
|
|
32
|
+
* that stops declaring it leaves an orphan the tooling reports instead. */
|
|
33
|
+
providedBy: text('provided_by'),
|
|
34
|
+
previousNames: text('previous_names').array().notNull().default(sql`'{}'`),
|
|
35
|
+
adminConfig: jsonb('admin_config').notNull().default(sql`'{}'`),
|
|
36
|
+
schemaVersion: integer('schema_version').notNull().default(1),
|
|
37
|
+
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
38
|
+
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
39
|
+
}, (t) => ({
|
|
40
|
+
uniqEnvName: unique().on(t.name),
|
|
41
|
+
}))
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Name: objects.ts
|
|
3
|
+
* Purpose: Defines the `_revisions` table, which stores immutable point-in-time
|
|
4
|
+
* snapshots of content objects used for version history and auditing.
|
|
5
|
+
*
|
|
6
|
+
* Author: Raghunath J
|
|
7
|
+
* License: MIT
|
|
8
|
+
*
|
|
9
|
+
* Dependencies/Notes: drizzle-orm/pg-core; authoritative live content lives in
|
|
10
|
+
* per-type tables provisioned by `tangen push` — this table only holds snapshots.
|
|
11
|
+
*/
|
|
12
|
+
import { pgTable, text, timestamp, jsonb, integer, index, uniqueIndex } from 'drizzle-orm/pg-core'
|
|
13
|
+
|
|
14
|
+
// Immutable point-in-time snapshots used for version history.
|
|
15
|
+
// Authoritative live content lives in per-type tables created by `tangen push`.
|
|
16
|
+
/**
|
|
17
|
+
* Stores a versioned, immutable JSONB snapshot of a content object at a specific
|
|
18
|
+
* point in time, enabling full version history and rollback for any object type.
|
|
19
|
+
*/
|
|
20
|
+
export const revisions = pgTable('_revisions', {
|
|
21
|
+
id: text('id').primaryKey(),
|
|
22
|
+
workspaceId: text('workspace_id').notNull(),
|
|
23
|
+
typeId: text('type_id').notNull(),
|
|
24
|
+
typeName: text('type_name').notNull(),
|
|
25
|
+
objectId: text('object_id').notNull(),
|
|
26
|
+
version: integer('version').notNull().default(1),
|
|
27
|
+
versionType: text('version_type'),
|
|
28
|
+
snapshot: jsonb('snapshot').notNull(),
|
|
29
|
+
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
30
|
+
createdBy: text('created_by'),
|
|
31
|
+
/** The locale this revision captured (K5); null for the base row. */
|
|
32
|
+
locale: text('locale'),
|
|
33
|
+
}, (t) => ({
|
|
34
|
+
idxObject: index('revisions_object').on(t.workspaceId, t.objectId, t.createdAt),
|
|
35
|
+
// The invariant the allocator relies on: one version number per object, ever.
|
|
36
|
+
// Scope matches the allocation scope (workspace + object, not env).
|
|
37
|
+
uniqObjectVersion: uniqueIndex('_revisions_object_version_uniq').on(t.workspaceId, t.objectId, t.version),
|
|
38
|
+
}))
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Name: paused-jobs.ts
|
|
3
|
+
* Purpose: Admin pause/resume for CODE-defined scheduled jobs — a row here means
|
|
4
|
+
* the job (workspace-level, all envs) is paused; the scheduler skips it. Presence
|
|
5
|
+
* = paused, absence = running. Lets operators stop a job from the admin without a
|
|
6
|
+
* code deploy.
|
|
7
|
+
*
|
|
8
|
+
* Author: Raghunath J
|
|
9
|
+
* License: MIT
|
|
10
|
+
*/
|
|
11
|
+
import { pgTable, text, timestamp, primaryKey } from 'drizzle-orm/pg-core'
|
|
12
|
+
|
|
13
|
+
export const pausedJobs = pgTable('paused_jobs', {
|
|
14
|
+
workspaceId: text('workspace_id').notNull(),
|
|
15
|
+
/** The defineScheduledJob id (from app/jobs). */
|
|
16
|
+
jobId: text('job_id').notNull(),
|
|
17
|
+
/** Env slug the pause applies to; '*' = all envs (legacy rows migrate here). */
|
|
18
|
+
pausedAt: timestamp('paused_at', { withTimezone: true }).notNull().defaultNow(),
|
|
19
|
+
/** Member who paused it (audit; nullable - null for the circuit breaker). */
|
|
20
|
+
pausedBy: text('paused_by'),
|
|
21
|
+
/** Why (circuit breaker writes this; manual pauses leave it null). */
|
|
22
|
+
reason: text('reason'),
|
|
23
|
+
}, (t) => ({
|
|
24
|
+
pk: primaryKey({ columns: [t.workspaceId, t.jobId] }),
|
|
25
|
+
}))
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Name: playground.ts
|
|
3
|
+
* Purpose: PG-3 — API Playground persistence: collections (folders of saved
|
|
4
|
+
* REST/GraphQL requests, private by default, shareable within the
|
|
5
|
+
* playground's own RBAC circle) and per-member variable sets
|
|
6
|
+
* ({{name}} interpolation; values never leave this table).
|
|
7
|
+
*
|
|
8
|
+
* Author: Raghunath J
|
|
9
|
+
* License: MIT
|
|
10
|
+
*/
|
|
11
|
+
import { pgTable, text, timestamp, jsonb, index, unique } from 'drizzle-orm/pg-core'
|
|
12
|
+
import { sql } from 'drizzle-orm'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A collection: an ordered jsonb list of saved requests. Requests never store
|
|
16
|
+
* key ids or variable values — keys are picked and variables resolved at run
|
|
17
|
+
* time. `visibility`: 'private' (creator only) | 'shared' (every member who
|
|
18
|
+
* clears the playground RBAC gate).
|
|
19
|
+
*/
|
|
20
|
+
export const playgroundCollections = pgTable('playground_collections', {
|
|
21
|
+
id: text('id').primaryKey(),
|
|
22
|
+
workspaceId: text('workspace_id').notNull(),
|
|
23
|
+
name: text('name').notNull(),
|
|
24
|
+
/** [{ id, name, request: { kind, method?, path?, query?, headers?, body?, graphql? } }] */
|
|
25
|
+
requests: jsonb('requests').notNull().default(sql`'[]'`),
|
|
26
|
+
visibility: text('visibility').notNull().default('private'),
|
|
27
|
+
createdBy: text('created_by').notNull(),
|
|
28
|
+
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
29
|
+
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
30
|
+
}, (t) => ({
|
|
31
|
+
playgroundCollectionsScopeIdx: index('playground_collections_scope_idx').on(t.workspaceId),
|
|
32
|
+
}))
|
|
33
|
+
|
|
34
|
+
/** One variable set per member per workspace+env: { name: { value, secret? } }. */
|
|
35
|
+
export const playgroundVariables = pgTable('playground_variables', {
|
|
36
|
+
id: text('id').primaryKey(),
|
|
37
|
+
workspaceId: text('workspace_id').notNull(),
|
|
38
|
+
memberId: text('member_id').notNull(),
|
|
39
|
+
vars: jsonb('vars').notNull().default(sql`'{}'`),
|
|
40
|
+
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
41
|
+
}, (t) => ({
|
|
42
|
+
playgroundVariablesOwner: unique('playground_variables_owner').on(t.workspaceId, t.memberId),
|
|
43
|
+
}))
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Name: porter-transfer.ts
|
|
3
|
+
* Purpose: Arc R gatekeeper storage. Transfer CODES are the human front
|
|
4
|
+
* door (single-use, short TTL, hashed at rest - the receiving admin
|
|
5
|
+
* reads one out to the sender); transfer TOKENS are what a redeemed code
|
|
6
|
+
* becomes (scoped to porter ingest endpoints and ONE run, never admin
|
|
7
|
+
* access). Both are directional: they grant write-IN only.
|
|
8
|
+
*
|
|
9
|
+
* Author: Raghunath J
|
|
10
|
+
* License: MIT
|
|
11
|
+
*/
|
|
12
|
+
import { pgTable, text, timestamp, jsonb, index } from 'drizzle-orm/pg-core'
|
|
13
|
+
|
|
14
|
+
export const porterTransferCodes = pgTable('porter_transfer_codes', {
|
|
15
|
+
id: text('id').primaryKey(),
|
|
16
|
+
workspaceId: text('workspace_id').notNull(),
|
|
17
|
+
/** The RECEIVING environment the code grants write-in to. */
|
|
18
|
+
/** sha256 of the normalized code - the plaintext exists only in the
|
|
19
|
+
* generating response and the receiver's screen. */
|
|
20
|
+
codeHash: text('code_hash').notNull(),
|
|
21
|
+
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
|
|
22
|
+
redeemedAt: timestamp('redeemed_at', { withTimezone: true }),
|
|
23
|
+
revokedAt: timestamp('revoked_at', { withTimezone: true }),
|
|
24
|
+
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
25
|
+
createdBy: text('created_by'),
|
|
26
|
+
}, (t) => ({
|
|
27
|
+
hashIdx: index('porter_codes_hash_idx').on(t.codeHash),
|
|
28
|
+
workspaceIdx: index('porter_codes_workspace_idx').on(t.workspaceId),
|
|
29
|
+
}))
|
|
30
|
+
|
|
31
|
+
export const porterTransferTokens = pgTable('porter_transfer_tokens', {
|
|
32
|
+
id: text('id').primaryKey(),
|
|
33
|
+
workspaceId: text('workspace_id').notNull(),
|
|
34
|
+
tokenHash: text('token_hash').notNull(),
|
|
35
|
+
/** The code this token was redeemed from (audit trail). */
|
|
36
|
+
codeId: text('code_id').notNull(),
|
|
37
|
+
/** The ONE receiver-side run this token is allowed to feed (set when the
|
|
38
|
+
* transfer starts; a token never feeds two runs). */
|
|
39
|
+
runId: text('run_id'),
|
|
40
|
+
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
|
|
41
|
+
revokedAt: timestamp('revoked_at', { withTimezone: true }),
|
|
42
|
+
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
43
|
+
/** Sender-declared audit info ({url, label} - informational only). */
|
|
44
|
+
senderInfo: jsonb('sender_info').notNull().default({}),
|
|
45
|
+
}, (t) => ({
|
|
46
|
+
hashIdx: index('porter_tokens_hash_idx').on(t.tokenHash),
|
|
47
|
+
}))
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Name: realtime.ts
|
|
3
|
+
* Purpose: The realtime_events table - the bounded replay window for the
|
|
4
|
+
* Postgres fan-out, and the row a NOTIFY receiver reads back.
|
|
5
|
+
*
|
|
6
|
+
* Author: Raghunath J
|
|
7
|
+
* License: MIT
|
|
8
|
+
*
|
|
9
|
+
* Dependencies/Notes: Realtime R2 (tasks/plan-realtime.md section 4.4).
|
|
10
|
+
* One bigserial mints the order for every API process; `id` is
|
|
11
|
+
* the `<epoch>-<seq>` cursor a client resumes with; `data` holds
|
|
12
|
+
* ids, slugs and changed-field names, never a document. Pruned by
|
|
13
|
+
* the queue worker like rate_limit_counters; migration 0028 and
|
|
14
|
+
* the ensure-system-schema guard create it.
|
|
15
|
+
*/
|
|
16
|
+
import { pgTable, text, jsonb, bigint, bigserial, index, uniqueIndex, primaryKey } from 'drizzle-orm/pg-core'
|
|
17
|
+
|
|
18
|
+
export const realtimeEvents = pgTable('realtime_events', {
|
|
19
|
+
seq: bigserial('seq', { mode: 'number' }).primaryKey(),
|
|
20
|
+
channel: text('channel').notNull(),
|
|
21
|
+
id: text('id').notNull(),
|
|
22
|
+
t: bigint('t', { mode: 'number' }).notNull(),
|
|
23
|
+
type: text('type').notNull(),
|
|
24
|
+
data: jsonb('data').notNull().default({}),
|
|
25
|
+
}, (t) => [
|
|
26
|
+
index('realtime_events_channel_seq_idx').on(t.channel, t.seq),
|
|
27
|
+
uniqueIndex('realtime_events_channel_id_idx').on(t.channel, t.id),
|
|
28
|
+
])
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The realtime_presence table (R3): one row per joined session per scope
|
|
32
|
+
* channel, the session record as JSON and its expiry for the sweep.
|
|
33
|
+
* Ephemeral by construction - pruned by the queue worker, never read by
|
|
34
|
+
* the activity log; migration 0029 and the ensure-system-schema guard
|
|
35
|
+
* create it.
|
|
36
|
+
*/
|
|
37
|
+
export const realtimePresence = pgTable('realtime_presence', {
|
|
38
|
+
channel: text('channel').notNull(),
|
|
39
|
+
sessionId: text('session_id').notNull(),
|
|
40
|
+
data: jsonb('data').notNull().default({}),
|
|
41
|
+
expiresAt: bigint('expires_at', { mode: 'number' }).notNull(),
|
|
42
|
+
}, (t) => [
|
|
43
|
+
primaryKey({ columns: [t.channel, t.sessionId] }),
|
|
44
|
+
index('realtime_presence_expires_idx').on(t.expiresAt),
|
|
45
|
+
])
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Name: releases.ts
|
|
3
|
+
* Purpose: Releases v1 (closure plan Phase 5B): a reviewed, immutable
|
|
4
|
+
* promotion from one environment to the next. `releases` carries
|
|
5
|
+
* the state machine (draft -> in_review -> approved -> applying ->
|
|
6
|
+
* applied | failed) with who did what and when, the approved
|
|
7
|
+
* payload's checksum and the failure record; `release_items` is
|
|
8
|
+
* one row per object with the review snapshot, the frozen payload
|
|
9
|
+
* approval wrote, and the per-item apply outcome;
|
|
10
|
+
* `release_history` is the operator's ledger of every transition
|
|
11
|
+
* and attempt; `promotion_log` stays the per-object trail.
|
|
12
|
+
*
|
|
13
|
+
* Author: Raghunath J
|
|
14
|
+
* License: MIT
|
|
15
|
+
*
|
|
16
|
+
* Dependencies/Notes: Migration 0049 added the lifecycle columns, the
|
|
17
|
+
* status check, the item identity (one per object per release)
|
|
18
|
+
* and the history table, and turned legacy `pending` into `draft`.
|
|
19
|
+
*/
|
|
20
|
+
import { pgTable, text, timestamp, jsonb, integer, index, uniqueIndex } from 'drizzle-orm/pg-core'
|
|
21
|
+
import { workspaces } from './workspaces'
|
|
22
|
+
|
|
23
|
+
export const releases = pgTable('releases', {
|
|
24
|
+
id: text('id').primaryKey(),
|
|
25
|
+
name: text('name').notNull(),
|
|
26
|
+
workspaceId: text('workspace_id').notNull().references(() => workspaces.id, { onDelete: 'cascade' }),
|
|
27
|
+
/** draft | in_review | approved | applying | applied | failed */
|
|
28
|
+
status: text('status').notNull().default('draft'),
|
|
29
|
+
createdBy: text('created_by'),
|
|
30
|
+
appliedBy: text('applied_by'),
|
|
31
|
+
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
32
|
+
appliedAt: timestamp('applied_at', { withTimezone: true }),
|
|
33
|
+
submittedAt: timestamp('submitted_at', { withTimezone: true }),
|
|
34
|
+
submittedBy: text('submitted_by'),
|
|
35
|
+
approvedAt: timestamp('approved_at', { withTimezone: true }),
|
|
36
|
+
approvedBy: text('approved_by'),
|
|
37
|
+
/** sha256 over the frozen payloads in apply order; the apply refuses when it no longer matches. */
|
|
38
|
+
approvedChecksum: text('approved_checksum'),
|
|
39
|
+
scheduledAt: timestamp('scheduled_at', { withTimezone: true }),
|
|
40
|
+
scheduledBy: text('scheduled_by'),
|
|
41
|
+
applyingStartedAt: timestamp('applying_started_at', { withTimezone: true }),
|
|
42
|
+
failedAt: timestamp('failed_at', { withTimezone: true }),
|
|
43
|
+
/** { stage, itemId?, error, errorId? } of the last failure. */
|
|
44
|
+
failure: jsonb('failure'),
|
|
45
|
+
attempts: integer('attempts').notNull().default(0),
|
|
46
|
+
}, (t) => ({
|
|
47
|
+
workspaceIdx: index('releases_workspace_idx').on(t.workspaceId),
|
|
48
|
+
}))
|
|
49
|
+
|
|
50
|
+
export const releaseItems = pgTable('release_items', {
|
|
51
|
+
id: text('id').primaryKey(),
|
|
52
|
+
releaseId: text('release_id').notNull().references(() => releases.id, { onDelete: 'cascade' }),
|
|
53
|
+
objectId: text('object_id').notNull(),
|
|
54
|
+
typeName: text('type_name').notNull(),
|
|
55
|
+
/** The main row at selection time: what review and diff show. */
|
|
56
|
+
fieldSnapshot: jsonb('field_snapshot').notNull(),
|
|
57
|
+
comparisonState: text('comparison_state'),
|
|
58
|
+
resolvedValues: jsonb('resolved_values'),
|
|
59
|
+
/** Written at APPROVE: the complete source (row, i18n rows, tags, extension data) the apply uses. */
|
|
60
|
+
frozenPayload: jsonb('frozen_payload'),
|
|
61
|
+
/** The target's fingerprint at APPROVE; a different one at apply is a conflict. */
|
|
62
|
+
targetFingerprint: text('target_fingerprint'),
|
|
63
|
+
/** Deterministic apply order within the release. */
|
|
64
|
+
position: integer('position'),
|
|
65
|
+
/** created | overwritten | unchanged | skipped | failed, per attempt. */
|
|
66
|
+
applyOutcome: text('apply_outcome'),
|
|
67
|
+
appliedAt: timestamp('applied_at', { withTimezone: true }),
|
|
68
|
+
applyError: text('apply_error'),
|
|
69
|
+
}, (t) => ({
|
|
70
|
+
releaseIdx: index('release_items_release_idx').on(t.releaseId),
|
|
71
|
+
objectIdx: index('release_items_object_idx').on(t.objectId),
|
|
72
|
+
identity: uniqueIndex('release_items_identity_unique').on(t.releaseId, t.typeName, t.objectId),
|
|
73
|
+
}))
|
|
74
|
+
|
|
75
|
+
export const releaseHistory = pgTable('release_history', {
|
|
76
|
+
id: text('id').primaryKey(),
|
|
77
|
+
releaseId: text('release_id').notNull(),
|
|
78
|
+
workspaceId: text('workspace_id').notNull(),
|
|
79
|
+
attempt: integer('attempt').notNull().default(0),
|
|
80
|
+
/** submit | withdraw | approve | schedule | unschedule | apply | retry | preflight-failed | apply-failed | applied */
|
|
81
|
+
action: text('action').notNull(),
|
|
82
|
+
fromStatus: text('from_status'),
|
|
83
|
+
toStatus: text('to_status').notNull(),
|
|
84
|
+
actor: text('actor'),
|
|
85
|
+
checksum: text('checksum'),
|
|
86
|
+
counts: jsonb('counts'),
|
|
87
|
+
errorId: text('error_id'),
|
|
88
|
+
errorSummary: text('error_summary'),
|
|
89
|
+
reason: text('reason'),
|
|
90
|
+
startedAt: timestamp('started_at', { withTimezone: true }).notNull().defaultNow(),
|
|
91
|
+
finishedAt: timestamp('finished_at', { withTimezone: true }),
|
|
92
|
+
}, (t) => ({
|
|
93
|
+
releaseIdx: index('release_history_release_idx').on(t.releaseId, t.startedAt),
|
|
94
|
+
}))
|
|
95
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Name: roles.ts
|
|
3
|
+
* Purpose: Drizzle schema for the `roles` table — workspace-defined custom
|
|
4
|
+
* member roles. members.role references a role's slug (or a built-in
|
|
5
|
+
* role name); permissions holds a MemberPermissions bundle.
|
|
6
|
+
*
|
|
7
|
+
* Author: Raghunath J
|
|
8
|
+
* License: MIT
|
|
9
|
+
*
|
|
10
|
+
* Dependencies/Notes: slug is the stable identifier (unique per workspace) —
|
|
11
|
+
* field-level access lists and env-role overrides reference slugs,
|
|
12
|
+
* never DB ids, so code-first configs survive environment moves.
|
|
13
|
+
*/
|
|
14
|
+
import { pgTable, text, timestamp, jsonb, unique } from 'drizzle-orm/pg-core'
|
|
15
|
+
|
|
16
|
+
export const roles = pgTable('roles', {
|
|
17
|
+
id: text('id').primaryKey(),
|
|
18
|
+
workspaceId: text('workspace_id').notNull(),
|
|
19
|
+
slug: text('slug').notNull(),
|
|
20
|
+
name: text('name').notNull(),
|
|
21
|
+
description: text('description'),
|
|
22
|
+
permissions: jsonb('permissions').notNull().default({}),
|
|
23
|
+
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
24
|
+
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
25
|
+
}, (t) => ({
|
|
26
|
+
uniqWorkspaceSlug: unique().on(t.workspaceId, t.slug),
|
|
27
|
+
}))
|