@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,4 +1,5 @@
1
1
  import { progressRecords } from "../schema/progress.js";
2
+ import { translateDbErrors } from "./pg-errors.js";
2
3
  import { and, eq, inArray } from "drizzle-orm";
3
4
  import { noopLogger } from "@headless-lms/core/shared/logger";
4
5
  //#region src/repositories/progress.ts
@@ -12,6 +13,7 @@ function toRecord(row) {
12
13
  startedAt: row.startedAt,
13
14
  position: row.position ?? null,
14
15
  completedAt: row.completedAt,
16
+ createdAt: row.createdAt,
15
17
  updatedAt: row.updatedAt
16
18
  };
17
19
  }
@@ -53,6 +55,7 @@ var DrizzleProgressRepository = class {
53
55
  return row ? toRecord(row) : null;
54
56
  }
55
57
  };
58
+ translateDbErrors(DrizzleProgressRepository);
56
59
  //#endregion
57
60
  export { DrizzleProgressRepository };
58
61
 
@@ -1 +1 @@
1
- {"version":3,"file":"progress.js","names":[],"sources":["../../src/repositories/progress.ts"],"sourcesContent":["// progress — Drizzle repository (implements the core outbound port).\nimport { and, eq, inArray } from 'drizzle-orm';\nimport type { NodePgDatabase } from 'drizzle-orm/node-postgres';\nimport type {\n ProgressRecord,\n ProgressRepository,\n ProgressTarget,\n ProgressTargetType,\n} from '@headless-lms/core/progress';\nimport { progressRecords } from '../schema/progress.js';\nimport type { Logger } from '@headless-lms/core/shared/ports';\nimport { noopLogger } from '@headless-lms/core/shared/logger';\n\ntype Row = typeof progressRecords.$inferSelect;\n\nfunction toRecord(row: Row): ProgressRecord {\n return {\n id: row.id,\n orgId: row.orgId,\n orgUserId: row.orgUserId,\n targetType: row.targetType as ProgressTargetType,\n targetId: row.targetId,\n startedAt: row.startedAt,\n position: row.position ?? null,\n completedAt: row.completedAt,\n updatedAt: row.updatedAt,\n };\n}\n\nexport class DrizzleProgressRepository implements ProgressRepository {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async insert(orgId: string, record: ProgressRecord): Promise<ProgressRecord | null> {\n const [row] = await this.db\n .insert(progressRecords)\n .values({\n id: record.id,\n orgId,\n orgUserId: record.orgUserId,\n targetType: record.targetType,\n targetId: record.targetId,\n startedAt: new Date(record.startedAt),\n position: record.position ?? null,\n completedAt: record.completedAt ? new Date(record.completedAt) : null,\n updatedAt: new Date(record.updatedAt),\n })\n .onConflictDoNothing()\n .returning();\n return row ? toRecord(row) : null;\n }\n\n async findByTarget(orgId: string, target: ProgressTarget): Promise<ProgressRecord | null> {\n const [row] = await this.db\n .select()\n .from(progressRecords)\n .where(\n and(\n eq(progressRecords.orgId, orgId),\n eq(progressRecords.orgUserId, target.orgUserId),\n eq(progressRecords.targetType, target.targetType),\n eq(progressRecords.targetId, target.targetId),\n ),\n )\n .limit(1);\n return row ? toRecord(row) : null;\n }\n\n async findByTargets(\n orgId: string,\n orgUserId: string,\n targetIds: string[],\n opts?: { forUpdate?: boolean },\n ): Promise<ProgressRecord[]> {\n if (targetIds.length === 0) {\n return [];\n }\n const base = this.db\n .select()\n .from(progressRecords)\n .where(\n and(\n eq(progressRecords.orgId, orgId),\n eq(progressRecords.orgUserId, orgUserId),\n inArray(progressRecords.targetId, targetIds),\n ),\n );\n // Deterministic lock order prevents deadlock between overlapping reports.\n const rows = opts?.forUpdate\n ? await base.orderBy(progressRecords.id).for('update')\n : await base;\n return rows.map(toRecord);\n }\n\n async update(\n orgId: string,\n id: string,\n patch: Partial<Pick<ProgressRecord, 'position' | 'completedAt'>>,\n ): Promise<ProgressRecord | null> {\n const [row] = await this.db\n .update(progressRecords)\n .set({\n ...('position' in patch ? { position: patch.position ?? null } : {}),\n ...('completedAt' in patch\n ? { completedAt: patch.completedAt ? new Date(patch.completedAt) : null }\n : {}),\n })\n .where(and(eq(progressRecords.orgId, orgId), eq(progressRecords.id, id)))\n .returning();\n return row ? toRecord(row) : null;\n }\n}\n"],"mappings":";;;;AAeA,SAAS,SAAS,KAA0B;CAC1C,OAAO;EACL,IAAI,IAAI;EACR,OAAO,IAAI;EACX,WAAW,IAAI;EACf,YAAY,IAAI;EAChB,UAAU,IAAI;EACd,WAAW,IAAI;EACf,UAAU,IAAI,YAAY;EAC1B,aAAa,IAAI;EACjB,WAAW,IAAI;CACjB;AACF;AAEA,IAAa,4BAAb,MAAqE;CAEhD;CACA;CAFnB,YACE,IACA,SAAkC,YAClC;EAFiB,KAAA,KAAA;EACA,KAAA,SAAA;CAChB;CAEH,MAAM,OAAO,OAAe,QAAwD;EAClF,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,eAAe,CAAC,CACvB,OAAO;GACN,IAAI,OAAO;GACX;GACA,WAAW,OAAO;GAClB,YAAY,OAAO;GACnB,UAAU,OAAO;GACjB,WAAW,IAAI,KAAK,OAAO,SAAS;GACpC,UAAU,OAAO,YAAY;GAC7B,aAAa,OAAO,cAAc,IAAI,KAAK,OAAO,WAAW,IAAI;GACjE,WAAW,IAAI,KAAK,OAAO,SAAS;EACtC,CAAC,CAAC,CACD,oBAAoB,CAAC,CACrB,UAAU;EACb,OAAO,MAAM,SAAS,GAAG,IAAI;CAC/B;CAEA,MAAM,aAAa,OAAe,QAAwD;EACxF,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,CAAC,CACR,KAAK,eAAe,CAAC,CACrB,MACC,IACE,GAAG,gBAAgB,OAAO,KAAK,GAC/B,GAAG,gBAAgB,WAAW,OAAO,SAAS,GAC9C,GAAG,gBAAgB,YAAY,OAAO,UAAU,GAChD,GAAG,gBAAgB,UAAU,OAAO,QAAQ,CAC9C,CACF,CAAC,CACA,MAAM,CAAC;EACV,OAAO,MAAM,SAAS,GAAG,IAAI;CAC/B;CAEA,MAAM,cACJ,OACA,WACA,WACA,MAC2B;EAC3B,IAAI,UAAU,WAAW,GACvB,OAAO,CAAC;EAEV,MAAM,OAAO,KAAK,GACf,OAAO,CAAC,CACR,KAAK,eAAe,CAAC,CACrB,MACC,IACE,GAAG,gBAAgB,OAAO,KAAK,GAC/B,GAAG,gBAAgB,WAAW,SAAS,GACvC,QAAQ,gBAAgB,UAAU,SAAS,CAC7C,CACF;EAKF,QAHa,MAAM,YACf,MAAM,KAAK,QAAQ,gBAAgB,EAAE,CAAC,CAAC,IAAI,QAAQ,IACnD,MAAM,KAAA,CACE,IAAI,QAAQ;CAC1B;CAEA,MAAM,OACJ,OACA,IACA,OACgC;EAChC,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,eAAe,CAAC,CACvB,IAAI;GACH,GAAI,cAAc,QAAQ,EAAE,UAAU,MAAM,YAAY,KAAK,IAAI,CAAC;GAClE,GAAI,iBAAiB,QACjB,EAAE,aAAa,MAAM,cAAc,IAAI,KAAK,MAAM,WAAW,IAAI,KAAK,IACtE,CAAC;EACP,CAAC,CAAC,CACD,MAAM,IAAI,GAAG,gBAAgB,OAAO,KAAK,GAAG,GAAG,gBAAgB,IAAI,EAAE,CAAC,CAAC,CAAC,CACxE,UAAU;EACb,OAAO,MAAM,SAAS,GAAG,IAAI;CAC/B;AACF"}
1
+ {"version":3,"file":"progress.js","names":[],"sources":["../../src/repositories/progress.ts"],"sourcesContent":["// progress — Drizzle repository (implements the core outbound port).\nimport { and, eq, inArray } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport type {\n ProgressRecord,\n ProgressRepository,\n ProgressTarget,\n ProgressTargetType,\n} from \"@headless-lms/core/progress\";\nimport { progressRecords } from \"../schema/progress.js\";\nimport type { Logger } from \"@headless-lms/core/shared/ports\";\nimport { noopLogger } from \"@headless-lms/core/shared/logger\";\nimport { translateDbErrors } from \"./pg-errors.js\";\n\ntype Row = typeof progressRecords.$inferSelect;\n\nfunction toRecord(row: Row): ProgressRecord {\n return {\n id: row.id,\n orgId: row.orgId,\n orgUserId: row.orgUserId,\n targetType: row.targetType as ProgressTargetType,\n targetId: row.targetId,\n startedAt: row.startedAt,\n position: row.position ?? null,\n completedAt: row.completedAt,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\nexport class DrizzleProgressRepository implements ProgressRepository {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async insert(orgId: string, record: ProgressRecord): Promise<ProgressRecord | null> {\n const [row] = await this.db\n .insert(progressRecords)\n .values({\n id: record.id,\n orgId,\n orgUserId: record.orgUserId,\n targetType: record.targetType,\n targetId: record.targetId,\n startedAt: new Date(record.startedAt),\n position: record.position ?? null,\n completedAt: record.completedAt ? new Date(record.completedAt) : null,\n updatedAt: new Date(record.updatedAt),\n })\n .onConflictDoNothing()\n .returning();\n return row ? toRecord(row) : null;\n }\n\n async findByTarget(orgId: string, target: ProgressTarget): Promise<ProgressRecord | null> {\n const [row] = await this.db\n .select()\n .from(progressRecords)\n .where(\n and(\n eq(progressRecords.orgId, orgId),\n eq(progressRecords.orgUserId, target.orgUserId),\n eq(progressRecords.targetType, target.targetType),\n eq(progressRecords.targetId, target.targetId),\n ),\n )\n .limit(1);\n return row ? toRecord(row) : null;\n }\n\n async findByTargets(\n orgId: string,\n orgUserId: string,\n targetIds: string[],\n opts?: { forUpdate?: boolean },\n ): Promise<ProgressRecord[]> {\n if (targetIds.length === 0) {\n return [];\n }\n const base = this.db\n .select()\n .from(progressRecords)\n .where(\n and(\n eq(progressRecords.orgId, orgId),\n eq(progressRecords.orgUserId, orgUserId),\n inArray(progressRecords.targetId, targetIds),\n ),\n );\n // Deterministic lock order prevents deadlock between overlapping reports.\n const rows = opts?.forUpdate\n ? await base.orderBy(progressRecords.id).for(\"update\")\n : await base;\n return rows.map(toRecord);\n }\n\n async update(\n orgId: string,\n id: string,\n patch: Partial<Pick<ProgressRecord, \"position\" | \"completedAt\">>,\n ): Promise<ProgressRecord | null> {\n const [row] = await this.db\n .update(progressRecords)\n .set({\n ...(\"position\" in patch ? { position: patch.position ?? null } : {}),\n ...(\"completedAt\" in patch\n ? { completedAt: patch.completedAt ? new Date(patch.completedAt) : null }\n : {}),\n })\n .where(and(eq(progressRecords.orgId, orgId), eq(progressRecords.id, id)))\n .returning();\n return row ? toRecord(row) : null;\n }\n}\ntranslateDbErrors(DrizzleProgressRepository);\n"],"mappings":";;;;;AAgBA,SAAS,SAAS,KAA0B;CAC1C,OAAO;EACL,IAAI,IAAI;EACR,OAAO,IAAI;EACX,WAAW,IAAI;EACf,YAAY,IAAI;EAChB,UAAU,IAAI;EACd,WAAW,IAAI;EACf,UAAU,IAAI,YAAY;EAC1B,aAAa,IAAI;EACjB,WAAW,IAAI;EACf,WAAW,IAAI;CACjB;AACF;AAEA,IAAa,4BAAb,MAAqE;CAEhD;CACA;CAFnB,YACE,IACA,SAAkC,YAClC;EAFiB,KAAA,KAAA;EACA,KAAA,SAAA;CAChB;CAEH,MAAM,OAAO,OAAe,QAAwD;EAClF,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,eAAe,CAAC,CACvB,OAAO;GACN,IAAI,OAAO;GACX;GACA,WAAW,OAAO;GAClB,YAAY,OAAO;GACnB,UAAU,OAAO;GACjB,WAAW,IAAI,KAAK,OAAO,SAAS;GACpC,UAAU,OAAO,YAAY;GAC7B,aAAa,OAAO,cAAc,IAAI,KAAK,OAAO,WAAW,IAAI;GACjE,WAAW,IAAI,KAAK,OAAO,SAAS;EACtC,CAAC,CAAC,CACD,oBAAoB,CAAC,CACrB,UAAU;EACb,OAAO,MAAM,SAAS,GAAG,IAAI;CAC/B;CAEA,MAAM,aAAa,OAAe,QAAwD;EACxF,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,CAAC,CACR,KAAK,eAAe,CAAC,CACrB,MACC,IACE,GAAG,gBAAgB,OAAO,KAAK,GAC/B,GAAG,gBAAgB,WAAW,OAAO,SAAS,GAC9C,GAAG,gBAAgB,YAAY,OAAO,UAAU,GAChD,GAAG,gBAAgB,UAAU,OAAO,QAAQ,CAC9C,CACF,CAAC,CACA,MAAM,CAAC;EACV,OAAO,MAAM,SAAS,GAAG,IAAI;CAC/B;CAEA,MAAM,cACJ,OACA,WACA,WACA,MAC2B;EAC3B,IAAI,UAAU,WAAW,GACvB,OAAO,CAAC;EAEV,MAAM,OAAO,KAAK,GACf,OAAO,CAAC,CACR,KAAK,eAAe,CAAC,CACrB,MACC,IACE,GAAG,gBAAgB,OAAO,KAAK,GAC/B,GAAG,gBAAgB,WAAW,SAAS,GACvC,QAAQ,gBAAgB,UAAU,SAAS,CAC7C,CACF;EAKF,QAHa,MAAM,YACf,MAAM,KAAK,QAAQ,gBAAgB,EAAE,CAAC,CAAC,IAAI,QAAQ,IACnD,MAAM,KAAA,CACE,IAAI,QAAQ;CAC1B;CAEA,MAAM,OACJ,OACA,IACA,OACgC;EAChC,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,eAAe,CAAC,CACvB,IAAI;GACH,GAAI,cAAc,QAAQ,EAAE,UAAU,MAAM,YAAY,KAAK,IAAI,CAAC;GAClE,GAAI,iBAAiB,QACjB,EAAE,aAAa,MAAM,cAAc,IAAI,KAAK,MAAM,WAAW,IAAI,KAAK,IACtE,CAAC;EACP,CAAC,CAAC,CACD,MAAM,IAAI,GAAG,gBAAgB,OAAO,KAAK,GAAG,GAAG,gBAAgB,IAAI,EAAE,CAAC,CAAC,CAAC,CACxE,UAAU;EACb,OAAO,MAAM,SAAS,GAAG,IAAI;CAC/B;AACF;AACA,kBAAkB,yBAAyB"}
@@ -1,4 +1,5 @@
1
1
  import { settings } from "../schema/settings.js";
