@headless-lms/adapter-db 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +79 -0
  2. package/dist/client.d.ts +1 -1
  3. package/dist/client.js.map +1 -1
  4. package/dist/index.d.ts +2 -1
  5. package/dist/index.js +3 -2
  6. package/dist/migrate.js.map +1 -1
  7. package/dist/repositories/assets.d.ts +1 -1
  8. package/dist/repositories/assets.js +4 -1
  9. package/dist/repositories/assets.js.map +1 -1
  10. package/dist/repositories/automations.d.ts +7 -5
  11. package/dist/repositories/automations.js +35 -10
  12. package/dist/repositories/automations.js.map +1 -1
  13. package/dist/repositories/content.d.ts +21 -7
  14. package/dist/repositories/content.js +168 -18
  15. package/dist/repositories/content.js.map +1 -1
  16. package/dist/repositories/course-analytics.d.ts +15 -0
  17. package/dist/repositories/course-analytics.js +137 -0
  18. package/dist/repositories/course-analytics.js.map +1 -0
  19. package/dist/repositories/credentials.js +2 -0
  20. package/dist/repositories/credentials.js.map +1 -1
  21. package/dist/repositories/dashboard.js +2 -0
  22. package/dist/repositories/dashboard.js.map +1 -1
  23. package/dist/repositories/discussion.d.ts +1 -1
  24. package/dist/repositories/discussion.js +2 -0
  25. package/dist/repositories/discussion.js.map +1 -1
  26. package/dist/repositories/entitlements.d.ts +11 -5
  27. package/dist/repositories/entitlements.js +42 -17
  28. package/dist/repositories/entitlements.js.map +1 -1
  29. package/dist/repositories/identity.js +2 -0
  30. package/dist/repositories/identity.js.map +1 -1
  31. package/dist/repositories/integrations.d.ts +1 -1
  32. package/dist/repositories/integrations.js +2 -0
  33. package/dist/repositories/integrations.js.map +1 -1
  34. package/dist/repositories/learn.js +6 -4
  35. package/dist/repositories/learn.js.map +1 -1
  36. package/dist/repositories/members.js +2 -0
  37. package/dist/repositories/members.js.map +1 -1
  38. package/dist/repositories/org-user-profile.js.map +1 -1
  39. package/dist/repositories/organizations.d.ts +4 -6
  40. package/dist/repositories/organizations.js +26 -45
  41. package/dist/repositories/organizations.js.map +1 -1
  42. package/dist/repositories/outbox.js +3 -0
  43. package/dist/repositories/outbox.js.map +1 -1
  44. package/dist/repositories/pg-errors.d.ts +53 -5
  45. package/dist/repositories/pg-errors.js +97 -8
  46. package/dist/repositories/pg-errors.js.map +1 -1
  47. package/dist/repositories/progress.d.ts +1 -1
  48. package/dist/repositories/progress.js +3 -0
  49. package/dist/repositories/progress.js.map +1 -1
  50. package/dist/repositories/settings.js +2 -0
  51. package/dist/repositories/settings.js.map +1 -1
  52. package/dist/repositories/students.d.ts +2 -1
  53. package/dist/repositories/students.js +80 -3
  54. package/dist/repositories/students.js.map +1 -1
  55. package/dist/schema/assets.d.ts +17 -0
  56. package/dist/schema/assets.js +2 -1
  57. package/dist/schema/assets.js.map +1 -1
  58. package/dist/schema/automations.d.ts +85 -8
  59. package/dist/schema/automations.js +7 -4
  60. package/dist/schema/automations.js.map +1 -1
  61. package/dist/schema/better-auth.js.map +1 -1
  62. package/dist/schema/content.d.ts +272 -22
  63. package/dist/schema/content.js +40 -5
  64. package/dist/schema/content.js.map +1 -1
  65. package/dist/schema/credentials.js +1 -1
  66. package/dist/schema/credentials.js.map +1 -1
  67. package/dist/schema/discussion.d.ts +34 -0
  68. package/dist/schema/discussion.js +4 -2
  69. package/dist/schema/discussion.js.map +1 -1
  70. package/dist/schema/entitlements.d.ts +52 -1
  71. package/dist/schema/entitlements.js +15 -5
  72. package/dist/schema/entitlements.js.map +1 -1
  73. package/dist/schema/identity.js.map +1 -1
  74. package/dist/schema/index.d.ts +3 -3
  75. package/dist/schema/index.js +4 -2
  76. package/dist/schema/index.js.map +1 -1
  77. package/dist/schema/integrations.js +1 -1
  78. package/dist/schema/integrations.js.map +1 -1
  79. package/dist/schema/organizations.d.ts +17 -0
  80. package/dist/schema/organizations.js +2 -1
  81. package/dist/schema/organizations.js.map +1 -1
  82. package/dist/schema/outbox.d.ts +17 -0
  83. package/dist/schema/outbox.js +1 -0
  84. package/dist/schema/outbox.js.map +1 -1
  85. package/dist/schema/progress.d.ts +17 -0
  86. package/dist/schema/progress.js +1 -0
  87. package/dist/schema/progress.js.map +1 -1
  88. package/dist/schema/settings.js.map +1 -1
  89. package/dist/unit-of-work.js +7 -2
  90. package/dist/unit-of-work.js.map +1 -1
  91. package/drizzle/0000_baseline.sql +0 -1
  92. package/drizzle/0001_new_sentry.sql +43 -0
  93. package/drizzle/0002_automation_kinds.sql +7 -0
  94. package/drizzle/meta/0000_snapshot.json +167 -597
  95. package/drizzle/meta/0001_snapshot.json +3769 -0
  96. package/drizzle/meta/0002_snapshot.json +3770 -0
  97. package/drizzle/meta/_journal.json +16 -2
  98. package/package.json +3 -2
@@ -1 +1 @@
1
- {"version":3,"file":"discussion.js","names":[],"sources":["../../src/schema/discussion.ts"],"sourcesContent":["// discussion tables — comments on an activity, their reactions and reports.\n// Configuration is not here: the course's comment settings ride on the course's\n// `content` settings row, and an activity's override rides on the activity's\n// own settings blob.\n//\n// The author is always an org user, so one column covers both\n// learners and staff. Staff-ness is NOT stored — it is read from the author's\n// current role. A removed comment keeps its row so replies below it survive;\n// only the body stops being served.\n//\n// No course_id: which course an activity sits in is content's fact and changes\n// when a course is restructured. The staff comment list joins to modules to\n// scope by course.\nimport {\n pgTable,\n text,\n timestamp,\n primaryKey,\n foreignKey,\n unique,\n index,\n check,\n} from 'drizzle-orm/pg-core';\nimport { sql } from 'drizzle-orm';\nimport type { Comment, CommentReaction, CommentReport } from '@headless-lms/core/schemas';\nimport { genId } from '@headless-lms/core/shared/id';\nimport { organizations, orgUsers } from './organizations.js';\nimport { activities } from './content.js';\nimport type { Expect, NoDrift } from './drift.js';\n\nexport const comments = pgTable(\n 'comments',\n {\n orgId: text('org_id')\n .notNull()\n .references(() => organizations.id),\n id: text('id')\n .notNull()\n .$defaultFn(() => genId('comment')),\n activityId: text('activity_id').notNull(),\n parentId: text('parent_id'),\n orgUserId: text('org_user_id').notNull(),\n body: text('body').notNull(),\n status: text('status', { enum: ['pending', 'published', 'removed'] })\n .notNull()\n .default('published'),\n removedBy: text('removed_by'),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp('updated_at', { withTimezone: true })\n .notNull()\n .defaultNow()\n .$onUpdate(() => new Date()),\n },\n (t) => ({\n pk: primaryKey({ columns: [t.orgId, t.id] }),\n // Removing an activity removes its discussion (spec, boundary 1).\n activityFk: foreignKey({\n columns: [t.orgId, t.activityId],\n foreignColumns: [activities.orgId, activities.id],\n }).onDelete('cascade'),\n authorFk: foreignKey({\n columns: [t.orgId, t.orgUserId],\n foreignColumns: [orgUsers.orgId, orgUsers.id],\n }),\n removedByFk: foreignKey({\n columns: [t.orgId, t.removedBy],\n foreignColumns: [orgUsers.orgId, orgUsers.id],\n }),\n // No cascade: removing a parent must not delete its replies.\n parentFk: foreignKey({\n columns: [t.orgId, t.parentId],\n foreignColumns: [t.orgId, t.id],\n }),\n // removed_by is set if and only if the comment is removed.\n removedCk: check(\n 'comments_removed_by_check',\n sql`(${t.status} = 'removed') = (${t.removedBy} is not null)`,\n ),\n activityIdx: index('comments_activity_idx').on(t.orgId, t.activityId, t.status, t.createdAt),\n // The staff list filters by status org-wide, then narrows by course through\n // the activity join — so the index leads on status, not on a stored course.\n queueIdx: index('comments_queue_idx').on(t.orgId, t.status, t.createdAt),\n }),\n);\n\nexport const commentReactions = pgTable(\n 'comment_reactions',\n {\n orgId: text('org_id')\n .notNull()\n .references(() => organizations.id),\n commentId: text('comment_id').notNull(),\n orgUserId: text('org_user_id').notNull(),\n emoji: text('emoji').notNull(),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n },\n (t) => ({\n // One reaction per person, per comment.\n pk: primaryKey({ columns: [t.orgId, t.commentId, t.orgUserId] }),\n commentFk: foreignKey({\n columns: [t.orgId, t.commentId],\n foreignColumns: [comments.orgId, comments.id],\n }).onDelete('cascade'),\n authorFk: foreignKey({\n columns: [t.orgId, t.orgUserId],\n foreignColumns: [orgUsers.orgId, orgUsers.id],\n }),\n }),\n);\n\nexport const commentReports = pgTable(\n 'comment_reports',\n {\n orgId: text('org_id')\n .notNull()\n .references(() => organizations.id),\n id: text('id')\n .notNull()\n .$defaultFn(() => genId('commentReport')),\n commentId: text('comment_id').notNull(),\n orgUserId: text('org_user_id').notNull(),\n reason: text('reason').notNull().default(''),\n resolvedAt: timestamp('resolved_at', { withTimezone: true }),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n },\n (t) => ({\n pk: primaryKey({ columns: [t.orgId, t.id] }),\n // One report per person, per comment.\n reporterUq: unique().on(t.orgId, t.commentId, t.orgUserId),\n commentFk: foreignKey({\n columns: [t.orgId, t.commentId],\n foreignColumns: [comments.orgId, comments.id],\n }).onDelete('cascade'),\n reporterFk: foreignKey({\n columns: [t.orgId, t.orgUserId],\n foreignColumns: [orgUsers.orgId, orgUsers.id],\n }),\n openIdx: index('comment_reports_open_idx').on(t.orgId, t.commentId, t.resolvedAt),\n }),\n);\n\ntype _CommentsDrift = Expect<NoDrift<typeof comments.$inferSelect, Comment>>;\ntype _CommentReactionsDrift = Expect<NoDrift<typeof commentReactions.$inferSelect, CommentReaction>>;\ntype _CommentReportsDrift = Expect<NoDrift<typeof commentReports.$inferSelect, CommentReport>>;\n\n"],"mappings":"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"}
1
+ {"version":3,"file":"discussion.js","names":[],"sources":["../../src/schema/discussion.ts"],"sourcesContent":["// discussion tables — comments on an activity, their reactions and reports.\n// Configuration is not here: the course's comment settings ride on the course's\n// `content` settings row, and an activity's override rides on the activity's\n// own settings blob.\n//\n// The author is always an org user, so one column covers both\n// learners and staff. Staff-ness is NOT stored — it is read from the author's\n// current role. A removed comment keeps its row so replies below it survive;\n// only the body stops being served.\n//\n// No course_id: which course an activity sits in is content's fact and changes\n// when a course is restructured. The staff comment list joins to modules to\n// scope by course.\nimport {\n pgTable,\n text,\n timestamp,\n primaryKey,\n foreignKey,\n unique,\n index,\n check,\n} from \"drizzle-orm/pg-core\";\nimport { sql } from \"drizzle-orm\";\nimport type { Comment, CommentReaction, CommentReport } from \"@headless-lms/core/schemas\";\nimport { genId } from \"@headless-lms/core/shared/id\";\nimport { organizations, orgUsers } from \"./organizations.js\";\nimport { activities } from \"./content.js\";\nimport type { Expect, NoDrift } from \"./drift.js\";\n\nexport const comments = pgTable(\n \"comments\",\n {\n orgId: text(\"org_id\")\n .notNull()\n .references(() => organizations.id),\n id: text(\"id\")\n .notNull()\n .$defaultFn(() => genId(\"comment\")),\n activityId: text(\"activity_id\").notNull(),\n parentId: text(\"parent_id\"),\n orgUserId: text(\"org_user_id\").notNull(),\n body: text(\"body\").notNull(),\n status: text(\"status\", { enum: [\"pending\", \"published\", \"removed\"] })\n .notNull()\n .default(\"published\"),\n removedBy: text(\"removed_by\"),\n createdAt: timestamp(\"created_at\", { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true })\n .notNull()\n .defaultNow()\n .$onUpdate(() => new Date()),\n },\n (t) => ({\n pk: primaryKey({ columns: [t.orgId, t.id] }),\n // Removing an activity removes its discussion (spec, boundary 1).\n activityFk: foreignKey({\n columns: [t.orgId, t.activityId],\n foreignColumns: [activities.orgId, activities.id],\n }).onDelete(\"cascade\"),\n authorFk: foreignKey({\n columns: [t.orgId, t.orgUserId],\n foreignColumns: [orgUsers.orgId, orgUsers.id],\n }),\n removedByFk: foreignKey({\n columns: [t.orgId, t.removedBy],\n foreignColumns: [orgUsers.orgId, orgUsers.id],\n }),\n // No cascade: removing a parent must not delete its replies.\n parentFk: foreignKey({\n columns: [t.orgId, t.parentId],\n foreignColumns: [t.orgId, t.id],\n }),\n // removed_by is set if and only if the comment is removed.\n removedCk: check(\n \"comments_removed_by_check\",\n sql`(${t.status} = 'removed') = (${t.removedBy} is not null)`,\n ),\n activityIdx: index(\"comments_activity_idx\").on(t.orgId, t.activityId, t.status, t.createdAt),\n // The staff list filters by status org-wide, then narrows by course through\n // the activity join — so the index leads on status, not on a stored course.\n queueIdx: index(\"comments_queue_idx\").on(t.orgId, t.status, t.createdAt),\n }),\n);\n\nexport const commentReactions = pgTable(\n \"comment_reactions\",\n {\n orgId: text(\"org_id\")\n .notNull()\n .references(() => organizations.id),\n commentId: text(\"comment_id\").notNull(),\n orgUserId: text(\"org_user_id\").notNull(),\n emoji: text(\"emoji\").notNull(),\n createdAt: timestamp(\"created_at\", { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true })\n .notNull()\n .defaultNow()\n .$onUpdate(() => new Date()),\n },\n (t) => ({\n // One reaction per person, per comment.\n pk: primaryKey({ columns: [t.orgId, t.commentId, t.orgUserId] }),\n commentFk: foreignKey({\n columns: [t.orgId, t.commentId],\n foreignColumns: [comments.orgId, comments.id],\n }).onDelete(\"cascade\"),\n authorFk: foreignKey({\n columns: [t.orgId, t.orgUserId],\n foreignColumns: [orgUsers.orgId, orgUsers.id],\n }),\n }),\n);\n\nexport const commentReports = pgTable(\n \"comment_reports\",\n {\n orgId: text(\"org_id\")\n .notNull()\n .references(() => organizations.id),\n id: text(\"id\")\n .notNull()\n .$defaultFn(() => genId(\"commentReport\")),\n commentId: text(\"comment_id\").notNull(),\n orgUserId: text(\"org_user_id\").notNull(),\n reason: text(\"reason\").notNull().default(\"\"),\n resolvedAt: timestamp(\"resolved_at\", { withTimezone: true }),\n createdAt: timestamp(\"created_at\", { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true })\n .notNull()\n .defaultNow()\n .$onUpdate(() => new Date()),\n },\n (t) => ({\n pk: primaryKey({ columns: [t.orgId, t.id] }),\n // One report per person, per comment.\n reporterUq: unique().on(t.orgId, t.commentId, t.orgUserId),\n commentFk: foreignKey({\n columns: [t.orgId, t.commentId],\n foreignColumns: [comments.orgId, comments.id],\n }).onDelete(\"cascade\"),\n reporterFk: foreignKey({\n columns: [t.orgId, t.orgUserId],\n foreignColumns: [orgUsers.orgId, orgUsers.id],\n }),\n openIdx: index(\"comment_reports_open_idx\").on(t.orgId, t.commentId, t.resolvedAt),\n }),\n);\n\ntype _CommentsDrift = Expect<NoDrift<typeof comments.$inferSelect, Comment>>;\ntype _CommentReactionsDrift = Expect<\n NoDrift<typeof commentReactions.$inferSelect, CommentReaction>\n>;\ntype _CommentReportsDrift = Expect<NoDrift<typeof commentReports.$inferSelect, CommentReport>>;\n"],"mappings":"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"}
@@ -54,6 +54,23 @@ declare const entitlements: import("drizzle-orm/pg-core").PgTableWithColumns<{
54
54
  identity: undefined;
55
55
  generated: undefined;
56
56
  }, {}, {}>;
