@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.
Files changed (127) hide show
  1. package/README.md +73 -2
  2. package/drizzle/0000_flimsy_shotgun.sql +339 -0
  3. package/drizzle/0001_app_data.sql +11 -0
  4. package/drizzle/0002_add_indexes.sql +36 -0
  5. package/drizzle/0003_extension_data_rename.sql +3 -0
  6. package/drizzle/0004_extension_data_published_globals.sql +12 -0
  7. package/drizzle/0005_auth.sql +51 -0
  8. package/drizzle/0006_auth_identities.sql +13 -0
  9. package/drizzle/0007_ai.sql +18 -0
  10. package/drizzle/0008_ai_chat_threads.sql +5 -0
  11. package/drizzle/0009_forms_source_slug.sql +11 -0
  12. package/drizzle/0010_rbac_roles_status.sql +14 -0
  13. package/drizzle/0011_jobs_v2.sql +67 -0
  14. package/drizzle/0012_search_platform.sql +36 -0
  15. package/drizzle/0013_search_synonym_label.sql +2 -0
  16. package/drizzle/0014_object_types_disabled.sql +2 -0
  17. package/drizzle/0015_playground_collections.sql +25 -0
  18. package/drizzle/0016_catchup_activity_promotion.sql +36 -0
  19. package/drizzle/0017_fields_disabled.sql +3 -0
  20. package/drizzle/0018_object_types_plural.sql +1 -0
  21. package/drizzle/0019_member_profile.sql +2 -0
  22. package/drizzle/0020_paused_jobs.sql +7 -0
  23. package/drizzle/0021_form_submission_notes.sql +2 -0
  24. package/drizzle/0022_paused_jobs_env_reason.sql +4 -0
  25. package/drizzle/0023_scale_indexes.sql +3 -0
  26. package/drizzle/0024_migration_runs.sql +24 -0
  27. package/drizzle/0025_porter_transfer.sql +30 -0
  28. package/drizzle/0026_cron_timezone.sql +1 -0
  29. package/drizzle/0027_rate_limit_counters.sql +6 -0
  30. package/drizzle/0028_realtime_events.sql +10 -0
  31. package/drizzle/0030_delivery_artifacts.sql +10 -0
  32. package/drizzle/0031_realtime_presence.sql +8 -0
  33. package/drizzle/0032_schema_pushes.sql +15 -0
  34. package/drizzle/0033_studio_design_capability.sql +37 -0
  35. package/drizzle/0034_object_types_provided_by.sql +12 -0
  36. package/drizzle/0035_custom_table_owners.sql +24 -0
  37. package/drizzle/0036_revision_version_unique.sql +42 -0
  38. package/drizzle/0037_job_queue_dedupe.sql +20 -0
  39. package/drizzle/0038_search_documents.sql +87 -0
  40. package/drizzle/0039_job_prune_indexes.sql +25 -0
  41. package/drizzle/0040_extension_data_object_idx.sql +10 -0
  42. package/drizzle/0041_knowledge.sql +79 -0
  43. package/drizzle/0042_schema_pushes_checksum.sql +7 -0
  44. package/drizzle/0043_guard_adoption.sql +12 -0
  45. package/drizzle/0044_retention_indexes.sql +7 -0
  46. package/drizzle/0045_extension_install_per_env.sql +40 -0
  47. package/drizzle/0046_extension_migrations.sql +18 -0
  48. package/drizzle/0047_environment_role.sql +25 -0
  49. package/drizzle/0048_environment_role_adoption.sql +30 -0
  50. package/drizzle/0049_release_lifecycle.sql +55 -0
  51. package/drizzle/0050_member_env_membership.sql +21 -0
  52. package/drizzle/0051_remove_environments.sql +350 -0
  53. package/drizzle/0052_release_actor_columns.sql +9 -0
  54. package/drizzle/0053_kernel_event_record.sql +26 -0
  55. package/drizzle/0054_kernel_run_identity.sql +13 -0
  56. package/drizzle/0055_kernel_identities.sql +19 -0
  57. package/drizzle/0056_forms_leave_core.sql +7 -0
  58. package/drizzle/0057_auth_leave_core.sql +8 -0
  59. package/drizzle/0058_member_grants.sql +11 -0
  60. package/drizzle/0059_extension_granted_grants.sql +7 -0
  61. package/drizzle/0060_connections.sql +21 -0
  62. package/drizzle/meta/0000_snapshot.json +1692 -0
  63. package/drizzle/meta/0002_snapshot.json +1935 -0
  64. package/drizzle/meta/0003_snapshot.json +1935 -0
  65. package/drizzle/meta/_journal.json +426 -0
  66. package/package.json +85 -4
  67. package/src/cache-keys.ts +43 -0
  68. package/src/client.ts +136 -0
  69. package/src/content-ddl.ts +70 -0
  70. package/src/content-tables.ts +110 -0
  71. package/src/custom-tables-loader.ts +65 -0
  72. package/src/ensure-system-schema.ts +195 -0
  73. package/src/extension-install-state.ts +220 -0
  74. package/src/extension-migrations.ts +270 -0
  75. package/src/generated/content-tables.ts +271 -0
  76. package/src/generated/custom-tables.ts +18 -0
  77. package/src/index.ts +24 -0
  78. package/src/instrument.ts +139 -0
  79. package/src/knowledge-schema.ts +232 -0
  80. package/src/migrate.ts +67 -0
  81. package/src/prepare-policy.ts +118 -0
  82. package/src/reconcile.ts +115 -0
  83. package/src/schema/ai-chats.ts +36 -0
  84. package/src/schema/ai-settings.ts +24 -0
  85. package/src/schema/api-keys.ts +37 -0
  86. package/src/schema/audit-log.ts +54 -0
  87. package/src/schema/connections.ts +35 -0
  88. package/src/schema/custom-table-owners.ts +32 -0
  89. package/src/schema/delivery-artifacts.ts +33 -0
  90. package/src/schema/entity-tags.ts +33 -0
  91. package/src/schema/extension-data.ts +38 -0
  92. package/src/schema/extension-migrations.ts +42 -0
  93. package/src/schema/fields.ts +37 -0
  94. package/src/schema/globals.ts +35 -0
  95. package/src/schema/identities.ts +27 -0
  96. package/src/schema/index.ts +51 -0
  97. package/src/schema/jobs.ts +104 -0
  98. package/src/schema/knowledge.ts +85 -0
  99. package/src/schema/media.ts +52 -0
  100. package/src/schema/member-grants.ts +34 -0
  101. package/src/schema/members.ts +49 -0
  102. package/src/schema/migration-runs.ts +46 -0
  103. package/src/schema/object-types.ts +41 -0
  104. package/src/schema/objects.ts +38 -0
  105. package/src/schema/paused-jobs.ts +25 -0
  106. package/src/schema/playground.ts +43 -0
  107. package/src/schema/porter-transfer.ts +47 -0
  108. package/src/schema/realtime.ts +45 -0
  109. package/src/schema/releases.ts +95 -0
  110. package/src/schema/roles.ts +27 -0
  111. package/src/schema/saved-views.ts +33 -0
  112. package/src/schema/schema-pushes.ts +43 -0
  113. package/src/schema/search-documents.ts +47 -0
  114. package/src/schema/search-query-log.ts +24 -0
  115. package/src/schema/search-recommendations.ts +27 -0
  116. package/src/schema/search-synonyms.ts +23 -0
  117. package/src/schema/webhook-deliveries.ts +37 -0
  118. package/src/schema/webhooks.ts +36 -0
  119. package/src/schema/workspace-activity.ts +35 -0
  120. package/src/schema/workspace-domains.ts +24 -0
  121. package/src/schema/workspace-installed-apps.ts +48 -0
  122. package/src/schema/workspaces.ts +24 -0
  123. package/src/schema-kit.ts +64 -0
  124. package/src/sealed.ts +84 -0
  125. package/src/seed/skeleton.ts +77 -0
  126. package/src/sql/apply-gin-indexes.ts +31 -0
  127. 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
+ }))