2
+ import { translateDbErrors } from "./pg-errors.js";
2
3
  import { and, eq } from "drizzle-orm";
3
4
  import { noopLogger } from "@headless-lms/core/shared/logger";
4
5
  //#region src/repositories/settings.ts
@@ -67,6 +68,7 @@ var DrizzleSettingsRepository = class {
67
68
  return toRecord(row);
68
69
  }
69
70
  };
71
+ translateDbErrors(DrizzleSettingsRepository);
70
72
  //#endregion
71
73
  export { DrizzleSettingsRepository, deepMerge };
72
74
 
@@ -1 +1 @@
1
- {"version":3,"file":"settings.js","names":[],"sources":["../../src/repositories/settings.ts"],"sourcesContent":["import { and, eq } from 'drizzle-orm';\nimport type { Setting } from '@headless-lms/core/schemas';\nimport type { NodePgDatabase } from 'drizzle-orm/node-postgres';\nimport type { SettingsRecord, SettingsRepository } from '@headless-lms/core/shared/settings';\nimport { settings } from '../schema/settings.js';\nimport type { Logger } from '@headless-lms/core/types';\nimport { noopLogger } from '@headless-lms/core/shared/logger';\n\ntype Row = typeof settings.$inferSelect;\n\nfunction toRecord(row: Row): SettingsRecord {\n return {\n namespace: row.namespace,\n scopeId: row.scopeId,\n value: row.value,\n };\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/**\n * Recursive merge of a partial patch into a stored value: a patch cannot\n * clobber sibling keys at any depth, only the leaves it names. Arrays and\n * scalars are replaced outright — merging arrays element-wise has no meaning\n * for settings, and null is a value a caller may legitimately store.\n * Exported for tests.\n */\nexport function deepMerge(base: unknown, patch: unknown): unknown {\n if (!isPlainObject(base) || !isPlainObject(patch)) {\n return patch;\n }\n const merged: Record<string, unknown> = { ...base };\n for (const [key, value] of Object.entries(patch)) {\n merged[key] = key in base ? deepMerge(base[key], value) : value;\n }\n return merged;\n}\n\nexport class DrizzleSettingsRepository implements SettingsRepository {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async find(orgId: string, scopeId: string, namespace?: string): Promise<SettingsRecord[]> {\n const baseConditions = [eq(settings.orgId, orgId), eq(settings.scopeId, scopeId)];\n const where = namespace\n ? and(...baseConditions, eq(settings.namespace, namespace))\n : and(...baseConditions);\n\n const rows = await this.db.select().from(settings).where(where);\n return rows.map(toRecord);\n }\n\n async patch(\n orgId: string,\n namespace: string,\n scopeId: string,\n patch: Record<string, unknown>,\n ): Promise<SettingsRecord> {\n const key = and(\n eq(settings.orgId, orgId),\n eq(settings.namespace, namespace),\n eq(settings.scopeId, scopeId),\n );\n\n // The merge happens in JS, so the read and the write must be one atomic\n // unit or concurrent patches would clobber each other's sibling keys:\n // SELECT ... FOR UPDATE holds the row for the rest of the transaction.\n const row = await this.db.transaction(async (tx) => {\n let [existing] = await tx.select().from(settings).where(key).for('update');\n\n if (!existing) {\n const [inserted] = await tx\n .insert(settings)\n .values({ orgId, namespace, scopeId, value: patch as Setting['value'] })\n .onConflictDoNothing()\n .returning();\n if (inserted) {\n return inserted;\n }\n // Lost the race: a concurrent transaction inserted the row and has\n // committed by the time onConflictDoNothing returned. Take the lock\n // and merge onto what it wrote.\n [existing] = await tx.select().from(settings).where(key).for('update');\n if (!existing) {\n throw new Error('settings patch: row vanished mid-transaction');\n }\n }\n\n const [updated] = await tx\n .update(settings)\n .set({ value: deepMerge(existing.value, patch) as Setting['value'], updatedAt: new Date() })\n .where(key)\n .returning();\n return updated;\n });\n\n if (!row) {\n throw new Error('settings patch returned no row');\n }\n this.logger.debug('settings.patch', { orgId, namespace, scopeId });\n return toRecord(row);\n }\n}\n"],"mappings":";;;;AAUA,SAAS,SAAS,KAA0B;CAC1C,OAAO;EACL,WAAW,IAAI;EACf,SAAS,IAAI;EACb,OAAO,IAAI;CACb;AACF;AAEA,SAAS,cAAc,OAAkD;CACvE,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;;;;;;;;AASA,SAAgB,UAAU,MAAe,OAAyB;CAChE,IAAI,CAAC,cAAc,IAAI,KAAK,CAAC,cAAc,KAAK,GAC9C,OAAO;CAET,MAAM,SAAkC,EAAE,GAAG,KAAK;CAClD,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,KAAK,GAC7C,OAAO,OAAO,OAAO,OAAO,UAAU,KAAK,MAAM,KAAK,IAAI;CAE5D,OAAO;AACT;AAEA,IAAa,4BAAb,MAAqE;CAEhD;CACA;CAFnB,YACE,IACA,SAAkC,YAClC;EAFiB,KAAA,KAAA;EACA,KAAA,SAAA;CAChB;CAEH,MAAM,KAAK,OAAe,SAAiB,WAA+C;EACxF,MAAM,iBAAiB,CAAC,GAAG,SAAS,OAAO,KAAK,GAAG,GAAG,SAAS,SAAS,OAAO,CAAC;EAChF,MAAM,QAAQ,YACV,IAAI,GAAG,gBAAgB,GAAG,SAAS,WAAW,SAAS,CAAC,IACxD,IAAI,GAAG,cAAc;EAGzB,QAAO,MADY,KAAK,GAAG,OAAO,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC,MAAM,KAAK,EAAA,CAClD,IAAI,QAAQ;CAC1B;CAEA,MAAM,MACJ,OACA,WACA,SACA,OACyB;EACzB,MAAM,MAAM,IACV,GAAG,SAAS,OAAO,KAAK,GACxB,GAAG,SAAS,WAAW,SAAS,GAChC,GAAG,SAAS,SAAS,OAAO,CAC9B;EAKA,MAAM,MAAM,MAAM,KAAK,GAAG,YAAY,OAAO,OAAO;GAClD,IAAI,CAAC,YAAY,MAAM,GAAG,OAAO,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,QAAQ;GAEzE,IAAI,CAAC,UAAU;IACb,MAAM,CAAC,YAAY,MAAM,GACtB,OAAO,QAAQ,CAAC,CAChB,OAAO;KAAE;KAAO;KAAW;KAAS,OAAO;IAA0B,CAAC,CAAC,CACvE,oBAAoB,CAAC,CACrB,UAAU;IACb,IAAI,UACF,OAAO;IAKT,CAAC,YAAY,MAAM,GAAG,OAAO,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,QAAQ;IACrE,IAAI,CAAC,UACH,MAAM,IAAI,MAAM,8CAA8C;GAElE;GAEA,MAAM,CAAC,WAAW,MAAM,GACrB,OAAO,QAAQ,CAAC,CAChB,IAAI;IAAE,OAAO,UAAU,SAAS,OAAO,KAAK;IAAuB,2BAAW,IAAI,KAAK;GAAE,CAAC,CAAC,CAC3F,MAAM,GAAG,CAAC,CACV,UAAU;GACb,OAAO;EACT,CAAC;EAED,IAAI,CAAC,KACH,MAAM,IAAI,MAAM,gCAAgC;EAElD,KAAK,OAAO,MAAM,kBAAkB;GAAE;GAAO;GAAW;EAAQ,CAAC;EACjE,OAAO,SAAS,GAAG;CACrB;AACF"}
1
+ {"version":3,"file":"settings.js","names":[],"sources":["../../src/repositories/settings.ts"],"sourcesContent":["import { and, eq } from \"drizzle-orm\";\nimport type { Setting } from \"@headless-lms/core/schemas\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport type { SettingsRecord, SettingsRepository } from \"@headless-lms/core/shared/settings\";\nimport { settings } from \"../schema/settings.js\";\nimport type { Logger } from \"@headless-lms/core/types\";\nimport { noopLogger } from \"@headless-lms/core/shared/logger\";\nimport { translateDbErrors } from \"./pg-errors.js\";\n\ntype Row = typeof settings.$inferSelect;\n\nfunction toRecord(row: Row): SettingsRecord {\n return {\n namespace: row.namespace,\n scopeId: row.scopeId,\n value: row.value,\n };\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * Recursive merge of a partial patch into a stored value: a patch cannot\n * clobber sibling keys at any depth, only the leaves it names. Arrays and\n * scalars are replaced outright — merging arrays element-wise has no meaning\n * for settings, and null is a value a caller may legitimately store.\n * Exported for tests.\n */\nexport function deepMerge(base: unknown, patch: unknown): unknown {\n if (!isPlainObject(base) || !isPlainObject(patch)) {\n return patch;\n }\n const merged: Record<string, unknown> = { ...base };\n for (const [key, value] of Object.entries(patch)) {\n merged[key] = key in base ? deepMerge(base[key], value) : value;\n }\n return merged;\n}\n\nexport class DrizzleSettingsRepository implements SettingsRepository {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async find(orgId: string, scopeId: string, namespace?: string): Promise<SettingsRecord[]> {\n const baseConditions = [eq(settings.orgId, orgId), eq(settings.scopeId, scopeId)];\n const where = namespace\n ? and(...baseConditions, eq(settings.namespace, namespace))\n : and(...baseConditions);\n\n const rows = await this.db.select().from(settings).where(where);\n return rows.map(toRecord);\n }\n\n async patch(\n orgId: string,\n namespace: string,\n scopeId: string,\n patch: Record<string, unknown>,\n ): Promise<SettingsRecord> {\n const key = and(\n eq(settings.orgId, orgId),\n eq(settings.namespace, namespace),\n eq(settings.scopeId, scopeId),\n );\n\n // The merge happens in JS, so the read and the write must be one atomic\n // unit or concurrent patches would clobber each other's sibling keys:\n // SELECT ... FOR UPDATE holds the row for the rest of the transaction.\n const row = await this.db.transaction(async (tx) => {\n let [existing] = await tx.select().from(settings).where(key).for(\"update\");\n\n if (!existing) {\n const [inserted] = await tx\n .insert(settings)\n .values({ orgId, namespace, scopeId, value: patch as Setting[\"value\"] })\n .onConflictDoNothing()\n .returning();\n if (inserted) {\n return inserted;\n }\n // Lost the race: a concurrent transaction inserted the row and has\n // committed by the time onConflictDoNothing returned. Take the lock\n // and merge onto what it wrote.\n [existing] = await tx.select().from(settings).where(key).for(\"update\");\n if (!existing) {\n throw new Error(\"settings patch: row vanished mid-transaction\");\n }\n }\n\n const [updated] = await tx\n .update(settings)\n .set({ value: deepMerge(existing.value, patch) as Setting[\"value\"], updatedAt: new Date() })\n .where(key)\n .returning();\n return updated;\n });\n\n if (!row) {\n throw new Error(\"settings patch returned no row\");\n }\n this.logger.debug(\"settings.patch\", { orgId, namespace, scopeId });\n return toRecord(row);\n }\n}\ntranslateDbErrors(DrizzleSettingsRepository);\n"],"mappings":";;;;;AAWA,SAAS,SAAS,KAA0B;CAC1C,OAAO;EACL,WAAW,IAAI;EACf,SAAS,IAAI;EACb,OAAO,IAAI;CACb;AACF;AAEA,SAAS,cAAc,OAAkD;CACvE,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;;;;;;;;AASA,SAAgB,UAAU,MAAe,OAAyB;CAChE,IAAI,CAAC,cAAc,IAAI,KAAK,CAAC,cAAc,KAAK,GAC9C,OAAO;CAET,MAAM,SAAkC,EAAE,GAAG,KAAK;CAClD,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,KAAK,GAC7C,OAAO,OAAO,OAAO,OAAO,UAAU,KAAK,MAAM,KAAK,IAAI;CAE5D,OAAO;AACT;AAEA,IAAa,4BAAb,MAAqE;CAEhD;CACA;CAFnB,YACE,IACA,SAAkC,YAClC;EAFiB,KAAA,KAAA;EACA,KAAA,SAAA;CAChB;CAEH,MAAM,KAAK,OAAe,SAAiB,WAA+C;EACxF,MAAM,iBAAiB,CAAC,GAAG,SAAS,OAAO,KAAK,GAAG,GAAG,SAAS,SAAS,OAAO,CAAC;EAChF,MAAM,QAAQ,YACV,IAAI,GAAG,gBAAgB,GAAG,SAAS,WAAW,SAAS,CAAC,IACxD,IAAI,GAAG,cAAc;EAGzB,QAAO,MADY,KAAK,GAAG,OAAO,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC,MAAM,KAAK,EAAA,CAClD,IAAI,QAAQ;CAC1B;CAEA,MAAM,MACJ,OACA,WACA,SACA,OACyB;EACzB,MAAM,MAAM,IACV,GAAG,SAAS,OAAO,KAAK,GACxB,GAAG,SAAS,WAAW,SAAS,GAChC,GAAG,SAAS,SAAS,OAAO,CAC9B;EAKA,MAAM,MAAM,MAAM,KAAK,GAAG,YAAY,OAAO,OAAO;GAClD,IAAI,CAAC,YAAY,MAAM,GAAG,OAAO,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,QAAQ;GAEzE,IAAI,CAAC,UAAU;IACb,MAAM,CAAC,YAAY,MAAM,GACtB,OAAO,QAAQ,CAAC,CAChB,OAAO;KAAE;KAAO;KAAW;KAAS,OAAO;IAA0B,CAAC,CAAC,CACvE,oBAAoB,CAAC,CACrB,UAAU;IACb,IAAI,UACF,OAAO;IAKT,CAAC,YAAY,MAAM,GAAG,OAAO,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,QAAQ;IACrE,IAAI,CAAC,UACH,MAAM,IAAI,MAAM,8CAA8C;GAElE;GAEA,MAAM,CAAC,WAAW,MAAM,GACrB,OAAO,QAAQ,CAAC,CAChB,IAAI;IAAE,OAAO,UAAU,SAAS,OAAO,KAAK;IAAuB,2BAAW,IAAI,KAAK;GAAE,CAAC,CAAC,CAC3F,MAAM,GAAG,CAAC,CACV,UAAU;GACb,OAAO;EACT,CAAC;EAED,IAAI,CAAC,KACH,MAAM,IAAI,MAAM,gCAAgC;EAElD,KAAK,OAAO,MAAM,kBAAkB;GAAE;GAAO;GAAW;EAAQ,CAAC;EACjE,OAAO,SAAS,GAAG;CACrB;AACF;AACA,kBAAkB,yBAAyB"}
@@ -1,6 +1,6 @@
1
1
  import { NodePgDatabase } from "drizzle-orm/node-postgres";