57
+ bundleId: import("drizzle-orm/pg-core").PgColumn<{
58
+ name: "bundle_id";
59
+ tableName: "entitlements";
60
+ dataType: "string";
61
+ columnType: "PgText";
62
+ data: string;
63
+ driverParam: string;
64
+ notNull: false;
65
+ hasDefault: false;
66
+ isPrimaryKey: false;
67
+ isAutoincrement: false;
68
+ hasRuntimeDefault: false;
69
+ enumValues: [string, ...string[]];
70
+ baseColumn: never;
71
+ identity: undefined;
72
+ generated: undefined;
73
+ }, {}, {}>;
57
74
  contentId: import("drizzle-orm/pg-core").PgColumn<{
58
75
  name: "content_id";
59
76
  tableName: "entitlements";
@@ -61,7 +78,7 @@ declare const entitlements: import("drizzle-orm/pg-core").PgTableWithColumns<{
61
78
  columnType: "PgText";
62
79
  data: string;
63
80
  driverParam: string;
64
- notNull: true;
81
+ notNull: false;
65
82
  hasDefault: false;
66
83
  isPrimaryKey: false;
67
84
  isAutoincrement: false;
@@ -139,6 +156,40 @@ declare const entitlements: import("drizzle-orm/pg-core").PgTableWithColumns<{
139
156
  identity: undefined;
140
157
  generated: undefined;
141
158
  }, {}, {}>;
159
+ createdAt: import("drizzle-orm/pg-core").PgColumn<{
160
+ name: "created_at";
161
+ tableName: "entitlements";
162
+ dataType: "date";
163
+ columnType: "PgTimestamp";
164
+ data: Date;
165
+ driverParam: string;
166
+ notNull: true;
167
+ hasDefault: true;
168
+ isPrimaryKey: false;
169
+ isAutoincrement: false;
170
+ hasRuntimeDefault: false;
171
+ enumValues: undefined;
172
+ baseColumn: never;
173
+ identity: undefined;
174
+ generated: undefined;
175
+ }, {}, {}>;
176
+ updatedAt: import("drizzle-orm/pg-core").PgColumn<{
177
+ name: "updated_at";
178
+ tableName: "entitlements";
179
+ dataType: "date";
180
+ columnType: "PgTimestamp";
181
+ data: Date;
182
+ driverParam: string;
183
+ notNull: true;
184
+ hasDefault: true;
185
+ isPrimaryKey: false;
186
+ isAutoincrement: false;
187
+ hasRuntimeDefault: false;
188
+ enumValues: undefined;
189
+ baseColumn: never;
190
+ identity: undefined;
191
+ generated: undefined;
192
+ }, {}, {}>;
142
193
  };
143
194
  dialect: "pg";
144
195
  }>;
@@ -1,28 +1,38 @@
1
1
  import { orgUsers, organizations } from "./organizations.js";
2
- import { contentItems } from "./content.js";
3
- import { foreignKey, pgTable, primaryKey, text, timestamp, unique } from "drizzle-orm/pg-core";
2
+ import { bundles, contentItems } from "./content.js";
3
+ import { sql } from "drizzle-orm";
4
+ import { check, foreignKey, pgTable, primaryKey, text, timestamp, unique } from "drizzle-orm/pg-core";
4
5
  import { genId } from "@headless-lms/core/shared/id";
5
6
  //#region src/schema/entitlements.ts
6
7
  const entitlements = pgTable("entitlements", {
7
8
  orgId: text("org_id").notNull().references(() => organizations.id),
8
9
  id: text("id").notNull().$defaultFn(() => genId("entitlement")),
9
10
  orgUserId: text("org_user_id").notNull(),
10
- contentId: text("content_id").notNull(),
11
+ bundleId: text("bundle_id"),
12
+ contentId: text("content_id"),
11
13
  status: text("status", { enum: ["active", "revoked"] }).notNull().default("active"),
12
14
  source: text("source").notNull().default("manual"),
13
15
  grantedAt: timestamp("granted_at", { withTimezone: true }).notNull().defaultNow(),
14
- expiresAt: timestamp("expires_at", { withTimezone: true })
16
+ expiresAt: timestamp("expires_at", { withTimezone: true }),
17
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
18
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow().$onUpdate(() => /* @__PURE__ */ new Date())
15
19
  }, (t) => ({
16
20
  pk: primaryKey({ columns: [t.orgId, t.id] }),
17
21
  contentFk: foreignKey({
18
22
  columns: [t.orgId, t.contentId],
19
23
  foreignColumns: [contentItems.orgId, contentItems.id]
20
24
  }).onDelete("cascade"),
25
+ bundleFk: foreignKey({
26
+ columns: [t.orgId, t.bundleId],
27
+ foreignColumns: [bundles.orgId, bundles.id]
28
+ }).onDelete("cascade"),
21
29
  orgUserFk: foreignKey({
22
30
  columns: [t.orgId, t.orgUserId],
23
31
  foreignColumns: [orgUsers.orgId, orgUsers.id]
24
32
  }).onDelete("cascade"),
25
- orgUserContentUq: unique().on(t.orgId, t.orgUserId, t.contentId)
33
+ orgUserContentUq: unique().on(t.orgId, t.orgUserId, t.contentId),
34
+ orgUserBundleUq: unique().on(t.orgId, t.orgUserId, t.bundleId),
35
+ targetCk: check("entitlements_target_check", sql`num_nonnulls(${t.bundleId}, ${t.contentId}) = 1`)
26
36
  }));
27
37
  //#endregion
28
38
  export { entitlements };