2
2
  import { Logger } from "@headless-lms/core/shared/ports";
3
- import { Page, Student, StudentsQuery, StudentsReportRepository } from "@headless-lms/core/reporting/students";
3
+ import { Page, Student, StudentAnalytics, StudentsQuery, StudentsReportRepository } from "@headless-lms/core/reporting/students";
4
4
  //#region src/repositories/students.d.ts
5
5
  declare class DrizzleStudentsRepository implements StudentsReportRepository {
6
6
  private readonly db;
@@ -8,6 +8,7 @@ declare class DrizzleStudentsRepository implements StudentsReportRepository {
8
8
  constructor(db: NodePgDatabase, logger?: Logger);
9
9
  list(orgId: string, query: StudentsQuery): Promise<Page<Student>>;
10
10
  findById(orgId: string, id: string): Promise<Student | null>;
11
+ analytics(orgId: string, id: string): Promise<StudentAnalytics | null>;
11
12
  private resolveOrder;
12
13
  }
13
14
  //#endregion
@@ -2,6 +2,7 @@ import { users } from "../schema/identity.js";
2
2
  import { orgUsers } from "../schema/organizations.js";
3
3
  import { entitlements } from "../schema/entitlements.js";
4
4
  import "../schema/index.js";
5
+ import { translateDbErrors } from "./pg-errors.js";
5
6
  import { user } from "../schema/better-auth.js";
6
7
  import { orgUserProfileColumns } from "./org-user-profile.js";
7
8
  import { and, asc, desc, eq, ilike, or, sql } from "drizzle-orm";
@@ -10,6 +11,10 @@ import { STUDENT_ROLE } from "@headless-lms/core/organizations";
10
11
  //#region src/repositories/students.ts
11
12
  const entitlementCountExpr = sql`count(${entitlements.id})`;
12
13
  const avgProgressExpr = sql`0`;
14
+ const lastActiveExpr = sql`(
15
+ select max(pr.updated_at) from progress_records pr
16
+ where pr.org_id = ${orgUsers.orgId} and pr.org_user_id = ${orgUsers.id}
17
+ )`;
13
18
  function toStudent(row) {
14
19
  return {
15
20
  id: row.id,
@@ -21,7 +26,7 @@ function toStudent(row) {
21
26
  entitlementCount: Number(row.entitlementCount),
22
27
  avgProgress: Number(row.avgProgress),
23
28
  joinedAt: row.createdAt.toISOString(),
24
- lastActiveAt: null
29
+ lastActiveAt: row.lastActiveAt ? new Date(row.lastActiveAt).toISOString() : null
25
30
  };
26
31
  }
27
32
  var DrizzleStudentsRepository = class {
@@ -46,7 +51,8 @@ var DrizzleStudentsRepository = class {
46
51
  status: orgUsers.status,
47
52
  createdAt: orgUsers.createdAt,
48
53
  entitlementCount: entitlementCountExpr,
49
- avgProgress: avgProgressExpr
54
+ avgProgress: avgProgressExpr,
55
+ lastActiveAt: lastActiveExpr
50
56
  }).from(orgUsers).leftJoin(entitlements, and(eq(entitlements.orgId, orgUsers.orgId), eq(entitlements.orgUserId, orgUsers.id))).innerJoin(users, eq(users.id, orgUsers.userId)).leftJoin(user, eq(user.id, users.externalId)).where(where).groupBy(orgUsers.orgId, orgUsers.id, orgUsers.status, users.email, users.firstName, users.lastName, user.image).orderBy(...this.resolveOrder(query.sort)).limit(query.pageSize).offset((query.page - 1) * query.pageSize)).map(toStudent),
51
57
  total: Number(totals?.total ?? 0),
52
58
  page: query.page,
@@ -59,10 +65,79 @@ var DrizzleStudentsRepository = class {
59
65
  status: orgUsers.status,
60
66
  createdAt: orgUsers.createdAt,
61
67
  entitlementCount: entitlementCountExpr,
62
- avgProgress: avgProgressExpr
68
+ avgProgress: avgProgressExpr,
69
+ lastActiveAt: lastActiveExpr
63
70
  }).from(orgUsers).leftJoin(entitlements, and(eq(entitlements.orgId, orgUsers.orgId), eq(entitlements.orgUserId, orgUsers.id))).innerJoin(users, eq(users.id, orgUsers.userId)).leftJoin(user, eq(user.id, users.externalId)).where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.id, id), eq(orgUsers.role, STUDENT_ROLE))).groupBy(orgUsers.orgId, orgUsers.id, orgUsers.status, users.email, users.firstName, users.lastName, user.image).limit(1);
64
71
  return row ? toStudent(row) : null;
65
72
  }
73
+ async analytics(orgId, id) {
74
+ const [exists] = await this.db.select({ id: orgUsers.id }).from(orgUsers).where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.id, id), eq(orgUsers.role, STUDENT_ROLE)));
75
+ if (!exists) return null;
76
+ const courses = (await this.db.execute(sql`
77
+ with cohort_courses as (
78
+ select c.id, c.title
79
+ from entitlements e
80
+ join courses c on c.org_id = e.org_id and c.id = e.content_id
81
+ where e.org_id = ${orgId}
82
+ and e.org_user_id = ${id}
83
+ and e.status = 'active'
84
+ and (e.expires_at is null or e.expires_at >= now())
85
+ ),
86
+ acts as (
87
+ select a.course_id, a.id
88
+ from activities a
89
+ where a.org_id = ${orgId}
90
+ and a.course_id in (select id from cohort_courses)
91
+ and coalesce((a.settings ->> 'published')::boolean, true)
92
+ )
93
+ select
94
+ cc.id as course_id,
95
+ cc.title,
96
+ count(a.id)::int as total_activities,
97
+ count(pr.id) filter (where pr.completed_at is not null)::int as completed_activities,
98
+ min(pr.started_at) as started_at,
99
+ max(pr.updated_at) as last_activity_at,
100
+ (
101
+ select prc.completed_at from progress_records prc
102
+ where prc.org_id = ${orgId}
103
+ and prc.org_user_id = ${id}
104
+ and prc.target_type = 'course'
105
+ and prc.target_id = cc.id
106
+ ) as completed_at
107
+ from cohort_courses cc
108
+ left join acts a on a.course_id = cc.id
109
+ left join progress_records pr
110
+ on pr.org_id = ${orgId}
111
+ and pr.org_user_id = ${id}
112
+ and pr.target_type = 'activity'
113
+ and pr.target_id = a.id
114
+ group by cc.id, cc.title
115
+ order by cc.title
116
+ `)).rows.map((r) => {
117
+ const total = Number(r.total_activities);
118
+ const done = Number(r.completed_activities);
119
+ return {
120
+ courseId: r.course_id,
121
+ title: r.title,
122
+ totalActivities: total,
123
+ completedActivities: done,
124
+ progress: total > 0 ? Math.round(Math.min(done / total, 1) * 100) : 0,
125
+ startedAt: r.started_at ? new Date(r.started_at).toISOString() : null,
126
+ lastActivityAt: r.last_activity_at ? new Date(r.last_activity_at).toISOString() : null,
127
+ completedAt: r.completed_at ? new Date(r.completed_at).toISOString() : null
128
+ };
129
+ });
130
+ const started = courses.filter((c) => c.startedAt !== null).length;
131
+ const completed = courses.filter((c) => c.completedAt !== null).length;
132
+ const avgProgress = courses.length > 0 ? Math.round(courses.reduce((sum, c) => sum + c.progress, 0) / courses.length) : 0;
133
+ return {
134
+ enrolled: courses.length,
135
+ started,
136
+ completed,
137
+ avgProgress,
138
+ courses
139
+ };
140
+ }
66
141
  resolveOrder(sort) {
67
142
  const descending = sort?.startsWith("-") ?? false;
68
143
  const field = sort ? descending ? sort.slice(1) : sort : "firstName";
@@ -72,11 +147,13 @@ var DrizzleStudentsRepository = class {
72
147
  case "entitlementCount": return [dir(entitlementCountExpr)];
73
148
  case "avgProgress": return [dir(avgProgressExpr)];
74
149
  case "joinedAt": return [dir(orgUsers.createdAt)];
150
+ case "lastActiveAt": return [dir(lastActiveExpr)];
75
151
  case "lastName": return [dir(users.lastName), dir(users.firstName)];
76
152
  default: return [dir(users.firstName), dir(users.lastName)];
77
153
  }
78
154
  }
79
155
  };
156
+ translateDbErrors(DrizzleStudentsRepository);
80
157
  //#endregion
81
158
  export { DrizzleStudentsRepository };
82
159
 
@@ -1 +1 @@
1
- {"version":3,"file":"students.js","names":[],"sources":["../../src/repositories/students.ts"],"sourcesContent":["// students — Drizzle repository (implements the core outbound port).\n// A read-model over org users + entitlements: a \"student\" row in the report is\n// any org_users row with role 'student', with or without entitlements. Rooted at\n// `org_users` and scoped by its org, with entitlements LEFT JOINed in for the\n// aggregated count. Name/email come from the identity `users` row (INNER, since\n// user_id is NOT NULL); the avatar comes from the better-auth `user` table,\n// LEFT JOINed via that same identity row.\nimport { and, asc, desc, eq, ilike, or, sql, type SQL } from 'drizzle-orm';\nimport type { NodePgDatabase } from 'drizzle-orm/node-postgres';\nimport type {\n Page,\n Student,\n StudentsQuery,\n StudentsReportRepository,\n} from '@headless-lms/core/reporting/students';\nimport { orgUsers, users, entitlements } from '../schema/index.js';\nimport { STUDENT_ROLE } from '@headless-lms/core/organizations';\nimport { user } from '../schema/better-auth.js';\nimport type { OrgUserStatus } from '@headless-lms/core/types';\nimport type { Logger } from '@headless-lms/core/shared/ports';\nimport { noopLogger } from '@headless-lms/core/shared/logger';\nimport { orgUserProfileColumns } from './org-user-profile.js';\n\nconst entitlementCountExpr = sql<number>`count(${entitlements.id})`;\n// Completion now lives in the progress domain; the students report no longer\n// derives a percentage from entitlements. Placeholder until wired to progress.\nconst avgProgressExpr = sql<number>`0`;\n\ninterface StudentRow {\n id: string;\n email: string;\n image: string | null;\n firstName: string | null;\n lastName: string | null;\n status: OrgUserStatus;\n createdAt: Date;\n entitlementCount: number;\n avgProgress: number;\n}\n\nfunction toStudent(row: StudentRow): Student {\n return {\n id: row.id,\n email: row.email,\n image: row.image ?? null,\n firstName: row.firstName,\n lastName: row.lastName,\n status: row.status,\n entitlementCount: Number(row.entitlementCount),\n avgProgress: Number(row.avgProgress),\n joinedAt: row.createdAt.toISOString(),\n lastActiveAt: null,\n };\n}\n\nexport class DrizzleStudentsRepository implements StudentsReportRepository {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async list(orgId: string, query: StudentsQuery): Promise<Page<Student>> {\n // Staff share this table now — the role filter is what keeps them out of\n // the students report.\n const filters: SQL[] = [eq(orgUsers.orgId, orgId), eq(orgUsers.role, STUDENT_ROLE)];\n const q = query.search?.trim();\n if (q) {\n const like = `%${q}%`;\n filters.push(\n or(\n ilike(users.firstName, like),\n ilike(users.lastName, like),\n ilike(users.email, like),\n )!,\n );\n }\n const where = and(...filters);\n\n const [totals] = await this.db\n .select({ total: sql<number>`count(*)` })\n .from(orgUsers)\n .innerJoin(users, eq(users.id, orgUsers.userId))\n .where(where);\n\n const rows = await this.db\n .select({\n ...orgUserProfileColumns,\n status: orgUsers.status,\n createdAt: orgUsers.createdAt,\n entitlementCount: entitlementCountExpr,\n avgProgress: avgProgressExpr,\n })\n .from(orgUsers)\n .leftJoin(\n entitlements,\n and(eq(entitlements.orgId, orgUsers.orgId), eq(entitlements.orgUserId, orgUsers.id)),\n )\n .innerJoin(users, eq(users.id, orgUsers.userId))\n .leftJoin(user, eq(user.id, users.externalId))\n .where(where)\n // Group by the full composite PK (orgId, id): grouping by id alone gives\n // Postgres no functional dependency for the other students columns.\n .groupBy(\n orgUsers.orgId,\n orgUsers.id,\n orgUsers.status,\n users.email,\n users.firstName,\n users.lastName,\n user.image,\n )\n .orderBy(...this.resolveOrder(query.sort))\n .limit(query.pageSize)\n .offset((query.page - 1) * query.pageSize);\n\n return {\n rows: rows.map(toStudent),\n total: Number(totals?.total ?? 0),\n page: query.page,\n pageSize: query.pageSize,\n };\n }\n\n async findById(orgId: string, id: string): Promise<Student | null> {\n const [row] = await this.db\n .select({\n ...orgUserProfileColumns,\n status: orgUsers.status,\n createdAt: orgUsers.createdAt,\n entitlementCount: entitlementCountExpr,\n avgProgress: avgProgressExpr,\n })\n .from(orgUsers)\n .leftJoin(\n entitlements,\n and(eq(entitlements.orgId, orgUsers.orgId), eq(entitlements.orgUserId, orgUsers.id)),\n )\n .innerJoin(users, eq(users.id, orgUsers.userId))\n .leftJoin(user, eq(user.id, users.externalId))\n .where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.id, id), eq(orgUsers.role, STUDENT_ROLE)))\n .groupBy(\n orgUsers.orgId,\n orgUsers.id,\n orgUsers.status,\n users.email,\n users.firstName,\n users.lastName,\n user.image,\n )\n .limit(1);\n return row ? toStudent(row) : null;\n }\n\n private resolveOrder(sort?: string): SQL[] {\n const descending = sort?.startsWith('-') ?? false;\n const field = sort ? (descending ? sort.slice(1) : sort) : 'firstName';\n const dir = descending ? desc : asc;\n switch (field) {\n case 'email':\n return [dir(users.email)];\n case 'entitlementCount':\n return [dir(entitlementCountExpr)];\n case 'avgProgress':\n return [dir(avgProgressExpr)];\n case 'joinedAt':\n return [dir(orgUsers.createdAt)];\n case 'lastName':\n return [dir(users.lastName), dir(users.firstName)];\n case 'firstName':\n default:\n return [dir(users.firstName), dir(users.lastName)];\n }\n }\n}\n"],"mappings":";;;;;;;;;;AAuBA,MAAM,uBAAuB,GAAW,SAAS,aAAa,GAAG;AAGjE,MAAM,kBAAkB,GAAW;AAcnC,SAAS,UAAU,KAA0B;CAC3C,OAAO;EACL,IAAI,IAAI;EACR,OAAO,IAAI;EACX,OAAO,IAAI,SAAS;EACpB,WAAW,IAAI;EACf,UAAU,IAAI;EACd,QAAQ,IAAI;EACZ,kBAAkB,OAAO,IAAI,gBAAgB;EAC7C,aAAa,OAAO,IAAI,WAAW;EACnC,UAAU,IAAI,UAAU,YAAY;EACpC,cAAc;CAChB;AACF;AAEA,IAAa,4BAAb,MAA2E;CAEtD;CACA;CAFnB,YACE,IACA,SAAkC,YAClC;EAFiB,KAAA,KAAA;EACA,KAAA,SAAA;CAChB;CAEH,MAAM,KAAK,OAAe,OAA8C;EAGtE,MAAM,UAAiB,CAAC,GAAG,SAAS,OAAO,KAAK,GAAG,GAAG,SAAS,MAAM,YAAY,CAAC;EAClF,MAAM,IAAI,MAAM,QAAQ,KAAK;EAC7B,IAAI,GAAG;GACL,MAAM,OAAO,IAAI,EAAE;GACnB,QAAQ,KACN,GACE,MAAM,MAAM,WAAW,IAAI,GAC3B,MAAM,MAAM,UAAU,IAAI,GAC1B,MAAM,MAAM,OAAO,IAAI,CACzB,CACF;EACF;EACA,MAAM,QAAQ,IAAI,GAAG,OAAO;EAE5B,MAAM,CAAC,UAAU,MAAM,KAAK,GACzB,OAAO,EAAE,OAAO,GAAW,WAAW,CAAC,CAAC,CACxC,KAAK,QAAQ,CAAC,CACd,UAAU,OAAO,GAAG,MAAM,IAAI,SAAS,MAAM,CAAC,CAAC,CAC/C,MAAM,KAAK;EAiCd,OAAO;GACL,OAAM,MAhCW,KAAK,GACrB,OAAO;IACN,GAAG;IACH,QAAQ,SAAS;IACjB,WAAW,SAAS;IACpB,kBAAkB;IAClB,aAAa;GACf,CAAC,CAAC,CACD,KAAK,QAAQ,CAAC,CACd,SACC,cACA,IAAI,GAAG,aAAa,OAAO,SAAS,KAAK,GAAG,GAAG,aAAa,WAAW,SAAS,EAAE,CAAC,CACrF,CAAC,CACA,UAAU,OAAO,GAAG,MAAM,IAAI,SAAS,MAAM,CAAC,CAAC,CAC/C,SAAS,MAAM,GAAG,KAAK,IAAI,MAAM,UAAU,CAAC,CAAC,CAC7C,MAAM,KAAK,CAAC,CAGZ,QACC,SAAS,OACT,SAAS,IACT,SAAS,QACT,MAAM,OACN,MAAM,WACN,MAAM,UACN,KAAK,KACP,CAAC,CACA,QAAQ,GAAG,KAAK,aAAa,MAAM,IAAI,CAAC,CAAC,CACzC,MAAM,MAAM,QAAQ,CAAC,CACrB,QAAQ,MAAM,OAAO,KAAK,MAAM,QAAQ,EAAA,CAG9B,IAAI,SAAS;GACxB,OAAO,OAAO,QAAQ,SAAS,CAAC;GAChC,MAAM,MAAM;GACZ,UAAU,MAAM;EAClB;CACF;CAEA,MAAM,SAAS,OAAe,IAAqC;EACjE,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO;GACN,GAAG;GACH,QAAQ,SAAS;GACjB,WAAW,SAAS;GACpB,kBAAkB;GAClB,aAAa;EACf,CAAC,CAAC,CACD,KAAK,QAAQ,CAAC,CACd,SACC,cACA,IAAI,GAAG,aAAa,OAAO,SAAS,KAAK,GAAG,GAAG,aAAa,WAAW,SAAS,EAAE,CAAC,CACrF,CAAC,CACA,UAAU,OAAO,GAAG,MAAM,IAAI,SAAS,MAAM,CAAC,CAAC,CAC/C,SAAS,MAAM,GAAG,KAAK,IAAI,MAAM,UAAU,CAAC,CAAC,CAC7C,MAAM,IAAI,GAAG,SAAS,OAAO,KAAK,GAAG,GAAG,SAAS,IAAI,EAAE,GAAG,GAAG,SAAS,MAAM,YAAY,CAAC,CAAC,CAAC,CAC3F,QACC,SAAS,OACT,SAAS,IACT,SAAS,QACT,MAAM,OACN,MAAM,WACN,MAAM,UACN,KAAK,KACP,CAAC,CACA,MAAM,CAAC;EACV,OAAO,MAAM,UAAU,GAAG,IAAI;CAChC;CAEA,aAAqB,MAAsB;EACzC,MAAM,aAAa,MAAM,WAAW,GAAG,KAAK;EAC5C,MAAM,QAAQ,OAAQ,aAAa,KAAK,MAAM,CAAC,IAAI,OAAQ;EAC3D,MAAM,MAAM,aAAa,OAAO;EAChC,QAAQ,OAAR;GACE,KAAK,SACH,OAAO,CAAC,IAAI,MAAM,KAAK,CAAC;GAC1B,KAAK,oBACH,OAAO,CAAC,IAAI,oBAAoB,CAAC;GACnC,KAAK,eACH,OAAO,CAAC,IAAI,eAAe,CAAC;GAC9B,KAAK,YACH,OAAO,CAAC,IAAI,SAAS,SAAS,CAAC;GACjC,KAAK,YACH,OAAO,CAAC,IAAI,MAAM,QAAQ,GAAG,IAAI,MAAM,SAAS,CAAC;GAEnD,SACE,OAAO,CAAC,IAAI,MAAM,SAAS,GAAG,IAAI,MAAM,QAAQ,CAAC;EACrD;CACF;AACF"}
1
+ {"version":3,"file":"students.js","names":[],"sources":["../../src/repositories/students.ts"],"sourcesContent":["// students — Drizzle repository (implements the core outbound port).\n// A read-model over org users + entitlements: a \"student\" row in the report is\n// any org_users row with role 'student', with or without entitlements. Rooted at\n// `org_users` and scoped by its org, with entitlements LEFT JOINed in for the\n// aggregated count. Name/email come from the identity `users` row (INNER, since\n// user_id is NOT NULL); the avatar comes from the better-auth `user` table,\n// LEFT JOINed via that same identity row.\nimport { and, asc, desc, eq, ilike, or, sql, type SQL } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport type {\n Page,\n Student,\n StudentAnalytics,\n StudentCourseProgress,\n StudentsQuery,\n StudentsReportRepository,\n} from \"@headless-lms/core/reporting/students\";\nimport { orgUsers, users, entitlements } from \"../schema/index.js\";\nimport { STUDENT_ROLE } from \"@headless-lms/core/organizations\";\nimport { user } from \"../schema/better-auth.js\";\nimport type { OrgUserStatus } from \"@headless-lms/core/types\";\nimport type { Logger } from \"@headless-lms/core/shared/ports\";\nimport { noopLogger } from \"@headless-lms/core/shared/logger\";\nimport { orgUserProfileColumns } from \"./org-user-profile.js\";\nimport { translateDbErrors } from \"./pg-errors.js\";\n\nconst entitlementCountExpr = sql<number>`count(${entitlements.id})`;\n// Completion now lives in the progress domain; the students report no longer\n// derives a percentage from entitlements. Placeholder until wired to progress.\nconst avgProgressExpr = sql<number>`0`;\n// Last learning activity: the newest touch on any of the student's progress\n// records. A scalar subquery so it neither fans out the entitlement count nor\n// widens the GROUP BY.\nconst lastActiveExpr = sql<Date | null>`(\n select max(pr.updated_at) from progress_records pr\n where pr.org_id = ${orgUsers.orgId} and pr.org_user_id = ${orgUsers.id}\n)`;\n\ninterface StudentRow {\n id: string;\n email: string;\n image: string | null;\n firstName: string | null;\n lastName: string | null;\n status: OrgUserStatus;\n createdAt: Date;\n entitlementCount: number;\n avgProgress: number;\n lastActiveAt: Date | null;\n}\n\nfunction toStudent(row: StudentRow): Student {\n return {\n id: row.id,\n email: row.email,\n image: row.image ?? null,\n firstName: row.firstName,\n lastName: row.lastName,\n status: row.status,\n entitlementCount: Number(row.entitlementCount),\n avgProgress: Number(row.avgProgress),\n joinedAt: row.createdAt.toISOString(),\n lastActiveAt: row.lastActiveAt ? new Date(row.lastActiveAt).toISOString() : null,\n };\n}\n\nexport class DrizzleStudentsRepository implements StudentsReportRepository {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async list(orgId: string, query: StudentsQuery): Promise<Page<Student>> {\n // Staff share this table now — the role filter is what keeps them out of\n // the students report.\n const filters: SQL[] = [eq(orgUsers.orgId, orgId), eq(orgUsers.role, STUDENT_ROLE)];\n const q = query.search?.trim();\n if (q) {\n const like = `%${q}%`;\n filters.push(\n or(ilike(users.firstName, like), ilike(users.lastName, like), ilike(users.email, like))!,\n );\n }\n const where = and(...filters);\n\n const [totals] = await this.db\n .select({ total: sql<number>`count(*)` })\n .from(orgUsers)\n .innerJoin(users, eq(users.id, orgUsers.userId))\n .where(where);\n\n const rows = await this.db\n .select({\n ...orgUserProfileColumns,\n status: orgUsers.status,\n createdAt: orgUsers.createdAt,\n entitlementCount: entitlementCountExpr,\n avgProgress: avgProgressExpr,\n lastActiveAt: lastActiveExpr,\n })\n .from(orgUsers)\n .leftJoin(\n entitlements,\n and(eq(entitlements.orgId, orgUsers.orgId), eq(entitlements.orgUserId, orgUsers.id)),\n )\n .innerJoin(users, eq(users.id, orgUsers.userId))\n .leftJoin(user, eq(user.id, users.externalId))\n .where(where)\n // Group by the full composite PK (orgId, id): grouping by id alone gives\n // Postgres no functional dependency for the other students columns.\n .groupBy(\n orgUsers.orgId,\n orgUsers.id,\n orgUsers.status,\n users.email,\n users.firstName,\n users.lastName,\n user.image,\n )\n .orderBy(...this.resolveOrder(query.sort))\n .limit(query.pageSize)\n .offset((query.page - 1) * query.pageSize);\n\n return {\n rows: rows.map(toStudent),\n total: Number(totals?.total ?? 0),\n page: query.page,\n pageSize: query.pageSize,\n };\n }\n\n async findById(orgId: string, id: string): Promise<Student | null> {\n const [row] = await this.db\n .select({\n ...orgUserProfileColumns,\n status: orgUsers.status,\n createdAt: orgUsers.createdAt,\n entitlementCount: entitlementCountExpr,\n avgProgress: avgProgressExpr,\n lastActiveAt: lastActiveExpr,\n })\n .from(orgUsers)\n .leftJoin(\n entitlements,\n and(eq(entitlements.orgId, orgUsers.orgId), eq(entitlements.orgUserId, orgUsers.id)),\n )\n .innerJoin(users, eq(users.id, orgUsers.userId))\n .leftJoin(user, eq(user.id, users.externalId))\n .where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.id, id), eq(orgUsers.role, STUDENT_ROLE)))\n .groupBy(\n orgUsers.orgId,\n orgUsers.id,\n orgUsers.status,\n users.email,\n users.firstName,\n users.lastName,\n user.image,\n )\n .limit(1);\n return row ? toStudent(row) : null;\n }\n\n // Learner record: one row per course the student holds an effective-active\n // entitlement to (mirroring the course-analytics cohort rule), with progress\n // computed over published activities the same way reporting/courses does.\n async analytics(orgId: string, id: string): Promise<StudentAnalytics | null> {\n const [exists] = await this.db\n .select({ id: orgUsers.id })\n .from(orgUsers)\n .where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.id, id), eq(orgUsers.role, STUDENT_ROLE)));\n if (!exists) {\n return null;\n }\n\n const result = await this.db.execute(sql`\n with cohort_courses as (\n select c.id, c.title\n from entitlements e\n join courses c on c.org_id = e.org_id and c.id = e.content_id\n where e.org_id = ${orgId}\n and e.org_user_id = ${id}\n and e.status = 'active'\n and (e.expires_at is null or e.expires_at >= now())\n ),\n acts as (\n select a.course_id, a.id\n from activities a\n where a.org_id = ${orgId}\n and a.course_id in (select id from cohort_courses)\n and coalesce((a.settings ->> 'published')::boolean, true)\n )\n select\n cc.id as course_id,\n cc.title,\n count(a.id)::int as total_activities,\n count(pr.id) filter (where pr.completed_at is not null)::int as completed_activities,\n min(pr.started_at) as started_at,\n max(pr.updated_at) as last_activity_at,\n (\n select prc.completed_at from progress_records prc\n where prc.org_id = ${orgId}\n and prc.org_user_id = ${id}\n and prc.target_type = 'course'\n and prc.target_id = cc.id\n ) as completed_at\n from cohort_courses cc\n left join acts a on a.course_id = cc.id\n left join progress_records pr\n on pr.org_id = ${orgId}\n and pr.org_user_id = ${id}\n and pr.target_type = 'activity'\n and pr.target_id = a.id\n group by cc.id, cc.title\n order by cc.title\n `);\n\n const courses: StudentCourseProgress[] = (\n result.rows as {\n course_id: string;\n title: string;\n total_activities: number;\n completed_activities: number;\n started_at: Date | string | null;\n last_activity_at: Date | string | null;\n completed_at: Date | string | null;\n }[]\n ).map((r) => {\n const total = Number(r.total_activities);\n const done = Number(r.completed_activities);\n return {\n courseId: r.course_id,\n title: r.title,\n totalActivities: total,\n completedActivities: done,\n progress: total > 0 ? Math.round(Math.min(done / total, 1) * 100) : 0,\n startedAt: r.started_at ? new Date(r.started_at).toISOString() : null,\n lastActivityAt: r.last_activity_at ? new Date(r.last_activity_at).toISOString() : null,\n completedAt: r.completed_at ? new Date(r.completed_at).toISOString() : null,\n };\n });\n\n const started = courses.filter((c) => c.startedAt !== null).length;\n const completed = courses.filter((c) => c.completedAt !== null).length;\n const avgProgress =\n courses.length > 0\n ? Math.round(courses.reduce((sum, c) => sum + c.progress, 0) / courses.length)\n : 0;\n\n return { enrolled: courses.length, started, completed, avgProgress, courses };\n }\n\n private resolveOrder(sort?: string): SQL[] {\n const descending = sort?.startsWith(\"-\") ?? false;\n const field = sort ? (descending ? sort.slice(1) : sort) : \"firstName\";\n const dir = descending ? desc : asc;\n switch (field) {\n case \"email\":\n return [dir(users.email)];\n case \"entitlementCount\":\n return [dir(entitlementCountExpr)];\n case \"avgProgress\":\n return [dir(avgProgressExpr)];\n case \"joinedAt\":\n return [dir(orgUsers.createdAt)];\n case \"lastActiveAt\":\n return [dir(lastActiveExpr)];\n case \"lastName\":\n return [dir(users.lastName), dir(users.firstName)];\n case \"firstName\":\n default:\n return [dir(users.firstName), dir(users.lastName)];\n }\n }\n}\ntranslateDbErrors(DrizzleStudentsRepository);\n"],"mappings":"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"}
@@ -173,6 +173,23 @@ declare const assets: import("drizzle-orm/pg-core").PgTableWithColumns<{
173
173
  identity: undefined;
174
174
  generated: undefined;
175
175
  }, {}, {}>;