@@ -1 +1 @@
1
- {"version":3,"file":"entitlements.js","names":[],"sources":["../../src/schema/entitlements.ts"],"sourcesContent":["\nimport { pgTable, text, timestamp, primaryKey, foreignKey, unique } from 'drizzle-orm/pg-core';\nimport type { Entitlement } from '@headless-lms/core/schemas';\nimport { genId } from '@headless-lms/core/shared/id';\nimport { organizations, orgUsers } from './organizations.js';\nimport { contentItems } from './content.js';\nimport type { Expect, NoDrift } from './drift.js';\n\nexport const entitlements = pgTable(\n 'entitlements',\n {\n orgId: text('org_id')\n .notNull()\n .references(() => organizations.id),\n id: text('id')\n .notNull()\n .$defaultFn(() => genId('entitlement')),\n orgUserId: text('org_user_id').notNull(),\n contentId: text('content_id').notNull(),\n status: text('status', { enum: ['active', 'revoked'] })\n .notNull()\n .default('active'),\n // Free text: 'manual', 'import', integration ids, …\n source: text('source').notNull().default('manual'),\n grantedAt: timestamp('granted_at', { withTimezone: true }).notNull().defaultNow(),\n expiresAt: timestamp('expires_at', { withTimezone: true }), // null = lifetime\n },\n (t) => ({\n pk: primaryKey({ columns: [t.orgId, t.id] }),\n // Grants die with their content: deleting the registry row cascades here.\n contentFk: foreignKey({\n columns: [t.orgId, t.contentId],\n foreignColumns: [contentItems.orgId, contentItems.id],\n }).onDelete('cascade'),\n // org_users is org-scoped (composite PK) — the FK must match both columns.\n // Grants die with their org user: deleting the org_users row cascades here.\n orgUserFk: foreignKey({\n columns: [t.orgId, t.orgUserId],\n foreignColumns: [orgUsers.orgId, orgUsers.id],\n }).onDelete('cascade'),\n // One grant per (org, org user, content).\n orgUserContentUq: unique().on(t.orgId, t.orgUserId, t.contentId),\n }),\n);\n\ntype _EntitlementsDrift = Expect<NoDrift<typeof entitlements.$inferSelect, Entitlement>>;\n"],"mappings":";;;;;AAQA,MAAa,eAAe,QAC1B,gBACA;CACE,OAAO,KAAK,QAAQ,CAAC,CAClB,QAAQ,CAAC,CACT,iBAAiB,cAAc,EAAE;CACpC,IAAI,KAAK,IAAI,CAAC,CACX,QAAQ,CAAC,CACT,iBAAiB,MAAM,aAAa,CAAC;CACxC,WAAW,KAAK,aAAa,CAAC,CAAC,QAAQ;CACvC,WAAW,KAAK,YAAY,CAAC,CAAC,QAAQ;CACtC,QAAQ,KAAK,UAAU,EAAE,MAAM,CAAC,UAAU,SAAS,EAAE,CAAC,CAAC,CACpD,QAAQ,CAAC,CACT,QAAQ,QAAQ;CAEnB,QAAQ,KAAK,QAAQ,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,QAAQ;CACjD,WAAW,UAAU,cAAc,EAAE,cAAc,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,WAAW;CAChF,WAAW,UAAU,cAAc,EAAE,cAAc,KAAK,CAAC;AAC3D,IACC,OAAO;CACN,IAAI,WAAW,EAAE,SAAS,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;CAE3C,WAAW,WAAW;EACpB,SAAS,CAAC,EAAE,OAAO,EAAE,SAAS;EAC9B,gBAAgB,CAAC,aAAa,OAAO,aAAa,EAAE;CACtD,CAAC,CAAC,CAAC,SAAS,SAAS;CAGrB,WAAW,WAAW;EACpB,SAAS,CAAC,EAAE,OAAO,EAAE,SAAS;EAC9B,gBAAgB,CAAC,SAAS,OAAO,SAAS,EAAE;CAC9C,CAAC,CAAC,CAAC,SAAS,SAAS;CAErB,kBAAkB,OAAO,CAAC,CAAC,GAAG,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS;AACjE,EACF"}
1
+ {"version":3,"file":"entitlements.js","names":[],"sources":["../../src/schema/entitlements.ts"],"sourcesContent":["import { sql } from \"drizzle-orm\";\nimport {\n pgTable,\n text,\n timestamp,\n primaryKey,\n foreignKey,\n unique,\n check,\n} from \"drizzle-orm/pg-core\";\nimport type { Entitlement } from \"@headless-lms/core/schemas\";\nimport { genId } from \"@headless-lms/core/shared/id\";\nimport { organizations, orgUsers } from \"./organizations.js\";\nimport { bundles, contentItems } from \"./content.js\";\nimport type { Expect, NoDrift } from \"./drift.js\";\n\nexport const entitlements = pgTable(\n \"entitlements\",\n {\n orgId: text(\"org_id\")\n .notNull()\n .references(() => organizations.id),\n id: text(\"id\")\n .notNull()\n .$defaultFn(() => genId(\"entitlement\")),\n orgUserId: text(\"org_user_id\").notNull(),\n bundleId: text(\"bundle_id\"),\n contentId: text(\"content_id\"),\n status: text(\"status\", { enum: [\"active\", \"revoked\"] })\n .notNull()\n .default(\"active\"),\n // Free text: 'manual', 'import', integration ids, …\n source: text(\"source\").notNull().default(\"manual\"),\n grantedAt: timestamp(\"granted_at\", { withTimezone: true }).notNull().defaultNow(),\n expiresAt: timestamp(\"expires_at\", { withTimezone: true }), // null = lifetime\n createdAt: timestamp(\"created_at\", { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true })\n .notNull()\n .defaultNow()\n .$onUpdate(() => new Date()),\n },\n (t) => ({\n pk: primaryKey({ columns: [t.orgId, t.id] }),\n // Grants die with their content: deleting the registry row cascades here.\n contentFk: foreignKey({\n columns: [t.orgId, t.contentId],\n foreignColumns: [contentItems.orgId, contentItems.id],\n }).onDelete(\"cascade\"),\n // Grants die with their bundle, same as with their content.\n bundleFk: foreignKey({\n columns: [t.orgId, t.bundleId],\n foreignColumns: [bundles.orgId, bundles.id],\n }).onDelete(\"cascade\"),\n // org_users is org-scoped (composite PK) — the FK must match both columns.\n // Grants die with their org user: deleting the org_users row cascades here.\n orgUserFk: foreignKey({\n columns: [t.orgId, t.orgUserId],\n foreignColumns: [orgUsers.orgId, orgUsers.id],\n }).onDelete(\"cascade\"),\n // One grant per (org, org user, content) and per (org, org user, bundle) —\n // the unused column is null, and nulls never collide, so each constraint\n // only sees grants of its own kind. Both are the upsert targets.\n orgUserContentUq: unique().on(t.orgId, t.orgUserId, t.contentId),\n orgUserBundleUq: unique().on(t.orgId, t.orgUserId, t.bundleId),\n // A grant targets exactly one of: a bundle or a content item.\n targetCk: check(\n \"entitlements_target_check\",\n sql`num_nonnulls(${t.bundleId}, ${t.contentId}) = 1`,\n ),\n }),\n);\n\ntype _EntitlementsDrift = Expect<NoDrift<typeof entitlements.$inferSelect, Entitlement>>;\n"],"mappings":";;;;;;AAgBA,MAAa,eAAe,QAC1B,gBACA;CACE,OAAO,KAAK,QAAQ,CAAC,CAClB,QAAQ,CAAC,CACT,iBAAiB,cAAc,EAAE;CACpC,IAAI,KAAK,IAAI,CAAC,CACX,QAAQ,CAAC,CACT,iBAAiB,MAAM,aAAa,CAAC;CACxC,WAAW,KAAK,aAAa,CAAC,CAAC,QAAQ;CACvC,UAAU,KAAK,WAAW;CAC1B,WAAW,KAAK,YAAY;CAC5B,QAAQ,KAAK,UAAU,EAAE,MAAM,CAAC,UAAU,SAAS,EAAE,CAAC,CAAC,CACpD,QAAQ,CAAC,CACT,QAAQ,QAAQ;CAEnB,QAAQ,KAAK,QAAQ,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,QAAQ;CACjD,WAAW,UAAU,cAAc,EAAE,cAAc,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,WAAW;CAChF,WAAW,UAAU,cAAc,EAAE,cAAc,KAAK,CAAC;CACzD,WAAW,UAAU,cAAc,EAAE,cAAc,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,WAAW;CAChF,WAAW,UAAU,cAAc,EAAE,cAAc,KAAK,CAAC,CAAC,CACvD,QAAQ,CAAC,CACT,WAAW,CAAC,CACZ,gCAAgB,IAAI,KAAK,CAAC;AAC/B,IACC,OAAO;CACN,IAAI,WAAW,EAAE,SAAS,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;CAE3C,WAAW,WAAW;EACpB,SAAS,CAAC,EAAE,OAAO,EAAE,SAAS;EAC9B,gBAAgB,CAAC,aAAa,OAAO,aAAa,EAAE;CACtD,CAAC,CAAC,CAAC,SAAS,SAAS;CAErB,UAAU,WAAW;EACnB,SAAS,CAAC,EAAE,OAAO,EAAE,QAAQ;EAC7B,gBAAgB,CAAC,QAAQ,OAAO,QAAQ,EAAE;CAC5C,CAAC,CAAC,CAAC,SAAS,SAAS;CAGrB,WAAW,WAAW;EACpB,SAAS,CAAC,EAAE,OAAO,EAAE,SAAS;EAC9B,gBAAgB,CAAC,SAAS,OAAO,SAAS,EAAE;CAC9C,CAAC,CAAC,CAAC,SAAS,SAAS;CAIrB,kBAAkB,OAAO,CAAC,CAAC,GAAG,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS;CAC/D,iBAAiB,OAAO,CAAC,CAAC,GAAG,EAAE,OAAO,EAAE,WAAW,EAAE,QAAQ;CAE7D,UAAU,MACR,6BACA,GAAG,gBAAgB,EAAE,SAAS,IAAI,EAAE,UAAU,MAChD;AACF,EACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"identity.js","names":[],"sources":["../../src/schema/identity.ts"],"sourcesContent":["// identity tables — the person.\n//\n// One row per human, global, storing auth ID in `external_id`.\nimport { pgTable, text, timestamp } from 'drizzle-orm/pg-core';\nimport type { User } from '@headless-lms/core/schemas';\nimport { genId } from '@headless-lms/core/shared/id';\nimport type { Expect, NoDrift } from './drift.js';\n\nexport const users = pgTable('users', {\n id: text('id')\n .primaryKey()\n .$defaultFn(() => genId('user')),\n // auth engine's ID - e.g. better-auth.\n externalId: text('external_id').unique(),\n email: text('email').notNull().unique(),\n firstName: text('first_name'),\n lastName: text('last_name'),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp('updated_at', { withTimezone: true })\n .notNull()\n .defaultNow()\n .$onUpdate(() => new Date()),\n});\n\ntype _UsersDrift = Expect<NoDrift<typeof users.$inferSelect, User>>;\n"],"mappings":";;;AAQA,MAAa,QAAQ,QAAQ,SAAS;CACpC,IAAI,KAAK,IAAI,CAAC,CACX,WAAW,CAAC,CACZ,iBAAiB,MAAM,MAAM,CAAC;CAEjC,YAAY,KAAK,aAAa,CAAC,CAAC,OAAO;CACvC,OAAO,KAAK,OAAO,CAAC,CAAC,QAAQ,CAAC,CAAC,OAAO;CACtC,WAAW,KAAK,YAAY;CAC5B,UAAU,KAAK,WAAW;CAC1B,WAAW,UAAU,cAAc,EAAE,cAAc,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,WAAW;CAChF,WAAW,UAAU,cAAc,EAAE,cAAc,KAAK,CAAC,CAAC,CACvD,QAAQ,CAAC,CACT,WAAW,CAAC,CACZ,gCAAgB,IAAI,KAAK,CAAC;AAC/B,CAAC"}
1
+ {"version":3,"file":"identity.js","names":[],"sources":["../../src/schema/identity.ts"],"sourcesContent":["// identity tables — the person.\n//\n// One row per human, global, storing auth ID in `external_id`.\nimport { pgTable, text, timestamp } from \"drizzle-orm/pg-core\";\nimport type { User } from \"@headless-lms/core/schemas\";\nimport { genId } from \"@headless-lms/core/shared/id\";\nimport type { Expect, NoDrift } from \"./drift.js\";\n\nexport const users = pgTable(\"users\", {\n id: text(\"id\")\n .primaryKey()\n .$defaultFn(() => genId(\"user\")),\n // auth engine's ID - e.g. better-auth.\n externalId: text(\"external_id\").unique(),\n email: text(\"email\").notNull().unique(),\n firstName: text(\"first_name\"),\n lastName: text(\"last_name\"),\n createdAt: timestamp(\"created_at\", { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true })\n .notNull()\n .defaultNow()\n .$onUpdate(() => new Date()),\n});\n\ntype _UsersDrift = Expect<NoDrift<typeof users.$inferSelect, User>>;\n"],"mappings":";;;AAQA,MAAa,QAAQ,QAAQ,SAAS;CACpC,IAAI,KAAK,IAAI,CAAC,CACX,WAAW,CAAC,CACZ,iBAAiB,MAAM,MAAM,CAAC;CAEjC,YAAY,KAAK,aAAa,CAAC,CAAC,OAAO;CACvC,OAAO,KAAK,OAAO,CAAC,CAAC,QAAQ,CAAC,CAAC,OAAO;CACtC,WAAW,KAAK,YAAY;CAC5B,UAAU,KAAK,WAAW;CAC1B,WAAW,UAAU,cAAc,EAAE,cAAc,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,WAAW;CAChF,WAAW,UAAU,cAAc,EAAE,cAAc,KAAK,CAAC,CAAC,CACvD,QAAQ,CAAC,CACT,WAAW,CAAC,CACZ,gCAAgB,IAAI,KAAK,CAAC;AAC/B,CAAC"}
@@ -1,5 +1,5 @@
1
1
  import { invites, orgUsers, organizations } from "./organizations.js";
2
- import { activities, activityAssets, contentItems, courses, downloadAssets, downloads, modules } from "./content.js";
2
+ import { activities, activityAssets, bundleItems, bundles, contentItems, courses, downloadAssets, downloads, modules } from "./content.js";
3
3
  import { entitlements } from "./entitlements.js";
4
4
  import { progressRecords } from "./progress.js";
5
5
  import { users } from "./identity.js";
@@ -11,8 +11,8 @@ import { automationRuns, automations } from "./automations.js";
11
11
  import { settings } from "./settings.js";
12
12
  import { commentReactions, commentReports, comments } from "./discussion.js";
13
13
  declare namespace index_d_exports {
14
- export { activities, activityAssets, assets, automationRuns, automations, commentReactions, commentReports, comments, connections, contentItems, courses, credentials, downloadAssets, downloads, entitlements, eventOutbox, invites, modules, orgUsers, organizations, progressRecords, settings, users };
14
+ export { activities, activityAssets, assets, automationRuns, automations, bundleItems, bundles, commentReactions, commentReports, comments, connections, contentItems, courses, credentials, downloadAssets, downloads, entitlements, eventOutbox, invites, modules, orgUsers, organizations, progressRecords, settings, users };
15
15
  }
16
16
  //#endregion
17
- export { activities, activityAssets, assets, automationRuns, automations, commentReactions, commentReports, comments, connections, contentItems, courses, credentials, downloadAssets, downloads, entitlements, eventOutbox, index_d_exports, invites, modules, orgUsers, organizations, progressRecords, settings, users };
17
+ export { activities, activityAssets, assets, automationRuns, automations, bundleItems, bundles, commentReactions, commentReports, comments, connections, contentItems, courses, credentials, downloadAssets, downloads, entitlements, eventOutbox, index_d_exports, invites, modules, orgUsers, organizations, progressRecords, settings, users };
18
18
  //# sourceMappingURL=index.d.ts.map
@@ -2,7 +2,7 @@ import { __exportAll } from "../_virtual/_rolldown/runtime.js";
2
2
  import { users } from "./identity.js";
3
3
  import { invites, orgUsers, organizations } from "./organizations.js";
4
4
  import { assets } from "./assets.js";
5
- import { activities, activityAssets, contentItems, courses, downloadAssets, downloads, modules } from "./content.js";
5
+ import { activities, activityAssets, bundleItems, bundles, contentItems, courses, downloadAssets, downloads, modules } from "./content.js";
6
6
  import { entitlements } from "./entitlements.js";
7
7
  import { progressRecords } from "./progress.js";
8
8
  import { credentials } from "./credentials.js";
@@ -18,6 +18,8 @@ var schema_exports = /* @__PURE__ */ __exportAll({
18
18
  assets: () => assets,
19
19
  automationRuns: () => automationRuns,
20
20
  automations: () => automations,
21
+ bundleItems: () => bundleItems,
22
+ bundles: () => bundles,
21
23
  commentReactions: () => commentReactions,
22
24
  commentReports: () => commentReports,
23
25
  comments: () => comments,
@@ -38,6 +40,6 @@ var schema_exports = /* @__PURE__ */ __exportAll({
38
40
  users: () => users
39
41
  });
40
42
  //#endregion
41
- export { activities, activityAssets, assets, automationRuns, automations, commentReactions, commentReports, comments, connections, contentItems, courses, credentials, downloadAssets, downloads, entitlements, eventOutbox, invites, modules, orgUsers, organizations, progressRecords, schema_exports, settings, users };
43
+ export { activities, activityAssets, assets, automationRuns, automations, bundleItems, bundles, commentReactions, commentReports, comments, connections, contentItems, courses, credentials, downloadAssets, downloads, entitlements, eventOutbox, invites, modules, orgUsers, organizations, progressRecords, schema_exports, settings, users };
42
44
 
43
45
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/schema/index.ts"],"sourcesContent":["// Centralized Drizzle schema. All domain tables live here, grouped by the\n// bounded context that owns them. Re-exported so drizzle-kit and the\n// repositories have a single import surface.\nexport * from './organizations.js';\nexport * from './content.js';\nexport * from './entitlements.js';\nexport * from './progress.js';\nexport * from './identity.js';\nexport * from './assets.js';\nexport * from './credentials.js';\nexport * from './integrations.js';\nexport * from './outbox.js';\nexport * from './automations.js';\nexport * from './settings.js';\nexport * from './discussion.js';\n"],"mappings":""}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/schema/index.ts"],"sourcesContent":["// Centralized Drizzle schema. All domain tables live here, grouped by the\n// bounded context that owns them. Re-exported so drizzle-kit and the\n// repositories have a single import surface.\nexport * from \"./organizations.js\";\nexport * from \"./content.js\";\nexport * from \"./entitlements.js\";\nexport * from \"./progress.js\";\nexport * from \"./identity.js\";\nexport * from \"./assets.js\";\nexport * from \"./credentials.js\";\nexport * from \"./integrations.js\";\nexport * from \"./outbox.js\";\nexport * from \"./automations.js\";\nexport * from \"./settings.js\";\nexport * from \"./discussion.js\";\n"],"mappings":""}
@@ -10,7 +10,7 @@ const connections = pgTable("connections", {
10
10
  active: boolean("active").notNull().default(true),
11
11
  credentialRef: text("credential_ref").notNull(),
12
12
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
13
- updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
13
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow().$onUpdate(() => /* @__PURE__ */ new Date())
14
14
  }, (t) => ({
15
15
  pk: primaryKey({ columns: [t.orgId, t.id] }),
16
16
  onePerIntegration: unique().on(t.orgId, t.integrationId),
@@ -1 +1 @@
1
- {"version":3,"file":"integrations.js","names":[],"sources":["../../src/schema/integrations.ts"],"sourcesContent":["// integrations tables — an org's connections to external services. One\n// connection per integration per org; the secret itself lives in the\n// credentials table (secure credential store), referenced by credential_ref.\nimport {\n pgTable,\n text,\n boolean,\n jsonb,\n timestamp,\n primaryKey,\n foreignKey,\n unique,\n} from 'drizzle-orm/pg-core';\nimport type { Connection } from '@headless-lms/core/schemas';\nimport { organizations } from './organizations.js';\nimport { credentials } from './credentials.js';\nimport type { Expect, NoDrift } from './drift.js';\n\nexport const connections = pgTable(\n 'connections',\n {\n orgId: text('org_id')\n .notNull()\n .references(() => organizations.id),\n id: text('id').notNull(),\n integrationId: text('integration_id').notNull(),\n config: jsonb('config').$type<Connection['config']>().notNull().default({}),\n active: boolean('active').notNull().default(true),\n credentialRef: text('credential_ref').notNull(),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),\n },\n (t) => ({\n pk: primaryKey({ columns: [t.orgId, t.id] }),\n onePerIntegration: unique().on(t.orgId, t.integrationId),\n credentialFk: foreignKey({\n columns: [t.orgId, t.credentialRef],\n foreignColumns: [credentials.orgId, credentials.id],\n }),\n }),\n);\n\ntype _ConnectionsDrift = Expect<NoDrift<typeof connections.$inferSelect, Connection>>;\n"],"mappings":";;;;AAkBA,MAAa,cAAc,QACzB,eACA;CACE,OAAO,KAAK,QAAQ,CAAC,CAClB,QAAQ,CAAC,CACT,iBAAiB,cAAc,EAAE;CACpC,IAAI,KAAK,IAAI,CAAC,CAAC,QAAQ;CACvB,eAAe,KAAK,gBAAgB,CAAC,CAAC,QAAQ;CAC9C,QAAQ,MAAM,QAAQ,CAAC,CAAC,MAA4B,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC,CAAC;CAC1E,QAAQ,QAAQ,QAAQ,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,IAAI;CAChD,eAAe,KAAK,gBAAgB,CAAC,CAAC,QAAQ;CAC9C,WAAW,UAAU,cAAc,EAAE,cAAc,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,WAAW;CAChF,WAAW,UAAU,cAAc,EAAE,cAAc,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,WAAW;AAClF,IACC,OAAO;CACN,IAAI,WAAW,EAAE,SAAS,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;CAC3C,mBAAmB,OAAO,CAAC,CAAC,GAAG,EAAE,OAAO,EAAE,aAAa;CACvD,cAAc,WAAW;EACvB,SAAS,CAAC,EAAE,OAAO,EAAE,aAAa;EAClC,gBAAgB,CAAC,YAAY,OAAO,YAAY,EAAE;CACpD,CAAC;AACH,EACF"}
1
+ {"version":3,"file":"integrations.js","names":[],"sources":["../../src/schema/integrations.ts"],"sourcesContent":["// integrations tables — an org's connections to external services. One\n// connection per integration per org; the secret itself lives in the\n// credentials table (secure credential store), referenced by credential_ref.\nimport {\n pgTable,\n text,\n boolean,\n jsonb,\n timestamp,\n primaryKey,\n foreignKey,\n unique,\n} from \"drizzle-orm/pg-core\";\nimport type { Connection } from \"@headless-lms/core/schemas\";\nimport { organizations } from \"./organizations.js\";\nimport { credentials } from \"./credentials.js\";\nimport type { Expect, NoDrift } from \"./drift.js\";\n\nexport const connections = pgTable(\n \"connections\",\n {\n orgId: text(\"org_id\")\n .notNull()\n .references(() => organizations.id),\n id: text(\"id\").notNull(),\n integrationId: text(\"integration_id\").notNull(),\n config: jsonb(\"config\").$type<Connection[\"config\"]>().notNull().default({}),\n active: boolean(\"active\").notNull().default(true),\n credentialRef: text(\"credential_ref\").notNull(),\n createdAt: timestamp(\"created_at\", { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true })\n .notNull()\n .defaultNow()\n .$onUpdate(() => new Date()),\n },\n (t) => ({\n pk: primaryKey({ columns: [t.orgId, t.id] }),\n onePerIntegration: unique().on(t.orgId, t.integrationId),\n credentialFk: foreignKey({\n columns: [t.orgId, t.credentialRef],\n foreignColumns: [credentials.orgId, credentials.id],\n }),\n }),\n);\n\ntype _ConnectionsDrift = Expect<NoDrift<typeof connections.$inferSelect, Connection>>;\n"],"mappings":";;;;AAkBA,MAAa,cAAc,QACzB,eACA;CACE,OAAO,KAAK,QAAQ,CAAC,CAClB,QAAQ,CAAC,CACT,iBAAiB,cAAc,EAAE;CACpC,IAAI,KAAK,IAAI,CAAC,CAAC,QAAQ;CACvB,eAAe,KAAK,gBAAgB,CAAC,CAAC,QAAQ;CAC9C,QAAQ,MAAM,QAAQ,CAAC,CAAC,MAA4B,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC,CAAC;CAC1E,QAAQ,QAAQ,QAAQ,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,IAAI;CAChD,eAAe,KAAK,gBAAgB,CAAC,CAAC,QAAQ;CAC9C,WAAW,UAAU,cAAc,EAAE,cAAc,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,WAAW;CAChF,WAAW,UAAU,cAAc,EAAE,cAAc,KAAK,CAAC,CAAC,CACvD,QAAQ,CAAC,CACT,WAAW,CAAC,CACZ,gCAAgB,IAAI,KAAK,CAAC;AAC/B,IACC,OAAO;CACN,IAAI,WAAW,EAAE,SAAS,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;CAC3C,mBAAmB,OAAO,CAAC,CAAC,GAAG,EAAE,OAAO,EAAE,aAAa;CACvD,cAAc,WAAW;EACvB,SAAS,CAAC,EAAE,OAAO,EAAE,aAAa;EAClC,gBAAgB,CAAC,YAAY,OAAO,YAAY,EAAE;CACpD,CAAC;AACH,EACF"}
@@ -408,6 +408,23 @@ declare const invites: import("drizzle-orm/pg-core").PgTableWithColumns<{
408
408
  identity: undefined;
409
409
  generated: undefined;
410
410
  }, {}, {}>;
411
+ updatedAt: import("drizzle-orm/pg-core").PgColumn<{
412
+ name: "updated_at";
413
+ tableName: "invites";
414
+ dataType: "date";
415
+ columnType: "PgTimestamp";
416
+ data: Date;
417
+ driverParam: string;
418
+ notNull: true;
419
+ hasDefault: true;
420
+ isPrimaryKey: false;
421
+ isAutoincrement: false;
422
+ hasRuntimeDefault: false;
423
+ enumValues: undefined;
424
+ baseColumn: never;
425
+ identity: undefined;
426
+ generated: undefined;
427
+ }, {}, {}>;
411
428
  };
412
429
  dialect: "pg";
413
430
  }>;
@@ -47,7 +47,8 @@ const invites = pgTable("invites", {
47
47
  invitedBy: text("invited_by").notNull().references(() => users.id),
48
48
  tokenHash: text("token_hash").unique(),
49
49
  expiresAt: timestamp("expires_at", { withTimezone: true }),
50
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
50
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
51
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow().$onUpdate(() => /* @__PURE__ */ new Date())
51
52
  }, (t) => ({
52
53
  pk: primaryKey({ columns: [t.orgId, t.id] }),
53
54
  pendingEmailUq: uniqueIndex("invites_pending_email_uq").on(t.orgId, t.email).where(sql`${t.status} = 'pending'`)
@@ -1 +1 @@
1
- {"version":3,"file":"organizations.js","names":[],"sources":["../../src/schema/organizations.ts"],"sourcesContent":["import { sql } from 'drizzle-orm';\nimport {\n pgTable,\n text,\n timestamp,\n primaryKey,\n unique,\n uniqueIndex,\n} from 'drizzle-orm/pg-core';\nimport type { Invite, Organization, OrgUser } from '@headless-lms/core/schemas';\nimport { genId } from '@headless-lms/core/shared/id';\nimport { users } from './identity.js';\nimport type { Expect, NoDrift } from './drift.js';\n\nexport const organizations = pgTable('organizations', {\n id: text('id')\n .primaryKey()\n .$defaultFn(() => genId('organization')),\n // Links to the better-auth organization record.\n externalId: text('external_id').unique(),\n name: text('name').notNull(),\n slug: text('slug').notNull().unique(),\n ownerId: text('owner_id')\n .notNull()\n .references(() => users.id),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp('updated_at', { withTimezone: true })\n .notNull()\n .defaultNow()\n .$onUpdate(() => new Date()),\n});\n\n// A person's link to an organization under one role\nexport const orgUsers = pgTable(\n 'org_users',\n {\n orgId: text('org_id')\n .notNull()\n .references(() => organizations.id),\n id: text('id')\n .notNull()\n .$defaultFn(() => genId('orgUser')),\n\n userId: text('user_id')\n .notNull()\n .references(() => users.id),\n role: text('role', { enum: ['owner', 'admin', 'instructor', 'student'] }).notNull(),\n // 'invited' from the moment an admin adds a student until they accept.\n // Staff rows are mirrored from an auth member record that only exists once\n // they have joined, so they are created 'active'.\n status: text('status', { enum: ['invited', 'active'] })\n .notNull()\n .default('active'),\n\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp('updated_at', { withTimezone: true })\n .notNull()\n .defaultNow()\n .$onUpdate(() => new Date()),\n },\n (t) => ({\n pk: primaryKey({ columns: [t.orgId, t.id] }),\n userUq: unique().on(t.orgId, t.userId),\n }),\n);\n\nexport const invites = pgTable(\n 'invites',\n {\n orgId: text('org_id')\n .notNull()\n .references(() => organizations.id),\n id: text('id')\n .notNull()\n .$defaultFn(() => genId('invite')),\n email: text('email').notNull(),\n role: text('role', { enum: ['admin', 'instructor', 'student'] }).notNull(),\n status: text('status', {\n enum: ['pending', 'accepted', 'rejected', 'canceled'],\n }).notNull(),\n invitedBy: text('invited_by')\n .notNull()\n .references(() => users.id),\n tokenHash: text('token_hash').unique(),\n expiresAt: timestamp('expires_at', { withTimezone: true }),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n },\n (t) => ({\n pk: primaryKey({ columns: [t.orgId, t.id] }),\n // One pending invite per (org, email) — the upsert's conflict target.\n pendingEmailUq: uniqueIndex('invites_pending_email_uq')\n .on(t.orgId, t.email)\n .where(sql`${t.status} = 'pending'`),\n }),\n);\n\ntype _OrganizationsDrift = Expect<NoDrift<typeof organizations.$inferSelect, Organization>>;\ntype _OrgUsersDrift = Expect<NoDrift<typeof orgUsers.$inferSelect, OrgUser>>;\ntype _InvitesDrift = Expect<NoDrift<typeof invites.$inferSelect, Invite>>;\n\n"],"mappings":";;;;;AAcA,MAAa,gBAAgB,QAAQ,iBAAiB;CACpD,IAAI,KAAK,IAAI,CAAC,CACX,WAAW,CAAC,CACZ,iBAAiB,MAAM,cAAc,CAAC;CAEzC,YAAY,KAAK,aAAa,CAAC,CAAC,OAAO;CACvC,MAAM,KAAK,MAAM,CAAC,CAAC,QAAQ;CAC3B,MAAM,KAAK,MAAM,CAAC,CAAC,QAAQ,CAAC,CAAC,OAAO;CACpC,SAAS,KAAK,UAAU,CAAC,CACtB,QAAQ,CAAC,CACT,iBAAiB,MAAM,EAAE;CAC5B,WAAW,UAAU,cAAc,EAAE,cAAc,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,WAAW;CAChF,WAAW,UAAU,cAAc,EAAE,cAAc,KAAK,CAAC,CAAC,CACvD,QAAQ,CAAC,CACT,WAAW,CAAC,CACZ,gCAAgB,IAAI,KAAK,CAAC;AAC/B,CAAC;AAGD,MAAa,WAAW,QACtB,aACA;CACE,OAAO,KAAK,QAAQ,CAAC,CAClB,QAAQ,CAAC,CACT,iBAAiB,cAAc,EAAE;CACpC,IAAI,KAAK,IAAI,CAAC,CACX,QAAQ,CAAC,CACT,iBAAiB,MAAM,SAAS,CAAC;CAEpC,QAAQ,KAAK,SAAS,CAAC,CACpB,QAAQ,CAAC,CACT,iBAAiB,MAAM,EAAE;CAC5B,MAAM,KAAK,QAAQ,EAAE,MAAM;EAAC;EAAS;EAAS;EAAc;CAAS,EAAE,CAAC,CAAC,CAAC,QAAQ;CAIlF,QAAQ,KAAK,UAAU,EAAE,MAAM,CAAC,WAAW,QAAQ,EAAE,CAAC,CAAC,CACpD,QAAQ,CAAC,CACT,QAAQ,QAAQ;CAEnB,WAAW,UAAU,cAAc,EAAE,cAAc,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,WAAW;CAChF,WAAW,UAAU,cAAc,EAAE,cAAc,KAAK,CAAC,CAAC,CACvD,QAAQ,CAAC,CACT,WAAW,CAAC,CACZ,gCAAgB,IAAI,KAAK,CAAC;AAC/B,IACC,OAAO;CACN,IAAI,WAAW,EAAE,SAAS,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;CAC3C,QAAQ,OAAO,CAAC,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM;AACvC,EACF;AAEA,MAAa,UAAU,QACrB,WACA;CACE,OAAO,KAAK,QAAQ,CAAC,CAClB,QAAQ,CAAC,CACT,iBAAiB,cAAc,EAAE;CACpC,IAAI,KAAK,IAAI,CAAC,CACX,QAAQ,CAAC,CACT,iBAAiB,MAAM,QAAQ,CAAC;CACnC,OAAO,KAAK,OAAO,CAAC,CAAC,QAAQ;CAC7B,MAAM,KAAK,QAAQ,EAAE,MAAM;EAAC;EAAS;EAAc;CAAS,EAAE,CAAC,CAAC,CAAC,QAAQ;CACzE,QAAQ,KAAK,UAAU,EACrB,MAAM;EAAC;EAAW;EAAY;EAAY;CAAU,EACtD,CAAC,CAAC,CAAC,QAAQ;CACX,WAAW,KAAK,YAAY,CAAC,CAC1B,QAAQ,CAAC,CACT,iBAAiB,MAAM,EAAE;CAC5B,WAAW,KAAK,YAAY,CAAC,CAAC,OAAO;CACrC,WAAW,UAAU,cAAc,EAAE,cAAc,KAAK,CAAC;CACzD,WAAW,UAAU,cAAc,EAAE,cAAc,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,WAAW;AAClF,IACC,OAAO;CACN,IAAI,WAAW,EAAE,SAAS,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;CAE3C,gBAAgB,YAAY,0BAA0B,CAAC,CACpD,GAAG,EAAE,OAAO,EAAE,KAAK,CAAC,CACpB,MAAM,GAAG,GAAG,EAAE,OAAO,aAAa;AACvC,EACF"}
1
+ {"version":3,"file":"organizations.js","names":[],"sources":["../../src/schema/organizations.ts"],"sourcesContent":["import { sql } from \"drizzle-orm\";\nimport { pgTable, text, timestamp, primaryKey, unique, uniqueIndex } from \"drizzle-orm/pg-core\";\nimport type { Invite, Organization, OrgUser } from \"@headless-lms/core/schemas\";\nimport { genId } from \"@headless-lms/core/shared/id\";\nimport { users } from \"./identity.js\";\nimport type { Expect, NoDrift } from \"./drift.js\";\n\nexport const organizations = pgTable(\"organizations\", {\n id: text(\"id\")\n .primaryKey()\n .$defaultFn(() => genId(\"organization\")),\n // Links to the better-auth organization record.\n externalId: text(\"external_id\").unique(),\n name: text(\"name\").notNull(),\n slug: text(\"slug\").notNull().unique(),\n ownerId: text(\"owner_id\")\n .notNull()\n .references(() => users.id),\n createdAt: timestamp(\"created_at\", { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true })\n .notNull()\n .defaultNow()\n .$onUpdate(() => new Date()),\n});\n\n// A person's link to an organization under one role\nexport const orgUsers = pgTable(\n \"org_users\",\n {\n orgId: text(\"org_id\")\n .notNull()\n .references(() => organizations.id),\n id: text(\"id\")\n .notNull()\n .$defaultFn(() => genId(\"orgUser\")),\n\n userId: text(\"user_id\")\n .notNull()\n .references(() => users.id),\n role: text(\"role\", { enum: [\"owner\", \"admin\", \"instructor\", \"student\"] }).notNull(),\n // 'invited' from the moment an admin adds a student until they accept.\n // Staff rows are mirrored from an auth member record that only exists once\n // they have joined, so they are created 'active'.\n status: text(\"status\", { enum: [\"invited\", \"active\"] })\n .notNull()\n .default(\"active\"),\n\n createdAt: timestamp(\"created_at\", { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true })\n .notNull()\n .defaultNow()\n .$onUpdate(() => new Date()),\n },\n (t) => ({\n pk: primaryKey({ columns: [t.orgId, t.id] }),\n userUq: unique().on(t.orgId, t.userId),\n }),\n);\n\nexport const invites = pgTable(\n \"invites\",\n {\n orgId: text(\"org_id\")\n .notNull()\n .references(() => organizations.id),\n id: text(\"id\")\n .notNull()\n .$defaultFn(() => genId(\"invite\")),\n email: text(\"email\").notNull(),\n role: text(\"role\", { enum: [\"admin\", \"instructor\", \"student\"] }).notNull(),\n status: text(\"status\", {\n enum: [\"pending\", \"accepted\", \"rejected\", \"canceled\"],\n }).notNull(),\n invitedBy: text(\"invited_by\")\n .notNull()\n .references(() => users.id),\n tokenHash: text(\"token_hash\").unique(),\n expiresAt: timestamp(\"expires_at\", { withTimezone: true }),\n createdAt: timestamp(\"created_at\", { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true })\n .notNull()\n .defaultNow()\n .$onUpdate(() => new Date()),\n },\n (t) => ({\n pk: primaryKey({ columns: [t.orgId, t.id] }),\n // One pending invite per (org, email) — the upsert's conflict target.\n pendingEmailUq: uniqueIndex(\"invites_pending_email_uq\")\n .on(t.orgId, t.email)\n .where(sql`${t.status} = 'pending'`),\n }),\n);\n\ntype _OrganizationsDrift = Expect<NoDrift<typeof organizations.$inferSelect, Organization>>;\ntype _OrgUsersDrift = Expect<NoDrift<typeof orgUsers.$inferSelect, OrgUser>>;\ntype _InvitesDrift = Expect<NoDrift<typeof invites.$inferSelect, Invite>>;\n"],"mappings":"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"}
@@ -141,6 +141,23 @@ declare const eventOutbox: import("drizzle-orm/pg-core").PgTableWithColumns<{
141
141
  identity: undefined;
142
142
  generated: undefined;
143
143
  }, {}, {}>;
144
+ updatedAt: import("drizzle-orm/pg-core").PgColumn<{
145
+ name: "updated_at";
146
+ tableName: "event_outbox";
147
+ dataType: "date";
148
+ columnType: "PgTimestamp";
149
+ data: Date;
150
+ driverParam: string;
151
+ notNull: true;
152
+ hasDefault: true;
153
+ isPrimaryKey: false;
154
+ isAutoincrement: false;
155
+ hasRuntimeDefault: false;
156
+ enumValues: undefined;
157
+ baseColumn: never;
158
+ identity: undefined;
159
+ generated: undefined;
160
+ }, {}, {}>;
144
161
  processedAt: import("drizzle-orm/pg-core").PgColumn<{
145
162
  name: "processed_at";
146
163
  tableName: "event_outbox";
@@ -11,6 +11,7 @@ const eventOutbox = pgTable("event_outbox", {
11
11
  nextAttemptAt: timestamp("next_attempt_at", { withTimezone: true }).notNull().defaultNow(),
12
12
  lastError: text("last_error"),
13
13
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
14
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow().$onUpdate(() => /* @__PURE__ */ new Date()),
14
15
  processedAt: timestamp("processed_at", { withTimezone: true })
15
16
  }, (t) => ({ pendingIdx: index("event_outbox_pending_idx").on(t.id).where(sql`${t.processedAt} is null`) }));
16
17
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"outbox.js","names":[],"sources":["../../src/schema/outbox.ts"],"sourcesContent":["import { index, integer, jsonb, pgTable, text, timestamp } from 'drizzle-orm/pg-core';\nimport { sql } from 'drizzle-orm';\nimport type { EventOutbox } from '@headless-lms/core/schemas';\nimport { genId } from '@headless-lms/core/shared/id';\nimport type { Expect, NoDrift } from './drift.js';\n\nexport const eventOutbox = pgTable(\n 'event_outbox',\n {\n orgId: text('org_id').notNull(),\n id: text('id')\n .primaryKey()\n .$defaultFn(() => genId('event')),\n type: text('type').notNull(),\n payload: jsonb('payload').$type<EventOutbox['payload']>().notNull(),\n attempts: integer('attempts').notNull().default(0),\n nextAttemptAt: timestamp('next_attempt_at', { withTimezone: true }).notNull().defaultNow(),\n lastError: text('last_error'),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n processedAt: timestamp('processed_at', { withTimezone: true }),\n },\n (t) => ({\n pendingIdx: index('event_outbox_pending_idx')\n .on(t.id)\n .where(sql`${t.processedAt} is null`),\n }),\n);\n\ntype _EventOutboxDrift = Expect<NoDrift<typeof eventOutbox.$inferSelect, EventOutbox>>;\n"],"mappings":";;;;AAMA,MAAa,cAAc,QACzB,gBACA;CACE,OAAO,KAAK,QAAQ,CAAC,CAAC,QAAQ;CAC9B,IAAI,KAAK,IAAI,CAAC,CACX,WAAW,CAAC,CACZ,iBAAiB,MAAM,OAAO,CAAC;CAClC,MAAM,KAAK,MAAM,CAAC,CAAC,QAAQ;CAC3B,SAAS,MAAM,SAAS,CAAC,CAAC,MAA8B,CAAC,CAAC,QAAQ;CAClE,UAAU,QAAQ,UAAU,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC;CACjD,eAAe,UAAU,mBAAmB,EAAE,cAAc,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,WAAW;CACzF,WAAW,KAAK,YAAY;CAC5B,WAAW,UAAU,cAAc,EAAE,cAAc,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,WAAW;CAChF,aAAa,UAAU,gBAAgB,EAAE,cAAc,KAAK,CAAC;AAC/D,IACC,OAAO,EACN,YAAY,MAAM,0BAA0B,CAAC,CAC1C,GAAG,EAAE,EAAE,CAAC,CACR,MAAM,GAAG,GAAG,EAAE,YAAY,SAAS,EACxC,EACF"}
1
+ {"version":3,"file":"outbox.js","names":[],"sources":["../../src/schema/outbox.ts"],"sourcesContent":["import { index, integer, jsonb, pgTable, text, timestamp } from \"drizzle-orm/pg-core\";\nimport { sql } from \"drizzle-orm\";\nimport type { EventOutbox } from \"@headless-lms/core/schemas\";\nimport { genId } from \"@headless-lms/core/shared/id\";\nimport type { Expect, NoDrift } from \"./drift.js\";\n\nexport const eventOutbox = pgTable(\n \"event_outbox\",\n {\n orgId: text(\"org_id\").notNull(),\n id: text(\"id\")\n .primaryKey()\n .$defaultFn(() => genId(\"event\")),\n type: text(\"type\").notNull(),\n payload: jsonb(\"payload\").$type<EventOutbox[\"payload\"]>().notNull(),\n attempts: integer(\"attempts\").notNull().default(0),\n nextAttemptAt: timestamp(\"next_attempt_at\", { withTimezone: true }).notNull().defaultNow(),\n lastError: text(\"last_error\"),\n createdAt: timestamp(\"created_at\", { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true })\n .notNull()\n .defaultNow()\n .$onUpdate(() => new Date()),\n processedAt: timestamp(\"processed_at\", { withTimezone: true }),\n },\n (t) => ({\n pendingIdx: index(\"event_outbox_pending_idx\")\n .on(t.id)\n .where(sql`${t.processedAt} is null`),\n }),\n);\n\ntype _EventOutboxDrift = Expect<NoDrift<typeof eventOutbox.$inferSelect, EventOutbox>>;\n"],"mappings":";;;;AAMA,MAAa,cAAc,QACzB,gBACA;CACE,OAAO,KAAK,QAAQ,CAAC,CAAC,QAAQ;CAC9B,IAAI,KAAK,IAAI,CAAC,CACX,WAAW,CAAC,CACZ,iBAAiB,MAAM,OAAO,CAAC;CAClC,MAAM,KAAK,MAAM,CAAC,CAAC,QAAQ;CAC3B,SAAS,MAAM,SAAS,CAAC,CAAC,MAA8B,CAAC,CAAC,QAAQ;CAClE,UAAU,QAAQ,UAAU,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC;CACjD,eAAe,UAAU,mBAAmB,EAAE,cAAc,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,WAAW;CACzF,WAAW,KAAK,YAAY;CAC5B,WAAW,UAAU,cAAc,EAAE,cAAc,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,WAAW;CAChF,WAAW,UAAU,cAAc,EAAE,cAAc,KAAK,CAAC,CAAC,CACvD,QAAQ,CAAC,CACT,WAAW,CAAC,CACZ,gCAAgB,IAAI,KAAK,CAAC;CAC7B,aAAa,UAAU,gBAAgB,EAAE,cAAc,KAAK,CAAC;AAC/D,IACC,OAAO,EACN,YAAY,MAAM,0BAA0B,CAAC,CAC1C,GAAG,EAAE,EAAE,CAAC,CACR,MAAM,GAAG,GAAG,EAAE,YAAY,SAAS,EACxC,EACF"}
@@ -141,6 +141,23 @@ declare const progressRecords: import("drizzle-orm/pg-core").PgTableWithColumns<
141
141
  identity: undefined;
142
142
  generated: undefined;
143
143
  }, {}, {}>;
144
+ createdAt: import("drizzle-orm/pg-core").PgColumn<{
145
+ name: "created_at";
146
+ tableName: "progress_records";
147
+ dataType: "date";
148
+ columnType: "PgTimestamp";
149
+ data: Date;
150
+ driverParam: string;
151
+ notNull: true;
152
+ hasDefault: true;
153
+ isPrimaryKey: false;
154
+ isAutoincrement: false;
155
+ hasRuntimeDefault: false;
156
+ enumValues: undefined;
157
+ baseColumn: never;
158
+ identity: undefined;
159
+ generated: undefined;
160
+ }, {}, {}>;
144
161
  updatedAt: import("drizzle-orm/pg-core").PgColumn<{
145
162
  name: "updated_at";
146
163
  tableName: "progress_records";
@@ -15,6 +15,7 @@ const progressRecords = pgTable("progress_records", {
15
15
  startedAt: timestamp("started_at", { withTimezone: true }).notNull().defaultNow(),
16
16
  position: jsonb("position").$type(),
17
17
  completedAt: timestamp("completed_at", { withTimezone: true }),
18
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
18
19
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow().$onUpdate(() => /* @__PURE__ */ new Date())
19
20
  }, (t) => ({
20
21
  pk: primaryKey({ columns: [t.orgId, t.id] }),
@@ -1 +1 @@
1
- {"version":3,"file":"progress.js","names":[],"sources":["../../src/schema/progress.ts"],"sourcesContent":["// progress tables — one progress record per org user per target (activity,\n// module, or course). Tracks lifecycle: started_at on open, position\n// (typed resume payload) as the player reports it, completed_at when the rule is\n// satisfied (null = in progress). Target is denormalized (type + id, no FK) so a\n// record survives structure edits. Percentage and resume are derived on read;\n// nothing here is a stored percentage.\nimport {\n pgTable,\n text,\n jsonb,\n timestamp,\n primaryKey,\n unique,\n foreignKey,\n} from 'drizzle-orm/pg-core';\nimport type { ProgressRecord } from '@headless-lms/core/schemas';\nimport { genId } from '@headless-lms/core/shared/id';\nimport { organizations, orgUsers } from './organizations.js';\nimport type { Expect, NoDrift } from './drift.js';\n\nexport const progressRecords = pgTable(\n 'progress_records',\n {\n orgId: text('org_id')\n .notNull()\n .references(() => organizations.id),\n id: text('id')\n .notNull()\n .$defaultFn(() => genId('progress')),\n orgUserId: text('org_user_id').notNull(),\n targetType: text('target_type', {\n enum: ['activity', 'module', 'course'],\n }).notNull(),\n targetId: text('target_id').notNull(),\n startedAt: timestamp('started_at', { withTimezone: true }).notNull().defaultNow(),\n position: jsonb('position').$type<ProgressRecord['position']>(), // opaque typed payload; service interprets per target type\n completedAt: timestamp('completed_at', { withTimezone: true }), // null = in progress\n updatedAt: timestamp('updated_at', { withTimezone: true })\n .notNull()\n .defaultNow()\n .$onUpdate(() => new Date()),\n },\n (t) => ({\n pk: primaryKey({ columns: [t.orgId, t.id] }),\n targetUq: unique().on(t.orgId, t.orgUserId, t.targetType, t.targetId),\n // The one org user reference the schema previously left unenforced.\n orgUserFk: foreignKey({\n columns: [t.orgId, t.orgUserId],\n foreignColumns: [orgUsers.orgId, orgUsers.id],\n }),\n }),\n);\n\ntype _ProgressRecordsDrift = Expect<NoDrift<typeof progressRecords.$inferSelect, ProgressRecord>>;\n"],"mappings":";;;;AAoBA,MAAa,kBAAkB,QAC7B,oBACA;CACE,OAAO,KAAK,QAAQ,CAAC,CAClB,QAAQ,CAAC,CACT,iBAAiB,cAAc,EAAE;CACpC,IAAI,KAAK,IAAI,CAAC,CACX,QAAQ,CAAC,CACT,iBAAiB,MAAM,UAAU,CAAC;CACrC,WAAW,KAAK,aAAa,CAAC,CAAC,QAAQ;CACvC,YAAY,KAAK,eAAe,EAC9B,MAAM;EAAC;EAAY;EAAU;CAAQ,EACvC,CAAC,CAAC,CAAC,QAAQ;CACX,UAAU,KAAK,WAAW,CAAC,CAAC,QAAQ;CACpC,WAAW,UAAU,cAAc,EAAE,cAAc,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,WAAW;CAChF,UAAU,MAAM,UAAU,CAAC,CAAC,MAAkC;CAC9D,aAAa,UAAU,gBAAgB,EAAE,cAAc,KAAK,CAAC;CAC7D,WAAW,UAAU,cAAc,EAAE,cAAc,KAAK,CAAC,CAAC,CACvD,QAAQ,CAAC,CACT,WAAW,CAAC,CACZ,gCAAgB,IAAI,KAAK,CAAC;AAC/B,IACC,OAAO;CACN,IAAI,WAAW,EAAE,SAAS,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;CAC3C,UAAU,OAAO,CAAC,CAAC,GAAG,EAAE,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,QAAQ;CAEpE,WAAW,WAAW;EACpB,SAAS,CAAC,EAAE,OAAO,EAAE,SAAS;EAC9B,gBAAgB,CAAC,SAAS,OAAO,SAAS,EAAE;CAC9C,CAAC;AACH,EACF"}
1
+ {"version":3,"file":"progress.js","names":[],"sources":["../../src/schema/progress.ts"],"sourcesContent":["// progress tables — one progress record per org user per target (activity,\n// module, or course). Tracks lifecycle: started_at on open, position\n// (typed resume payload) as the player reports it, completed_at when the rule is\n// satisfied (null = in progress). Target is denormalized (type + id, no FK) so a\n// record survives structure edits. Percentage and resume are derived on read;\n// nothing here is a stored percentage.\nimport {\n pgTable,\n text,\n jsonb,\n timestamp,\n primaryKey,\n unique,\n foreignKey,\n} from \"drizzle-orm/pg-core\";\nimport type { ProgressRecord } from \"@headless-lms/core/schemas\";\nimport { genId } from \"@headless-lms/core/shared/id\";\nimport { organizations, orgUsers } from \"./organizations.js\";\nimport type { Expect, NoDrift } from \"./drift.js\";\n\nexport const progressRecords = pgTable(\n \"progress_records\",\n {\n orgId: text(\"org_id\")\n .notNull()\n .references(() => organizations.id),\n id: text(\"id\")\n .notNull()\n .$defaultFn(() => genId(\"progress\")),\n orgUserId: text(\"org_user_id\").notNull(),\n targetType: text(\"target_type\", {\n enum: [\"activity\", \"module\", \"course\"],\n }).notNull(),\n targetId: text(\"target_id\").notNull(),\n startedAt: timestamp(\"started_at\", { withTimezone: true }).notNull().defaultNow(),\n position: jsonb(\"position\").$type<ProgressRecord[\"position\"]>(), // opaque typed payload; service interprets per target type\n completedAt: timestamp(\"completed_at\", { withTimezone: true }), // null = in progress\n createdAt: timestamp(\"created_at\", { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true })\n .notNull()\n .defaultNow()\n .$onUpdate(() => new Date()),\n },\n (t) => ({\n pk: primaryKey({ columns: [t.orgId, t.id] }),\n targetUq: unique().on(t.orgId, t.orgUserId, t.targetType, t.targetId),\n // The one org user reference the schema previously left unenforced.\n orgUserFk: foreignKey({\n columns: [t.orgId, t.orgUserId],\n foreignColumns: [orgUsers.orgId, orgUsers.id],\n }),\n }),\n);\n\ntype _ProgressRecordsDrift = Expect<NoDrift<typeof progressRecords.$inferSelect, ProgressRecord>>;\n"],"mappings":";;;;AAoBA,MAAa,kBAAkB,QAC7B,oBACA;CACE,OAAO,KAAK,QAAQ,CAAC,CAClB,QAAQ,CAAC,CACT,iBAAiB,cAAc,EAAE;CACpC,IAAI,KAAK,IAAI,CAAC,CACX,QAAQ,CAAC,CACT,iBAAiB,MAAM,UAAU,CAAC;CACrC,WAAW,KAAK,aAAa,CAAC,CAAC,QAAQ;CACvC,YAAY,KAAK,eAAe,EAC9B,MAAM;EAAC;EAAY;EAAU;CAAQ,EACvC,CAAC,CAAC,CAAC,QAAQ;CACX,UAAU,KAAK,WAAW,CAAC,CAAC,QAAQ;CACpC,WAAW,UAAU,cAAc,EAAE,cAAc,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,WAAW;CAChF,UAAU,MAAM,UAAU,CAAC,CAAC,MAAkC;CAC9D,aAAa,UAAU,gBAAgB,EAAE,cAAc,KAAK,CAAC;CAC7D,WAAW,UAAU,cAAc,EAAE,cAAc,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,WAAW;CAChF,WAAW,UAAU,cAAc,EAAE,cAAc,KAAK,CAAC,CAAC,CACvD,QAAQ,CAAC,CACT,WAAW,CAAC,CACZ,gCAAgB,IAAI,KAAK,CAAC;AAC/B,IACC,OAAO;CACN,IAAI,WAAW,EAAE,SAAS,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;CAC3C,UAAU,OAAO,CAAC,CAAC,GAAG,EAAE,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,QAAQ;CAEpE,WAAW,WAAW;EACpB,SAAS,CAAC,EAAE,OAAO,EAAE,SAAS;EAC9B,gBAAgB,CAAC,SAAS,OAAO,SAAS,EAAE;CAC9C,CAAC;AACH,EACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"settings.js","names":[],"sources":["../../src/schema/settings.ts"],"sourcesContent":["// settings — cross-cutting configuration store. One row per\n// (org, namespace, scope): the namespace names the domain that owns and\n// interprets the values; the scope is the id the settings attach to (an org id\n// for org-wide defaults, otherwise any entity id — a course, an activity, …).\n//\n// Deliberately NOT bound to any domain: no FK to content or anywhere else, so\n// settings never learns what it is scoping. Rows orphaned by a deleted entity\n// are purged off that domain's *.deleted event, not by cascade.\nimport { pgTable, text, jsonb, timestamp, primaryKey } from 'drizzle-orm/pg-core';\nimport type { Setting } from '@headless-lms/core/schemas';\nimport { organizations } from './organizations.js';\nimport type { Expect, NoDrift } from './drift.js';\n\nexport const settings = pgTable(\n 'settings',\n {\n orgId: text('org_id')\n .notNull()\n .references(() => organizations.id),\n // The owning domain: 'content', 'discussion', … Values are validated and\n // interpreted by that domain, never here.\n namespace: text('namespace').notNull(),\n // Org-scoped rows carry the org id, so the column is never null and the\n // natural key can be the primary key.\n scopeId: text('scope_id').notNull(),\n value: jsonb('value').$type<Setting['value']>().notNull().default({}),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp('updated_at', { withTimezone: true })\n .notNull()\n .defaultNow()\n .$onUpdate(() => new Date()),\n },\n (t) => ({\n pk: primaryKey({ columns: [t.orgId, t.namespace, t.scopeId] }),\n }),\n);\n\ntype _SettingsDrift = Expect<NoDrift<typeof settings.$inferSelect, Setting>>;\n"],"mappings":";;;AAaA,MAAa,WAAW,QACtB,YACA;CACE,OAAO,KAAK,QAAQ,CAAC,CAClB,QAAQ,CAAC,CACT,iBAAiB,cAAc,EAAE;CAGpC,WAAW,KAAK,WAAW,CAAC,CAAC,QAAQ;CAGrC,SAAS,KAAK,UAAU,CAAC,CAAC,QAAQ;CAClC,OAAO,MAAM,OAAO,CAAC,CAAC,MAAwB,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC,CAAC;CACpE,WAAW,UAAU,cAAc,EAAE,cAAc,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,WAAW;CAChF,WAAW,UAAU,cAAc,EAAE,cAAc,KAAK,CAAC,CAAC,CACvD,QAAQ,CAAC,CACT,WAAW,CAAC,CACZ,gCAAgB,IAAI,KAAK,CAAC;AAC/B,IACC,OAAO,EACN,IAAI,WAAW,EAAE,SAAS;CAAC,EAAE;CAAO,EAAE;CAAW,EAAE;AAAO,EAAE,CAAC,EAC/D,EACF"}
1
+ {"version":3,"file":"settings.js","names":[],"sources":["../../src/schema/settings.ts"],"sourcesContent":["// settings — cross-cutting configuration store. One row per\n// (org, namespace, scope): the namespace names the domain that owns and\n// interprets the values; the scope is the id the settings attach to (an org id\n// for org-wide defaults, otherwise any entity id — a course, an activity, …).\n//\n// Deliberately NOT bound to any domain: no FK to content or anywhere else, so\n// settings never learns what it is scoping. Rows orphaned by a deleted entity\n// are purged off that domain's *.deleted event, not by cascade.\nimport { pgTable, text, jsonb, timestamp, primaryKey } from \"drizzle-orm/pg-core\";\nimport type { Setting } from \"@headless-lms/core/schemas\";\nimport { organizations } from \"./organizations.js\";\nimport type { Expect, NoDrift } from \"./drift.js\";\n\nexport const settings = pgTable(\n \"settings\",\n {\n orgId: text(\"org_id\")\n .notNull()\n .references(() => organizations.id),\n // The owning domain: 'content', 'discussion', … Values are validated and\n // interpreted by that domain, never here.\n namespace: text(\"namespace\").notNull(),\n // Org-scoped rows carry the org id, so the column is never null and the\n // natural key can be the primary key.\n scopeId: text(\"scope_id\").notNull(),\n value: jsonb(\"value\").$type<Setting[\"value\"]>().notNull().default({}),\n createdAt: timestamp(\"created_at\", { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true })\n .notNull()\n .defaultNow()\n .$onUpdate(() => new Date()),\n },\n (t) => ({\n pk: primaryKey({ columns: [t.orgId, t.namespace, t.scopeId] }),\n }),\n);\n\ntype _SettingsDrift = Expect<NoDrift<typeof settings.$inferSelect, Setting>>;\n"],"mappings":";;;AAaA,MAAa,WAAW,QACtB,YACA;CACE,OAAO,KAAK,QAAQ,CAAC,CAClB,QAAQ,CAAC,CACT,iBAAiB,cAAc,EAAE;CAGpC,WAAW,KAAK,WAAW,CAAC,CAAC,QAAQ;CAGrC,SAAS,KAAK,UAAU,CAAC,CAAC,QAAQ;CAClC,OAAO,MAAM,OAAO,CAAC,CAAC,MAAwB,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC,CAAC;CACpE,WAAW,UAAU,cAAc,EAAE,cAAc,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,WAAW;CAChF,WAAW,UAAU,cAAc,EAAE,cAAc,KAAK,CAAC,CAAC,CACvD,QAAQ,CAAC,CACT,WAAW,CAAC,CACZ,gCAAgB,IAAI,KAAK,CAAC;AAC/B,IACC,OAAO,EACN,IAAI,WAAW,EAAE,SAAS;CAAC,EAAE;CAAO,EAAE;CAAW,EAAE;AAAO,EAAE,CAAC,EAC/D,EACF"}
@@ -1,3 +1,4 @@
1
+ import { translateDbError } from "./repositories/pg-errors.js";
1
2
  //#region src/unit-of-work.ts
2
3
  var DrizzleUnitOfWork = class {
3
4
  db;
@@ -6,8 +7,12 @@ var DrizzleUnitOfWork = class {
6
7
  this.db = db;
7
8
  this.makeScope = makeScope;
8
9
  }
9
- run(fn) {
10
- return this.db.transaction((tx) => fn(this.makeScope(tx)));
10
+ async run(fn) {
11
+ try {
12
+ return await this.db.transaction((tx) => fn(this.makeScope(tx)));
13
+ } catch (err) {
14
+ throw translateDbError(err);
15
+ }
11
16
  }
12
17
  };
13
18
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"unit-of-work.js","names":[],"sources":["../src/unit-of-work.ts"],"sourcesContent":["import type { NodePgDatabase } from 'drizzle-orm/node-postgres';\nimport type { UnitOfWork } from '@headless-lms/core/shared/ports';\nimport type { Tx } from './client.js';\n\nexport class DrizzleUnitOfWork<Scope> implements UnitOfWork<Scope> {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly makeScope: (tx: Tx) => Scope,\n ) {}\n\n run<T>(fn: (scope: Scope) => Promise<T>): Promise<T> {\n return this.db.transaction((tx) => fn(this.makeScope(tx)));\n }\n}\n"],"mappings":";AAIA,IAAa,oBAAb,MAAmE;CAE9C;CACA;CAFnB,YACE,IACA,WACA;EAFiB,KAAA,KAAA;EACA,KAAA,YAAA;CAChB;CAEH,IAAO,IAA8C;EACnD,OAAO,KAAK,GAAG,aAAa,OAAO,GAAG,KAAK,UAAU,EAAE,CAAC,CAAC;CAC3D;AACF"}
1
+ {"version":3,"file":"unit-of-work.js","names":[],"sources":["../src/unit-of-work.ts"],"sourcesContent":["import type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport type { UnitOfWork } from \"@headless-lms/core/shared/ports\";\nimport type { Tx } from \"./client.js\";\nimport { translateDbError } from \"./repositories/pg-errors.js\";\n\nexport class DrizzleUnitOfWork<Scope> implements UnitOfWork<Scope> {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly makeScope: (tx: Tx) => Scope,\n ) {}\n\n async run<T>(fn: (scope: Scope) => Promise<T>): Promise<T> {\n try {\n return await this.db.transaction((tx) => fn(this.makeScope(tx)));\n } catch (err) {\n throw translateDbError(err);\n }\n }\n}\n"],"mappings":";;AAKA,IAAa,oBAAb,MAAmE;CAE9C;CACA;CAFnB,YACE,IACA,WACA;EAFiB,KAAA,KAAA;EACA,KAAA,YAAA;CAChB;CAEH,MAAM,IAAO,IAA8C;EACzD,IAAI;GACF,OAAO,MAAM,KAAK,GAAG,aAAa,OAAO,GAAG,KAAK,UAAU,EAAE,CAAC,CAAC;EACjE,SAAS,KAAK;GACZ,MAAM,iBAAiB,GAAG;EAC5B;CACF;AACF"}
@@ -79,7 +79,6 @@ CREATE TABLE "courses" (
79
79
  "status" text DEFAULT 'draft' NOT NULL,
80
80
  "category" text DEFAULT '' NOT NULL,
81
81
  "thumbnail_asset_id" text,
82
- "settings" jsonb DEFAULT '{}'::jsonb NOT NULL,
83
82
  "created_at" timestamp with time zone DEFAULT now() NOT NULL,
84
83
  "updated_at" timestamp with time zone DEFAULT now() NOT NULL,
85
84
  CONSTRAINT "courses_org_id_id_pk" PRIMARY KEY("org_id","id"),
@@ -0,0 +1,43 @@
1
+ CREATE TABLE "bundle_items" (
2
+ "org_id" text NOT NULL,
3
+ "bundle_id" text NOT NULL,
4
+ "content_id" text NOT NULL,
5
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
6
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL,
7
+ CONSTRAINT "bundle_items_org_id_bundle_id_content_id_pk" PRIMARY KEY("org_id","bundle_id","content_id")
8
+ );
9
+ --> statement-breakpoint
10
+ CREATE TABLE "bundles" (
11
+ "org_id" text NOT NULL,
12
+ "id" text NOT NULL,
13
+ "name" text NOT NULL,
14
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
15
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL,
16
+ CONSTRAINT "bundles_org_id_id_pk" PRIMARY KEY("org_id","id"),
17
+ CONSTRAINT "bundles_org_id_id_name_unique" UNIQUE("org_id","id","name")
18
+ );
19
+ --> statement-breakpoint
20
+ ALTER TABLE "entitlements" ALTER COLUMN "content_id" DROP NOT NULL;--> statement-breakpoint
21
+ ALTER TABLE "invites" ADD COLUMN "updated_at" timestamp with time zone DEFAULT now() NOT NULL;--> statement-breakpoint
22
+ ALTER TABLE "activity_assets" ADD COLUMN "created_at" timestamp with time zone DEFAULT now() NOT NULL;--> statement-breakpoint
23
+ ALTER TABLE "activity_assets" ADD COLUMN "updated_at" timestamp with time zone DEFAULT now() NOT NULL;--> statement-breakpoint
24
+ ALTER TABLE "content_items" ADD COLUMN "updated_at" timestamp with time zone DEFAULT now() NOT NULL;--> statement-breakpoint
25
+ ALTER TABLE "download_assets" ADD COLUMN "created_at" timestamp with time zone DEFAULT now() NOT NULL;--> statement-breakpoint
26
+ ALTER TABLE "download_assets" ADD COLUMN "updated_at" timestamp with time zone DEFAULT now() NOT NULL;--> statement-breakpoint
27
+ ALTER TABLE "entitlements" ADD COLUMN "bundle_id" text;--> statement-breakpoint
28
+ ALTER TABLE "entitlements" ADD COLUMN "created_at" timestamp with time zone DEFAULT now() NOT NULL;--> statement-breakpoint
29
+ ALTER TABLE "entitlements" ADD COLUMN "updated_at" timestamp with time zone DEFAULT now() NOT NULL;--> statement-breakpoint
30
+ ALTER TABLE "progress_records" ADD COLUMN "created_at" timestamp with time zone DEFAULT now() NOT NULL;--> statement-breakpoint
31
+ ALTER TABLE "assets" ADD COLUMN "updated_at" timestamp with time zone DEFAULT now() NOT NULL;--> statement-breakpoint
32
+ ALTER TABLE "event_outbox" ADD COLUMN "updated_at" timestamp with time zone DEFAULT now() NOT NULL;--> statement-breakpoint
33
+ ALTER TABLE "automation_runs" ADD COLUMN "created_at" timestamp with time zone DEFAULT now() NOT NULL;--> statement-breakpoint
34
+ ALTER TABLE "automation_runs" ADD COLUMN "updated_at" timestamp with time zone DEFAULT now() NOT NULL;--> statement-breakpoint
35
+ ALTER TABLE "comment_reactions" ADD COLUMN "updated_at" timestamp with time zone DEFAULT now() NOT NULL;--> statement-breakpoint
36
+ ALTER TABLE "comment_reports" ADD COLUMN "updated_at" timestamp with time zone DEFAULT now() NOT NULL;--> statement-breakpoint
37
+ ALTER TABLE "bundle_items" ADD CONSTRAINT "bundle_items_org_id_organizations_id_fk" FOREIGN KEY ("org_id") REFERENCES "public"."organizations"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
38
+ ALTER TABLE "bundle_items" ADD CONSTRAINT "bundle_items_org_id_content_id_content_items_org_id_id_fk" FOREIGN KEY ("org_id","content_id") REFERENCES "public"."content_items"("org_id","id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
39
+ ALTER TABLE "bundle_items" ADD CONSTRAINT "bundle_items_org_id_bundle_id_bundles_org_id_id_fk" FOREIGN KEY ("org_id","bundle_id") REFERENCES "public"."bundles"("org_id","id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
40
+ ALTER TABLE "bundles" ADD CONSTRAINT "bundles_org_id_organizations_id_fk" FOREIGN KEY ("org_id") REFERENCES "public"."organizations"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
41
+ ALTER TABLE "entitlements" ADD CONSTRAINT "entitlements_org_id_bundle_id_bundles_org_id_id_fk" FOREIGN KEY ("org_id","bundle_id") REFERENCES "public"."bundles"("org_id","id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
42
+ ALTER TABLE "entitlements" ADD CONSTRAINT "entitlements_org_id_org_user_id_bundle_id_unique" UNIQUE("org_id","org_user_id","bundle_id");--> statement-breakpoint
43
+ ALTER TABLE "entitlements" ADD CONSTRAINT "entitlements_target_check" CHECK (num_nonnulls("entitlements"."bundle_id", "entitlements"."content_id") = 1);
@@ -0,0 +1,7 @@
1
+ DROP INDEX "automations_org_trigger_idx";--> statement-breakpoint
2
+ ALTER TABLE "automations" ADD COLUMN "kind" text DEFAULT 'workflow' NOT NULL;--> statement-breakpoint
3
+ ALTER TABLE "automations" ADD COLUMN "triggers" text[];--> statement-breakpoint
4
+ UPDATE "automations" SET "triggers" = ARRAY["trigger"];--> statement-breakpoint
5
+ ALTER TABLE "automations" ALTER COLUMN "triggers" SET NOT NULL;--> statement-breakpoint
6
+ ALTER TABLE "automations" DROP COLUMN "trigger";--> statement-breakpoint
7
+ CREATE INDEX "automations_triggers_idx" ON "automations" USING gin ("triggers");