176
+ updatedAt: import("drizzle-orm/pg-core").PgColumn<{
177
+ name: "updated_at";
178
+ tableName: "assets";
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
+ }, {}, {}>;
176
193
  };
177
194
  dialect: "pg";
178
195
  }>;
@@ -16,7 +16,8 @@ const assets = pgTable("assets", {
16
16
  size: bigint("size", { mode: "number" }).notNull().default(0),
17
17
  status: text("status", { enum: ["pending", "ready"] }).notNull().default("pending"),
18
18
  uploadedBy: text("uploaded_by").notNull(),
19
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
19
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
20
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow().$onUpdate(() => /* @__PURE__ */ new Date())
20
21
  }, (t) => ({ pk: primaryKey({ columns: [t.orgId, t.id] }) }));
21
22
  //#endregion
22
23
  export { assets };
@@ -1 +1 @@
1
- {"version":3,"file":"assets.js","names":[],"sources":["../../src/schema/assets.ts"],"sourcesContent":["// assets table — the org's media library. Org-scoped: composite (org_id, id) PK\n// with org_id → organizations.id, mirroring the multi-tenant table shape.\nimport { pgTable, text, bigint, timestamp, primaryKey } from 'drizzle-orm/pg-core';\nimport type { Asset } from '@headless-lms/core/schemas';\nimport { genId } from '@headless-lms/core/shared/id';\nimport { organizations } from './organizations.js';\nimport type { Expect, NoDrift } from './drift.js';\n\nexport const assets = pgTable(\n 'assets',\n {\n orgId: text('org_id')\n .notNull()\n .references(() => organizations.id),\n id: text('id')\n .notNull()\n .$defaultFn(() => genId('asset')),\n key: text('key').notNull(),\n kind: text('kind', { enum: ['video', 'download', 'content'] }).notNull(),\n filename: text('filename').notNull(),\n contentType: text('content_type').notNull(),\n // Bytes; bigint because media files exceed int4. 0 until upload confirmed.\n size: bigint('size', { mode: 'number' }).notNull().default(0),\n status: text('status', { enum: ['pending', 'ready'] })\n .notNull()\n .default('pending'),\n uploadedBy: text('uploaded_by').notNull(),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n },\n (t) => ({ pk: primaryKey({ columns: [t.orgId, t.id] }) }),\n);\n\ntype _AssetsDrift = Expect<NoDrift<typeof assets.$inferSelect, Asset>>;\n"],"mappings":";;;;AAQA,MAAa,SAAS,QACpB,UACA;CACE,OAAO,KAAK,QAAQ,CAAC,CAClB,QAAQ,CAAC,CACT,iBAAiB,cAAc,EAAE;CACpC,IAAI,KAAK,IAAI,CAAC,CACX,QAAQ,CAAC,CACT,iBAAiB,MAAM,OAAO,CAAC;CAClC,KAAK,KAAK,KAAK,CAAC,CAAC,QAAQ;CACzB,MAAM,KAAK,QAAQ,EAAE,MAAM;EAAC;EAAS;EAAY;CAAS,EAAE,CAAC,CAAC,CAAC,QAAQ;CACvE,UAAU,KAAK,UAAU,CAAC,CAAC,QAAQ;CACnC,aAAa,KAAK,cAAc,CAAC,CAAC,QAAQ;CAE1C,MAAM,OAAO,QAAQ,EAAE,MAAM,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC;CAC5D,QAAQ,KAAK,UAAU,EAAE,MAAM,CAAC,WAAW,OAAO,EAAE,CAAC,CAAC,CACnD,QAAQ,CAAC,CACT,QAAQ,SAAS;CACpB,YAAY,KAAK,aAAa,CAAC,CAAC,QAAQ;CACxC,WAAW,UAAU,cAAc,EAAE,cAAc,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,WAAW;AAClF,IACC,OAAO,EAAE,IAAI,WAAW,EAAE,SAAS,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,EAAE,EACzD"}
1
+ {"version":3,"file":"assets.js","names":[],"sources":["../../src/schema/assets.ts"],"sourcesContent":["// assets table — the org's media library. Org-scoped: composite (org_id, id) PK\n// with org_id → organizations.id, mirroring the multi-tenant table shape.\nimport { pgTable, text, bigint, timestamp, primaryKey } from \"drizzle-orm/pg-core\";\nimport type { Asset } from \"@headless-lms/core/schemas\";\nimport { genId } from \"@headless-lms/core/shared/id\";\nimport { organizations } from \"./organizations.js\";\nimport type { Expect, NoDrift } from \"./drift.js\";\n\nexport const assets = pgTable(\n \"assets\",\n {\n orgId: text(\"org_id\")\n .notNull()\n .references(() => organizations.id),\n id: text(\"id\")\n .notNull()\n .$defaultFn(() => genId(\"asset\")),\n key: text(\"key\").notNull(),\n kind: text(\"kind\", { enum: [\"video\", \"download\", \"content\"] }).notNull(),\n filename: text(\"filename\").notNull(),\n contentType: text(\"content_type\").notNull(),\n // Bytes; bigint because media files exceed int4. 0 until upload confirmed.\n size: bigint(\"size\", { mode: \"number\" }).notNull().default(0),\n status: text(\"status\", { enum: [\"pending\", \"ready\"] })\n .notNull()\n .default(\"pending\"),\n uploadedBy: text(\"uploaded_by\").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) => ({ pk: primaryKey({ columns: [t.orgId, t.id] }) }),\n);\n\ntype _AssetsDrift = Expect<NoDrift<typeof assets.$inferSelect, Asset>>;\n"],"mappings":";;;;AAQA,MAAa,SAAS,QACpB,UACA;CACE,OAAO,KAAK,QAAQ,CAAC,CAClB,QAAQ,CAAC,CACT,iBAAiB,cAAc,EAAE;CACpC,IAAI,KAAK,IAAI,CAAC,CACX,QAAQ,CAAC,CACT,iBAAiB,MAAM,OAAO,CAAC;CAClC,KAAK,KAAK,KAAK,CAAC,CAAC,QAAQ;CACzB,MAAM,KAAK,QAAQ,EAAE,MAAM;EAAC;EAAS;EAAY;CAAS,EAAE,CAAC,CAAC,CAAC,QAAQ;CACvE,UAAU,KAAK,UAAU,CAAC,CAAC,QAAQ;CACnC,aAAa,KAAK,cAAc,CAAC,CAAC,QAAQ;CAE1C,MAAM,OAAO,QAAQ,EAAE,MAAM,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC;CAC5D,QAAQ,KAAK,UAAU,EAAE,MAAM,CAAC,WAAW,OAAO,EAAE,CAAC,CAAC,CACnD,QAAQ,CAAC,CACT,QAAQ,SAAS;CACpB,YAAY,KAAK,aAAa,CAAC,CAAC,QAAQ;CACxC,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,EAAE,IAAI,WAAW,EAAE,SAAS,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,EAAE,EACzD"}
@@ -55,6 +55,23 @@ declare const automations: import("drizzle-orm/pg-core").PgTableWithColumns<{
55
55
  identity: undefined;
56
56
  generated: undefined;
57
57
  }, {}, {}>;
58
+ kind: import("drizzle-orm/pg-core").PgColumn<{
59
+ name: "kind";
60
+ tableName: "automations";
61
+ dataType: "string";
62
+ columnType: "PgText";
63
+ data: "workflow" | "webhook";
64
+ driverParam: string;
65
+ notNull: true;
66
+ hasDefault: true;
67
+ isPrimaryKey: false;
68
+ isAutoincrement: false;
69
+ hasRuntimeDefault: false;
70
+ enumValues: ["workflow", "webhook"];
71
+ baseColumn: never;
72
+ identity: undefined;
73
+ generated: undefined;
74
+ }, {}, {}>;
58
75
  description: import("drizzle-orm/pg-core").PgColumn<{
59
76
  name: "description";
60
77
  tableName: "automations";
@@ -72,23 +89,49 @@ declare const automations: import("drizzle-orm/pg-core").PgTableWithColumns<{
72
89
  identity: undefined;
73
90
  generated: undefined;
74
91
  }, {}, {}>;
75
- trigger: import("drizzle-orm/pg-core").PgColumn<{
76
- name: "trigger";
92
+ triggers: import("drizzle-orm/pg-core").PgColumn<{
93
+ name: "triggers";
77
94
  tableName: "automations";
78
- dataType: "string";
79
- columnType: "PgText";
80
- data: string;
81
- driverParam: string;
95
+ dataType: "array";
96
+ columnType: "PgArray";
97
+ data: string[];
98
+ driverParam: string | string[];
82
99
  notNull: true;
83
100
  hasDefault: false;
84
101
  isPrimaryKey: false;
85
102
  isAutoincrement: false;
86
103
  hasRuntimeDefault: false;
87
104
  enumValues: [string, ...string[]];
88
- baseColumn: never;
105
+ baseColumn: import("drizzle-orm").Column<{
106
+ name: "triggers";
107
+ tableName: "automations";
108
+ dataType: "string";
109
+ columnType: "PgText";
110
+ data: string;
111
+ driverParam: string;
112
+ notNull: false;
113
+ hasDefault: false;
114
+ isPrimaryKey: false;
115
+ isAutoincrement: false;
116
+ hasRuntimeDefault: false;
117
+ enumValues: [string, ...string[]];
118
+ baseColumn: never;
119
+ identity: undefined;
120
+ generated: undefined;
121
+ }, {}, {}>;
89
122
  identity: undefined;
90
123
  generated: undefined;
91
- }, {}, {}>;
124
+ }, {}, {
125
+ size: undefined;
126
+ baseBuilder: import("drizzle-orm/pg-core").PgColumnBuilder<{
127
+ name: "triggers";
128
+ dataType: "string";
129
+ columnType: "PgText";
130
+ data: string;
131
+ enumValues: [string, ...string[]];
132
+ driverParam: string;
133
+ }, {}, {}, import("drizzle-orm").ColumnBuilderExtraConfig>;
134
+ }>;
92
135
  actions: import("drizzle-orm/pg-core").PgColumn<{
93
136
  name: "actions";
94
137
  tableName: "automations";
@@ -356,6 +399,40 @@ declare const automationRuns: import("drizzle-orm/pg-core").PgTableWithColumns<{
356
399
  identity: undefined;
357
400
  generated: undefined;
358
401
  }, {}, {}>;
402
+ createdAt: import("drizzle-orm/pg-core").PgColumn<{
403
+ name: "created_at";
404
+ tableName: "automation_runs";
405
+ dataType: "date";
406
+ columnType: "PgTimestamp";
407
+ data: Date;
408
+ driverParam: string;
409
+ notNull: true;
410
+ hasDefault: true;
411
+ isPrimaryKey: false;
412
+ isAutoincrement: false;
413
+ hasRuntimeDefault: false;
414
+ enumValues: undefined;
415
+ baseColumn: never;
416
+ identity: undefined;
417
+ generated: undefined;
418
+ }, {}, {}>;
419
+ updatedAt: import("drizzle-orm/pg-core").PgColumn<{
420
+ name: "updated_at";
421
+ tableName: "automation_runs";
422
+ dataType: "date";
423
+ columnType: "PgTimestamp";
424
+ data: Date;
425
+ driverParam: string;
426
+ notNull: true;
427
+ hasDefault: true;
428
+ isPrimaryKey: false;
429
+ isAutoincrement: false;
430
+ hasRuntimeDefault: false;
431
+ enumValues: undefined;
432
+ baseColumn: never;
433
+ identity: undefined;
434
+ generated: undefined;
435
+ }, {}, {}>;
359
436
  };
360
437
  dialect: "pg";
361
438
  }>;
@@ -6,15 +6,16 @@ const automations = pgTable("automations", {
6
6
  orgId: text("org_id").notNull().references(() => organizations.id),
7
7
  id: text("id").notNull().$defaultFn(() => genId("automation")),
8
8
  name: text("name").notNull(),
9
+ kind: text("kind", { enum: ["workflow", "webhook"] }).notNull().default("workflow"),
9
10
  description: text("description"),
10
- trigger: text("trigger").notNull(),
11
+ triggers: text("triggers").array().notNull(),
11
12
  actions: jsonb("actions").$type().notNull(),
12
13
  enabled: boolean("enabled").notNull().default(true),
13
14
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
14
- updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
15
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow().$onUpdate(() => /* @__PURE__ */ new Date())
15
16
  }, (t) => ({
16
17
  pk: primaryKey({ columns: [t.orgId, t.id] }),
17
- triggerIdx: index("automations_org_trigger_idx").on(t.orgId, t.trigger)
18
+ triggersIdx: index("automations_triggers_idx").using("gin", t.triggers)
18
19
  }));
19
20
  const automationRuns = pgTable("automation_runs", {
20
21
  orgId: text("org_id").notNull().references(() => organizations.id),
@@ -30,7 +31,9 @@ const automationRuns = pgTable("automation_runs", {
30
31
  ] }).notNull(),
31
32
  actionResults: jsonb("action_results").$type().notNull().default([]),
32
33
  startedAt: timestamp("started_at", { withTimezone: true }).notNull(),
33
- finishedAt: timestamp("finished_at", { withTimezone: true })
34
+ finishedAt: timestamp("finished_at", { withTimezone: true }),
35
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
36
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow().$onUpdate(() => /* @__PURE__ */ new Date())
34
37
  }, (t) => ({
35
38
  pk: primaryKey({ columns: [t.orgId, t.id] }),
36
39
  automationIdx: index("automation_runs_org_automation_idx").on(t.orgId, t.automationId),
@@ -1 +1 @@
1
- {"version":3,"file":"automations.js","names":[],"sources":["../../src/schema/automations.ts"],"sourcesContent":["// automations tables — org-scoped automation definitions and their run\n// history. `automations.actions` and `automation_runs.action_results` are\n// ordered jsonb blobs (contract types owned by @headless-lms/core/types); a run's\n// `event` is the triggering DomainEvent snapshot, stored verbatim.\nimport { pgTable, text, boolean, jsonb, timestamp, primaryKey, index, uniqueIndex } from 'drizzle-orm/pg-core';\nimport { genId } from '@headless-lms/core/shared/id';\nimport { organizations } from './organizations.js';\nimport type {\n Automation,\n AutomationAction,\n AutomationActionResult,\n AutomationRun,\n} from '@headless-lms/core/schemas';\nimport type { DomainEvent } from '@headless-lms/core/shared/ports';\nimport type { Expect, NoDrift } from './drift.js';\n\nexport const automations = pgTable(\n 'automations',\n {\n orgId: text('org_id')\n .notNull()\n .references(() => organizations.id),\n id: text('id')\n .notNull()\n .$defaultFn(() => genId('automation')),\n name: text('name').notNull(),\n description: text('description'),\n trigger: text('trigger').notNull(),\n actions: jsonb('actions').$type<AutomationAction[]>().notNull(),\n enabled: boolean('enabled').notNull().default(true),\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 triggerIdx: index('automations_org_trigger_idx').on(t.orgId, t.trigger),\n }),\n);\n\nexport const automationRuns = pgTable(\n 'automation_runs',\n {\n orgId: text('org_id')\n .notNull()\n .references(() => organizations.id),\n id: text('id')\n .notNull()\n .$defaultFn(() => genId('automationRun')),\n automationId: text('automation_id').notNull(),\n trigger: text('trigger').notNull(),\n // At-least-once dedupe key for the triggering event; DB-internal, not part of the domain `AutomationRun` type.\n eventId: text('event_id').notNull(),\n event: jsonb('event').$type<DomainEvent>().notNull(),\n status: text('status', { enum: ['running', 'completed', 'failed'] }).notNull(),\n actionResults: jsonb('action_results').$type<AutomationActionResult[]>().notNull().default([]),\n startedAt: timestamp('started_at', { withTimezone: true }).notNull(),\n finishedAt: timestamp('finished_at', { withTimezone: true }),\n },\n (t) => ({\n pk: primaryKey({ columns: [t.orgId, t.id] }),\n automationIdx: index('automation_runs_org_automation_idx').on(t.orgId, t.automationId),\n eventDedupeIdx: uniqueIndex('automation_runs_org_automation_event_idx').on(\n t.orgId,\n t.automationId,\n t.eventId,\n ),\n }),\n);\n\ntype _AutomationsDrift = Expect<NoDrift<typeof automations.$inferSelect, Automation>>;\ntype _AutomationRunsDrift = Expect<NoDrift<typeof automationRuns.$inferSelect, AutomationRun>>;\n"],"mappings":";;;;AAgBA,MAAa,cAAc,QACzB,eACA;CACE,OAAO,KAAK,QAAQ,CAAC,CAClB,QAAQ,CAAC,CACT,iBAAiB,cAAc,EAAE;CACpC,IAAI,KAAK,IAAI,CAAC,CACX,QAAQ,CAAC,CACT,iBAAiB,MAAM,YAAY,CAAC;CACvC,MAAM,KAAK,MAAM,CAAC,CAAC,QAAQ;CAC3B,aAAa,KAAK,aAAa;CAC/B,SAAS,KAAK,SAAS,CAAC,CAAC,QAAQ;CACjC,SAAS,MAAM,SAAS,CAAC,CAAC,MAA0B,CAAC,CAAC,QAAQ;CAC9D,SAAS,QAAQ,SAAS,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,IAAI;CAClD,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,YAAY,MAAM,6BAA6B,CAAC,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO;AACxE,EACF;AAEA,MAAa,iBAAiB,QAC5B,mBACA;CACE,OAAO,KAAK,QAAQ,CAAC,CAClB,QAAQ,CAAC,CACT,iBAAiB,cAAc,EAAE;CACpC,IAAI,KAAK,IAAI,CAAC,CACX,QAAQ,CAAC,CACT,iBAAiB,MAAM,eAAe,CAAC;CAC1C,cAAc,KAAK,eAAe,CAAC,CAAC,QAAQ;CAC5C,SAAS,KAAK,SAAS,CAAC,CAAC,QAAQ;CAEjC,SAAS,KAAK,UAAU,CAAC,CAAC,QAAQ;CAClC,OAAO,MAAM,OAAO,CAAC,CAAC,MAAmB,CAAC,CAAC,QAAQ;CACnD,QAAQ,KAAK,UAAU,EAAE,MAAM;EAAC;EAAW;EAAa;CAAQ,EAAE,CAAC,CAAC,CAAC,QAAQ;CAC7E,eAAe,MAAM,gBAAgB,CAAC,CAAC,MAAgC,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC,CAAC;CAC7F,WAAW,UAAU,cAAc,EAAE,cAAc,KAAK,CAAC,CAAC,CAAC,QAAQ;CACnE,YAAY,UAAU,eAAe,EAAE,cAAc,KAAK,CAAC;AAC7D,IACC,OAAO;CACN,IAAI,WAAW,EAAE,SAAS,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;CAC3C,eAAe,MAAM,oCAAoC,CAAC,CAAC,GAAG,EAAE,OAAO,EAAE,YAAY;CACrF,gBAAgB,YAAY,0CAA0C,CAAC,CAAC,GACtE,EAAE,OACF,EAAE,cACF,EAAE,OACJ;AACF,EACF"}
1
+ {"version":3,"file":"automations.js","names":[],"sources":["../../src/schema/automations.ts"],"sourcesContent":["// automations tables — org-scoped automation definitions and their run\n// history. `automations.actions` and `automation_runs.action_results` are\n// ordered jsonb blobs (contract types owned by @headless-lms/core/types); a run's\n// `event` is the triggering DomainEvent snapshot, stored verbatim.\nimport {\n pgTable,\n text,\n boolean,\n jsonb,\n timestamp,\n primaryKey,\n index,\n uniqueIndex,\n} from \"drizzle-orm/pg-core\";\nimport { genId } from \"@headless-lms/core/shared/id\";\nimport { organizations } from \"./organizations.js\";\nimport type {\n Automation,\n AutomationAction,\n AutomationActionResult,\n AutomationRun,\n} from \"@headless-lms/core/schemas\";\nimport type { DomainEvent } from \"@headless-lms/core/shared/ports\";\nimport type { Expect, NoDrift } from \"./drift.js\";\n\nexport const automations = pgTable(\n \"automations\",\n {\n orgId: text(\"org_id\")\n .notNull()\n .references(() => organizations.id),\n id: text(\"id\")\n .notNull()\n .$defaultFn(() => genId(\"automation\")),\n name: text(\"name\").notNull(),\n kind: text(\"kind\", { enum: [\"workflow\", \"webhook\"] })\n .notNull()\n .default(\"workflow\"),\n description: text(\"description\"),\n triggers: text(\"triggers\").array().notNull(),\n actions: jsonb(\"actions\").$type<AutomationAction[]>().notNull(),\n enabled: boolean(\"enabled\").notNull().default(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 triggersIdx: index(\"automations_triggers_idx\").using(\"gin\", t.triggers),\n }),\n);\n\nexport const automationRuns = pgTable(\n \"automation_runs\",\n {\n orgId: text(\"org_id\")\n .notNull()\n .references(() => organizations.id),\n id: text(\"id\")\n .notNull()\n .$defaultFn(() => genId(\"automationRun\")),\n automationId: text(\"automation_id\").notNull(),\n trigger: text(\"trigger\").notNull(),\n // At-least-once dedupe key for the triggering event; DB-internal, not part of the domain `AutomationRun` type.\n eventId: text(\"event_id\").notNull(),\n event: jsonb(\"event\").$type<DomainEvent>().notNull(),\n status: text(\"status\", { enum: [\"running\", \"completed\", \"failed\"] }).notNull(),\n actionResults: jsonb(\"action_results\").$type<AutomationActionResult[]>().notNull().default([]),\n startedAt: timestamp(\"started_at\", { withTimezone: true }).notNull(),\n finishedAt: timestamp(\"finished_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 automationIdx: index(\"automation_runs_org_automation_idx\").on(t.orgId, t.automationId),\n eventDedupeIdx: uniqueIndex(\"automation_runs_org_automation_event_idx\").on(\n t.orgId,\n t.automationId,\n t.eventId,\n ),\n }),\n);\n\ntype _AutomationsDrift = Expect<NoDrift<typeof automations.$inferSelect, Automation>>;\ntype _AutomationRunsDrift = Expect<NoDrift<typeof automationRuns.$inferSelect, AutomationRun>>;\n"],"mappings":";;;;AAyBA,MAAa,cAAc,QACzB,eACA;CACE,OAAO,KAAK,QAAQ,CAAC,CAClB,QAAQ,CAAC,CACT,iBAAiB,cAAc,EAAE;CACpC,IAAI,KAAK,IAAI,CAAC,CACX,QAAQ,CAAC,CACT,iBAAiB,MAAM,YAAY,CAAC;CACvC,MAAM,KAAK,MAAM,CAAC,CAAC,QAAQ;CAC3B,MAAM,KAAK,QAAQ,EAAE,MAAM,CAAC,YAAY,SAAS,EAAE,CAAC,CAAC,CAClD,QAAQ,CAAC,CACT,QAAQ,UAAU;CACrB,aAAa,KAAK,aAAa;CAC/B,UAAU,KAAK,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC,QAAQ;CAC3C,SAAS,MAAM,SAAS,CAAC,CAAC,MAA0B,CAAC,CAAC,QAAQ;CAC9D,SAAS,QAAQ,SAAS,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,IAAI;CAClD,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,aAAa,MAAM,0BAA0B,CAAC,CAAC,MAAM,OAAO,EAAE,QAAQ;AACxE,EACF;AAEA,MAAa,iBAAiB,QAC5B,mBACA;CACE,OAAO,KAAK,QAAQ,CAAC,CAClB,QAAQ,CAAC,CACT,iBAAiB,cAAc,EAAE;CACpC,IAAI,KAAK,IAAI,CAAC,CACX,QAAQ,CAAC,CACT,iBAAiB,MAAM,eAAe,CAAC;CAC1C,cAAc,KAAK,eAAe,CAAC,CAAC,QAAQ;CAC5C,SAAS,KAAK,SAAS,CAAC,CAAC,QAAQ;CAEjC,SAAS,KAAK,UAAU,CAAC,CAAC,QAAQ;CAClC,OAAO,MAAM,OAAO,CAAC,CAAC,MAAmB,CAAC,CAAC,QAAQ;CACnD,QAAQ,KAAK,UAAU,EAAE,MAAM;EAAC;EAAW;EAAa;CAAQ,EAAE,CAAC,CAAC,CAAC,QAAQ;CAC7E,eAAe,MAAM,gBAAgB,CAAC,CAAC,MAAgC,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC,CAAC;CAC7F,WAAW,UAAU,cAAc,EAAE,cAAc,KAAK,CAAC,CAAC,CAAC,QAAQ;CACnE,YAAY,UAAU,eAAe,EAAE,cAAc,KAAK,CAAC;CAC3D,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,eAAe,MAAM,oCAAoC,CAAC,CAAC,GAAG,EAAE,OAAO,EAAE,YAAY;CACrF,gBAAgB,YAAY,0CAA0C,CAAC,CAAC,GACtE,EAAE,OACF,EAAE,cACF,EAAE,OACJ;AACF,EACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"better-auth.js","names":[],"sources":["../../src/schema/better-auth.ts"],"sourcesContent":["// Better Auth's own tables. Every physical table is prefixed `ba_` so the auth\n// engine's storage is visibly separate from the domain's own tables (`users`,\n// `organizations`, …) it is mirrored into. The exported identifiers stay the\n// better-auth model names — that is the key `drizzleAdapter` looks each model\n// up by, so it must not carry the prefix.\nimport { pgTable, text, timestamp, boolean, index } from 'drizzle-orm/pg-core';\n\nexport const user = pgTable('ba_user', {\n id: text('id').primaryKey(),\n name: text('name').notNull(),\n email: text('email').notNull().unique(),\n emailVerified: boolean('email_verified')\n .$defaultFn(() => false)\n .notNull(),\n image: text('image'),\n createdAt: timestamp('created_at', { withTimezone: true })\n .$defaultFn(() => new Date())\n .notNull(),\n updatedAt: timestamp('updated_at', { withTimezone: true })\n .$defaultFn(() => new Date())\n .notNull(),\n});\n\nexport const session = pgTable('ba_session', {\n id: text('id').primaryKey(),\n expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),\n token: text('token').notNull().unique(),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull(),\n updatedAt: timestamp('updated_at', { withTimezone: true }).notNull(),\n ipAddress: text('ip_address'),\n userAgent: text('user_agent'),\n userId: text('user_id')\n .notNull()\n .references(() => user.id, { onDelete: 'cascade' }),\n // organization plugin: the org the session is currently acting in.\n activeOrganizationId: text('active_organization_id'),\n});\n\nexport const account = pgTable('ba_account', {\n id: text('id').primaryKey(),\n accountId: text('account_id').notNull(),\n providerId: text('provider_id').notNull(),\n userId: text('user_id')\n .notNull()\n .references(() => user.id, { onDelete: 'cascade' }),\n accessToken: text('access_token'),\n refreshToken: text('refresh_token'),\n idToken: text('id_token'),\n accessTokenExpiresAt: timestamp('access_token_expires_at', { withTimezone: true }),\n refreshTokenExpiresAt: timestamp('refresh_token_expires_at', { withTimezone: true }),\n scope: text('scope'),\n password: text('password'),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull(),\n updatedAt: timestamp('updated_at', { withTimezone: true }).notNull(),\n});\n\nexport const verification = pgTable('ba_verification', {\n id: text('id').primaryKey(),\n identifier: text('identifier').notNull(),\n value: text('value').notNull(),\n expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),\n createdAt: timestamp('created_at', { withTimezone: true }).$defaultFn(() => new Date()),\n updatedAt: timestamp('updated_at', { withTimezone: true }).$defaultFn(() => new Date()),\n});\n\n// --- organization plugin tables ---\n\nexport const organization = pgTable('ba_organization', {\n id: text('id').primaryKey(),\n name: text('name').notNull(),\n slug: text('slug').notNull().unique(),\n logo: text('logo'),\n metadata: text('metadata'),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull(),\n updatedAt: timestamp('updated_at', { withTimezone: true }),\n});\n\nexport const member = pgTable('ba_member', {\n id: text('id').primaryKey(),\n organizationId: text('organization_id')\n .notNull()\n .references(() => organization.id, { onDelete: 'cascade' }),\n userId: text('user_id')\n .notNull()\n .references(() => user.id, { onDelete: 'cascade' }),\n role: text('role').notNull().default('member'),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull(),\n});\n\nexport const invitation = pgTable('ba_invitation', {\n id: text('id').primaryKey(),\n organizationId: text('organization_id')\n .notNull()\n .references(() => organization.id, { onDelete: 'cascade' }),\n email: text('email').notNull(),\n role: text('role').notNull(),\n status: text('status').notNull().default('pending'),\n expiresAt: timestamp('expires_at', { withTimezone: true }),\n inviterId: text('inviter_id')\n .notNull()\n .references(() => user.id, { onDelete: 'cascade' }),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull(),\n});\n\n// --- OAuth 2.1 provider tables (MCP). Owned by @better-auth/oauth-provider. ---\n// `reference_id` is the plugin's tenant slot: the organization the person chose\n// at consent. It is the only thing that tells a token which org it acts in.\nexport const oauthClient = pgTable(\n 'ba_oauth_client',\n {\n id: text('id').primaryKey(),\n clientId: text('client_id').notNull().unique(),\n clientSecret: text('client_secret'),\n disabled: boolean('disabled').default(false),\n skipConsent: boolean('skip_consent'),\n enableEndSession: boolean('enable_end_session'),\n subjectType: text('subject_type'),\n scopes: text('scopes').array(),\n userId: text('user_id').references(() => user.id, { onDelete: 'cascade' }),\n name: text('name'),\n uri: text('uri'),\n icon: text('icon'),\n contacts: text('contacts').array(),\n tos: text('tos'),\n policy: text('policy'),\n softwareId: text('software_id'),\n softwareVersion: text('software_version'),\n softwareStatement: text('software_statement'),\n redirectUris: text('redirect_uris').array().notNull(),\n postLogoutRedirectUris: text('post_logout_redirect_uris').array(),\n tokenEndpointAuthMethod: text('token_endpoint_auth_method'),\n grantTypes: text('grant_types').array(),\n responseTypes: text('response_types').array(),\n public: boolean('public'),\n type: text('type'),\n requirePkce: boolean('require_pkce'),\n referenceId: text('reference_id'),\n metadata: text('metadata'),\n createdAt: timestamp('created_at', { withTimezone: true }),\n updatedAt: timestamp('updated_at', { withTimezone: true }),\n },\n (t) => [index('ba_oauth_client_user_id_idx').on(t.userId)],\n);\n\nexport const oauthRefreshToken = pgTable(\n 'ba_oauth_refresh_token',\n {\n id: text('id').primaryKey(),\n token: text('token').notNull().unique(),\n clientId: text('client_id')\n .notNull()\n .references(() => oauthClient.clientId),\n sessionId: text('session_id').references(() => session.id, { onDelete: 'set null' }),\n userId: text('user_id')\n .notNull()\n .references(() => user.id, { onDelete: 'cascade' }),\n referenceId: text('reference_id'),\n expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull(),\n revoked: timestamp('revoked', { withTimezone: true }),\n authTime: timestamp('auth_time', { withTimezone: true }),\n scopes: text('scopes').array().notNull(),\n },\n (t) => [\n index('ba_oauth_refresh_token_client_id_idx').on(t.clientId),\n index('ba_oauth_refresh_token_session_id_idx').on(t.sessionId),\n index('ba_oauth_refresh_token_user_id_idx').on(t.userId),\n ],\n);\n\nexport const oauthAccessToken = pgTable(\n 'ba_oauth_access_token',\n {\n id: text('id').primaryKey(),\n token: text('token').notNull().unique(),\n clientId: text('client_id')\n .notNull()\n .references(() => oauthClient.clientId),\n sessionId: text('session_id').references(() => session.id, { onDelete: 'set null' }),\n userId: text('user_id').references(() => user.id, { onDelete: 'cascade' }),\n referenceId: text('reference_id'),\n refreshId: text('refresh_id').references(() => oauthRefreshToken.id),\n expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull(),\n scopes: text('scopes').array().notNull(),\n },\n (t) => [\n index('ba_oauth_access_token_client_id_idx').on(t.clientId),\n index('ba_oauth_access_token_session_id_idx').on(t.sessionId),\n index('ba_oauth_access_token_user_id_idx').on(t.userId),\n index('ba_oauth_access_token_refresh_id_idx').on(t.refreshId),\n ],\n);\n\nexport const oauthConsent = pgTable(\n 'ba_oauth_consent',\n {\n id: text('id').primaryKey(),\n clientId: text('client_id')\n .notNull()\n .references(() => oauthClient.clientId),\n userId: text('user_id').references(() => user.id, { onDelete: 'cascade' }),\n referenceId: text('reference_id'),\n scopes: text('scopes').array().notNull(),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull(),\n updatedAt: timestamp('updated_at', { withTimezone: true }).notNull(),\n },\n (t) => [\n index('ba_oauth_consent_client_id_idx').on(t.clientId),\n index('ba_oauth_consent_user_id_idx').on(t.userId),\n ],\n);\n\n// better-auth's jwt plugin: the signing keys behind JWT access tokens.\nexport const jwks = pgTable('ba_jwks', {\n id: text('id').primaryKey(),\n publicKey: text('public_key').notNull(),\n privateKey: text('private_key').notNull(),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull(),\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"better-auth.js","names":[],"sources":["../../src/schema/better-auth.ts"],"sourcesContent":["// Better Auth's own tables. Every physical table is prefixed `ba_` so the auth\n// engine's storage is visibly separate from the domain's own tables (`users`,\n// `organizations`, …) it is mirrored into. The exported identifiers stay the\n// better-auth model names — that is the key `drizzleAdapter` looks each model\n// up by, so it must not carry the prefix.\nimport { pgTable, text, timestamp, boolean, index } from \"drizzle-orm/pg-core\";\n\nexport const user = pgTable(\"ba_user\", {\n id: text(\"id\").primaryKey(),\n name: text(\"name\").notNull(),\n email: text(\"email\").notNull().unique(),\n emailVerified: boolean(\"email_verified\")\n .$defaultFn(() => false)\n .notNull(),\n image: text(\"image\"),\n createdAt: timestamp(\"created_at\", { withTimezone: true })\n .$defaultFn(() => new Date())\n .notNull(),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true })\n .$defaultFn(() => new Date())\n .notNull(),\n});\n\nexport const session = pgTable(\"ba_session\", {\n id: text(\"id\").primaryKey(),\n expiresAt: timestamp(\"expires_at\", { withTimezone: true }).notNull(),\n token: text(\"token\").notNull().unique(),\n createdAt: timestamp(\"created_at\", { withTimezone: true }).notNull(),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true }).notNull(),\n ipAddress: text(\"ip_address\"),\n userAgent: text(\"user_agent\"),\n userId: text(\"user_id\")\n .notNull()\n .references(() => user.id, { onDelete: \"cascade\" }),\n // organization plugin: the org the session is currently acting in.\n activeOrganizationId: text(\"active_organization_id\"),\n});\n\nexport const account = pgTable(\"ba_account\", {\n id: text(\"id\").primaryKey(),\n accountId: text(\"account_id\").notNull(),\n providerId: text(\"provider_id\").notNull(),\n userId: text(\"user_id\")\n .notNull()\n .references(() => user.id, { onDelete: \"cascade\" }),\n accessToken: text(\"access_token\"),\n refreshToken: text(\"refresh_token\"),\n idToken: text(\"id_token\"),\n accessTokenExpiresAt: timestamp(\"access_token_expires_at\", { withTimezone: true }),\n refreshTokenExpiresAt: timestamp(\"refresh_token_expires_at\", { withTimezone: true }),\n scope: text(\"scope\"),\n password: text(\"password\"),\n createdAt: timestamp(\"created_at\", { withTimezone: true }).notNull(),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true }).notNull(),\n});\n\nexport const verification = pgTable(\"ba_verification\", {\n id: text(\"id\").primaryKey(),\n identifier: text(\"identifier\").notNull(),\n value: text(\"value\").notNull(),\n expiresAt: timestamp(\"expires_at\", { withTimezone: true }).notNull(),\n createdAt: timestamp(\"created_at\", { withTimezone: true }).$defaultFn(() => new Date()),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true }).$defaultFn(() => new Date()),\n});\n\n// --- organization plugin tables ---\n\nexport const organization = pgTable(\"ba_organization\", {\n id: text(\"id\").primaryKey(),\n name: text(\"name\").notNull(),\n slug: text(\"slug\").notNull().unique(),\n logo: text(\"logo\"),\n metadata: text(\"metadata\"),\n createdAt: timestamp(\"created_at\", { withTimezone: true }).notNull(),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true }),\n});\n\nexport const member = pgTable(\"ba_member\", {\n id: text(\"id\").primaryKey(),\n organizationId: text(\"organization_id\")\n .notNull()\n .references(() => organization.id, { onDelete: \"cascade\" }),\n userId: text(\"user_id\")\n .notNull()\n .references(() => user.id, { onDelete: \"cascade\" }),\n role: text(\"role\").notNull().default(\"member\"),\n createdAt: timestamp(\"created_at\", { withTimezone: true }).notNull(),\n});\n\nexport const invitation = pgTable(\"ba_invitation\", {\n id: text(\"id\").primaryKey(),\n organizationId: text(\"organization_id\")\n .notNull()\n .references(() => organization.id, { onDelete: \"cascade\" }),\n email: text(\"email\").notNull(),\n role: text(\"role\").notNull(),\n status: text(\"status\").notNull().default(\"pending\"),\n expiresAt: timestamp(\"expires_at\", { withTimezone: true }),\n inviterId: text(\"inviter_id\")\n .notNull()\n .references(() => user.id, { onDelete: \"cascade\" }),\n createdAt: timestamp(\"created_at\", { withTimezone: true }).notNull(),\n});\n\n// --- OAuth 2.1 provider tables (MCP). Owned by @better-auth/oauth-provider. ---\n// `reference_id` is the plugin's tenant slot: the organization the person chose\n// at consent. It is the only thing that tells a token which org it acts in.\nexport const oauthClient = pgTable(\n \"ba_oauth_client\",\n {\n id: text(\"id\").primaryKey(),\n clientId: text(\"client_id\").notNull().unique(),\n clientSecret: text(\"client_secret\"),\n disabled: boolean(\"disabled\").default(false),\n skipConsent: boolean(\"skip_consent\"),\n enableEndSession: boolean(\"enable_end_session\"),\n subjectType: text(\"subject_type\"),\n scopes: text(\"scopes\").array(),\n userId: text(\"user_id\").references(() => user.id, { onDelete: \"cascade\" }),\n name: text(\"name\"),\n uri: text(\"uri\"),\n icon: text(\"icon\"),\n contacts: text(\"contacts\").array(),\n tos: text(\"tos\"),\n policy: text(\"policy\"),\n softwareId: text(\"software_id\"),\n softwareVersion: text(\"software_version\"),\n softwareStatement: text(\"software_statement\"),\n redirectUris: text(\"redirect_uris\").array().notNull(),\n postLogoutRedirectUris: text(\"post_logout_redirect_uris\").array(),\n tokenEndpointAuthMethod: text(\"token_endpoint_auth_method\"),\n grantTypes: text(\"grant_types\").array(),\n responseTypes: text(\"response_types\").array(),\n public: boolean(\"public\"),\n type: text(\"type\"),\n requirePkce: boolean(\"require_pkce\"),\n referenceId: text(\"reference_id\"),\n metadata: text(\"metadata\"),\n createdAt: timestamp(\"created_at\", { withTimezone: true }),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true }),\n },\n (t) => [index(\"ba_oauth_client_user_id_idx\").on(t.userId)],\n);\n\nexport const oauthRefreshToken = pgTable(\n \"ba_oauth_refresh_token\",\n {\n id: text(\"id\").primaryKey(),\n token: text(\"token\").notNull().unique(),\n clientId: text(\"client_id\")\n .notNull()\n .references(() => oauthClient.clientId),\n sessionId: text(\"session_id\").references(() => session.id, { onDelete: \"set null\" }),\n userId: text(\"user_id\")\n .notNull()\n .references(() => user.id, { onDelete: \"cascade\" }),\n referenceId: text(\"reference_id\"),\n expiresAt: timestamp(\"expires_at\", { withTimezone: true }).notNull(),\n createdAt: timestamp(\"created_at\", { withTimezone: true }).notNull(),\n revoked: timestamp(\"revoked\", { withTimezone: true }),\n authTime: timestamp(\"auth_time\", { withTimezone: true }),\n scopes: text(\"scopes\").array().notNull(),\n },\n (t) => [\n index(\"ba_oauth_refresh_token_client_id_idx\").on(t.clientId),\n index(\"ba_oauth_refresh_token_session_id_idx\").on(t.sessionId),\n index(\"ba_oauth_refresh_token_user_id_idx\").on(t.userId),\n ],\n);\n\nexport const oauthAccessToken = pgTable(\n \"ba_oauth_access_token\",\n {\n id: text(\"id\").primaryKey(),\n token: text(\"token\").notNull().unique(),\n clientId: text(\"client_id\")\n .notNull()\n .references(() => oauthClient.clientId),\n sessionId: text(\"session_id\").references(() => session.id, { onDelete: \"set null\" }),\n userId: text(\"user_id\").references(() => user.id, { onDelete: \"cascade\" }),\n referenceId: text(\"reference_id\"),\n refreshId: text(\"refresh_id\").references(() => oauthRefreshToken.id),\n expiresAt: timestamp(\"expires_at\", { withTimezone: true }).notNull(),\n createdAt: timestamp(\"created_at\", { withTimezone: true }).notNull(),\n scopes: text(\"scopes\").array().notNull(),\n },\n (t) => [\n index(\"ba_oauth_access_token_client_id_idx\").on(t.clientId),\n index(\"ba_oauth_access_token_session_id_idx\").on(t.sessionId),\n index(\"ba_oauth_access_token_user_id_idx\").on(t.userId),\n index(\"ba_oauth_access_token_refresh_id_idx\").on(t.refreshId),\n ],\n);\n\nexport const oauthConsent = pgTable(\n \"ba_oauth_consent\",\n {\n id: text(\"id\").primaryKey(),\n clientId: text(\"client_id\")\n .notNull()\n .references(() => oauthClient.clientId),\n userId: text(\"user_id\").references(() => user.id, { onDelete: \"cascade\" }),\n referenceId: text(\"reference_id\"),\n scopes: text(\"scopes\").array().notNull(),\n createdAt: timestamp(\"created_at\", { withTimezone: true }).notNull(),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true }).notNull(),\n },\n (t) => [\n index(\"ba_oauth_consent_client_id_idx\").on(t.clientId),\n index(\"ba_oauth_consent_user_id_idx\").on(t.userId),\n ],\n);\n\n// better-auth's jwt plugin: the signing keys behind JWT access tokens.\nexport const jwks = pgTable(\"ba_jwks\", {\n id: text(\"id\").primaryKey(),\n publicKey: text(\"public_key\").notNull(),\n privateKey: text(\"private_key\").notNull(),\n createdAt: timestamp(\"created_at\", { withTimezone: true }).notNull(),\n});\n"],"mappings":"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"}