@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
package/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # @headless-lms/adapter-db
2
+
3
+ Drizzle/Postgres persistence adapter. Implements the outbound repository ports from
4
+ `@headless-lms/core` and ships the migrations for the LMS database.
5
+
6
+ It depends on `@headless-lms/core`, `drizzle-orm` and `pg` — never on the server or
7
+ any other adapter.
8
+
9
+ ## Layout
10
+
11
+ | Path | What lives there |
12
+ | --------------------- | -------------------------------------------------------------------------- |
13
+ | `src/schema/` | Drizzle tables, grouped by owning context, barrelled in `index.ts` |
14
+ | `src/repositories/` | One `Drizzle*Repository` per context port, plus pg error translation |
15
+ | `src/client.ts` | `createDb` — pool + drizzle instance; `Db`, `Tx`, `DbExecutor` types |
16
+ | `src/unit-of-work.ts` | `DrizzleUnitOfWork` — transaction scope for the core `UnitOfWork` port |
17
+ | `src/migrate.ts` | `runMigrations(databaseUrl)` against the packaged `drizzle/` folder |
18
+ | `drizzle/` | Generated SQL migrations and snapshots (committed, shipped in the package) |
19
+
20
+ ## Usage
21
+
22
+ ```ts
23
+ import { createDb, runMigrations, DrizzleProgressRepository } from "@headless-lms/adapter-db";
24
+
25
+ await runMigrations(process.env.DATABASE_URL!);
26
+
27
+ const db = createDb(process.env.DATABASE_URL!);
28
+ const progress = new DrizzleProgressRepository(db, logger);
29
+ ```
30
+
31
+ Repositories take a `DbExecutor`, so the same class works against the root `db` or a
32
+ transaction handle. `DrizzleUnitOfWork` builds a tx-bound scope, guaranteeing nothing
33
+ escapes the transaction:
34
+
35
+ ```ts
36
+ const uow = new DrizzleUnitOfWork(db, (tx) => ({
37
+ progress: new DrizzleProgressRepository(tx),
38
+ outbox: new DrizzleOutboxRepository(tx),
39
+ }));
40
+
41
+ await uow.run(async ({ progress, outbox }) => {
42
+ /* ... */
43
+ });
44
+ ```
45
+
46
+ ## Error translation
47
+
48
+ `repositories/pg-errors.ts` turns driver failures into domain errors — unique violations
49
+ become `ConflictError`, everything else becomes a `DbQueryError` carrying the SQL text and
50
+ structured pg fields but never the bind params (they can hold PII).
51
+
52
+ ## Drift checks
53
+
54
+ `schema/drift.ts` provides compile-time assertions that a table's row type matches its
55
+ domain type from `@headless-lms/core/schemas`. A row field may be wider than the domain's,
56
+ never narrower, missing, or extra. Drift fails `pnpm typecheck` with the offending keys.
57
+
58
+ ## Migration workflow
59
+
60
+ Push during dev, generate once at the end.
61
+
62
+ ```bash
63
+ pnpm db:push # apply schema changes straight to the dev database, no files
64
+ pnpm db:generate # diff against the last committed snapshot → one migration
65
+ pnpm db:migrate # apply pending migrations
66
+ ```
67
+
68
+ While iterating locally, use `db:push`. When the schema is settled and ready to ship, run
69
+ `db:generate` once — it diffs against the last committed snapshot, so you get a single
70
+ migration containing everything since the previous release.
71
+
72
+ All three read `DATABASE_URL` from the repo-root `.env`.
73
+
74
+ ## Tests
75
+
76
+ ```bash
77
+ pnpm test # vitest
78
+ pnpm typecheck
79
+ ```
package/dist/client.d.ts CHANGED
@@ -3,7 +3,7 @@ import { NodePgDatabase } from "drizzle-orm/node-postgres";
3
3
  //#region src/client.d.ts
4
4
  /** Transaction executor — the same query surface as the root db.
5
5
  * (Promoted from the structure.ts repository's local alias.) */
6
- type Tx = Parameters<Parameters<NodePgDatabase['transaction']>[0]>[0];
6
+ type Tx = Parameters<Parameters<NodePgDatabase["transaction"]>[0]>[0];
7
7
  /** What a Drizzle repository executes against: the root db OR a transaction
8
8
  * handle — tx-bound repo instances are how DrizzleUnitOfWork guarantees no
9
9
  * statement escapes the transaction. */
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":[],"sources":["../src/client.ts"],"sourcesContent":["// Drizzle client + connection. Owns connection pooling (Drizzle does not).\nimport { drizzle, type NodePgDatabase } from 'drizzle-orm/node-postgres';\nimport pg from 'pg';\n\n/** Transaction executor — the same query surface as the root db.\n * (Promoted from the structure.ts repository's local alias.) */\nexport type Tx = Parameters<Parameters<NodePgDatabase['transaction']>[0]>[0];\n\n/** What a Drizzle repository executes against: the root db OR a transaction\n * handle — tx-bound repo instances are how DrizzleUnitOfWork guarantees no\n * statement escapes the transaction. */\nexport type DbExecutor = NodePgDatabase | Tx;\n\n// Centralized schema — re-exported so the db adapter is the single source for tables.\nexport * as schema from './schema/index.js';\n\nexport type Db = ReturnType<typeof createDb>;\n\nexport function createDb(connectionString: string) {\n const pool = new pg.Pool({ connectionString });\n return drizzle(pool);\n}\n"],"mappings":";;;;AAkBA,SAAgB,SAAS,kBAA0B;CAEjD,OAAO,QAAQ,IADE,GAAG,KAAK,EAAE,iBAAiB,CAC1B,CAAC;AACrB"}
1
+ {"version":3,"file":"client.js","names":[],"sources":["../src/client.ts"],"sourcesContent":["// Drizzle client + connection. Owns connection pooling (Drizzle does not).\nimport { drizzle, type NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport pg from \"pg\";\n\n/** Transaction executor — the same query surface as the root db.\n * (Promoted from the structure.ts repository's local alias.) */\nexport type Tx = Parameters<Parameters<NodePgDatabase[\"transaction\"]>[0]>[0];\n\n/** What a Drizzle repository executes against: the root db OR a transaction\n * handle — tx-bound repo instances are how DrizzleUnitOfWork guarantees no\n * statement escapes the transaction. */\nexport type DbExecutor = NodePgDatabase | Tx;\n\n// Centralized schema — re-exported so the db adapter is the single source for tables.\nexport * as schema from \"./schema/index.js\";\n\nexport type Db = ReturnType<typeof createDb>;\n\nexport function createDb(connectionString: string) {\n const pool = new pg.Pool({ connectionString });\n return drizzle(pool);\n}\n"],"mappings":";;;;AAkBA,SAAgB,SAAS,kBAA0B;CAEjD,OAAO,QAAQ,IADE,GAAG,KAAK,EAAE,iBAAiB,CAC1B,CAAC;AACrB"}
package/dist/index.d.ts CHANGED
@@ -5,6 +5,7 @@ import { migrationsFolder, runMigrations } from "./migrate.js";
5
5
  import { DrizzleAssetsRepository } from "./repositories/assets.js";
6
6
  import { DrizzleAutomationRunsRepository, DrizzleAutomationsRepository } from "./repositories/automations.js";
7
7
  import { DrizzleContentRepository } from "./repositories/content.js";
8
+ import { DrizzleCourseAnalyticsRepository } from "./repositories/course-analytics.js";
8
9
  import { DrizzleCredentialStore, decryptSecret, encryptSecret } from "./repositories/credentials.js";
9
10
  import { DrizzleDashboardRepository } from "./repositories/dashboard.js";
10
11
  import { DrizzleDiscussionRepository } from "./repositories/discussion.js";
@@ -19,4 +20,4 @@ import { DrizzleOutboxAppender, DrizzleOutboxStore, OUTBOX_MAX_ATTEMPTS } from "
19
20
  import { DrizzleProgressRepository } from "./repositories/progress.js";
20
21
  import { DrizzleSettingsRepository, deepMerge } from "./repositories/settings.js";
21
22
  import { DrizzleStudentsRepository } from "./repositories/students.js";
22
- export { type Db, DrizzleAssetsRepository, DrizzleAutomationRunsRepository, DrizzleAutomationsRepository, DrizzleConnectionsRepository, DrizzleContentRepository, DrizzleCredentialStore, DrizzleDashboardRepository, DrizzleDiscussionRepository, DrizzleEntitlementsRepository, DrizzleIdentityRepository, DrizzleLearnRepository, DrizzleMembersRepository, DrizzleOrganizationsRepository, DrizzleOutboxAppender, DrizzleOutboxStore, DrizzleProgressRepository, DrizzleSettingsRepository, DrizzleStudentsRepository, DrizzleUnitOfWork, OUTBOX_MAX_ATTEMPTS, createDb, decryptSecret, deepMerge, encryptSecret, migrationsFolder, orgUserProfileColumns, runMigrations, index_d_exports as schema };
23
+ export { type Db, DrizzleAssetsRepository, DrizzleAutomationRunsRepository, DrizzleAutomationsRepository, DrizzleConnectionsRepository, DrizzleContentRepository, DrizzleCourseAnalyticsRepository, DrizzleCredentialStore, DrizzleDashboardRepository, DrizzleDiscussionRepository, DrizzleEntitlementsRepository, DrizzleIdentityRepository, DrizzleLearnRepository, DrizzleMembersRepository, DrizzleOrganizationsRepository, DrizzleOutboxAppender, DrizzleOutboxStore, DrizzleProgressRepository, DrizzleSettingsRepository, DrizzleStudentsRepository, DrizzleUnitOfWork, OUTBOX_MAX_ATTEMPTS, createDb, decryptSecret, deepMerge, encryptSecret, migrationsFolder, orgUserProfileColumns, runMigrations, index_d_exports as schema };
package/dist/index.js CHANGED
@@ -4,7 +4,9 @@ import { DrizzleUnitOfWork } from "./unit-of-work.js";
4
4
  import { migrationsFolder, runMigrations } from "./migrate.js";
5
5
  import { DrizzleAssetsRepository } from "./repositories/assets.js";
6
6
  import { DrizzleAutomationRunsRepository, DrizzleAutomationsRepository } from "./repositories/automations.js";
7
+ import { DrizzleSettingsRepository, deepMerge } from "./repositories/settings.js";
7
8
  import { DrizzleContentRepository } from "./repositories/content.js";
9
+ import { DrizzleCourseAnalyticsRepository } from "./repositories/course-analytics.js";
8
10
  import { DrizzleCredentialStore, decryptSecret, encryptSecret } from "./repositories/credentials.js";
9
11
  import { DrizzleDashboardRepository } from "./repositories/dashboard.js";
10
12
  import { orgUserProfileColumns } from "./repositories/org-user-profile.js";
@@ -17,6 +19,5 @@ import { DrizzleMembersRepository } from "./repositories/members.js";
17
19
  import { DrizzleOrganizationsRepository } from "./repositories/organizations.js";
18
20
  import { DrizzleOutboxAppender, DrizzleOutboxStore, OUTBOX_MAX_ATTEMPTS } from "./repositories/outbox.js";
19
21
  import { DrizzleProgressRepository } from "./repositories/progress.js";
20
- import { DrizzleSettingsRepository, deepMerge } from "./repositories/settings.js";
21
22
  import { DrizzleStudentsRepository } from "./repositories/students.js";
22
- export { DrizzleAssetsRepository, DrizzleAutomationRunsRepository, DrizzleAutomationsRepository, DrizzleConnectionsRepository, DrizzleContentRepository, DrizzleCredentialStore, DrizzleDashboardRepository, DrizzleDiscussionRepository, DrizzleEntitlementsRepository, DrizzleIdentityRepository, DrizzleLearnRepository, DrizzleMembersRepository, DrizzleOrganizationsRepository, DrizzleOutboxAppender, DrizzleOutboxStore, DrizzleProgressRepository, DrizzleSettingsRepository, DrizzleStudentsRepository, DrizzleUnitOfWork, OUTBOX_MAX_ATTEMPTS, createDb, decryptSecret, deepMerge, encryptSecret, migrationsFolder, orgUserProfileColumns, runMigrations, schema_exports as schema };
23
+ export { DrizzleAssetsRepository, DrizzleAutomationRunsRepository, DrizzleAutomationsRepository, DrizzleConnectionsRepository, DrizzleContentRepository, DrizzleCourseAnalyticsRepository, DrizzleCredentialStore, DrizzleDashboardRepository, DrizzleDiscussionRepository, DrizzleEntitlementsRepository, DrizzleIdentityRepository, DrizzleLearnRepository, DrizzleMembersRepository, DrizzleOrganizationsRepository, DrizzleOutboxAppender, DrizzleOutboxStore, DrizzleProgressRepository, DrizzleSettingsRepository, DrizzleStudentsRepository, DrizzleUnitOfWork, OUTBOX_MAX_ATTEMPTS, createDb, decryptSecret, deepMerge, encryptSecret, migrationsFolder, orgUserProfileColumns, runMigrations, schema_exports as schema };
@@ -1 +1 @@
1
- {"version":3,"file":"migrate.js","names":[],"sources":["../src/migrate.ts"],"sourcesContent":["// Runs the drizzle migrations shipped inside this package against the\n// installation's database. Fails loudly and legibly — this is the first\n// command every new installation runs.\nimport { fileURLToPath } from 'node:url';\nimport pg from 'pg';\nimport { drizzle } from 'drizzle-orm/node-postgres';\nimport { migrate } from 'drizzle-orm/node-postgres/migrator';\n\n/** drizzle/ sits at the package root: ../drizzle from src/ AND dist/. */\nexport function migrationsFolder(): string {\n return fileURLToPath(new URL('../drizzle/', import.meta.url));\n}\n\nexport async function runMigrations(databaseUrl: string): Promise<void> {\n if (!databaseUrl) {\n throw new Error(\n 'DATABASE_URL is not set. Put it in your .env (e.g. postgres://postgres:postgres@localhost:8005/headless_lms) and re-run.',\n );\n }\n const pool = new pg.Pool({ connectionString: databaseUrl, max: 1 });\n try {\n await migrate(drizzle(pool), { migrationsFolder: migrationsFolder() });\n } catch (err) {\n throw new Error(\n `Migration failed against ${databaseUrl.replace(/\\/\\/.*@/, '//***@')}: ${err instanceof Error ? err.message : String(err)}`,\n { cause: err },\n );\n } finally {\n await pool.end();\n }\n}\n"],"mappings":";;;;;;AASA,SAAgB,mBAA2B;CACzC,OAAO,cAAc,IAAI,IAAI,eAAe,OAAO,KAAK,GAAG,CAAC;AAC9D;AAEA,eAAsB,cAAc,aAAoC;CACtE,IAAI,CAAC,aACH,MAAM,IAAI,MACR,0HACF;CAEF,MAAM,OAAO,IAAI,GAAG,KAAK;EAAE,kBAAkB;EAAa,KAAK;CAAE,CAAC;CAClE,IAAI;EACF,MAAM,QAAQ,QAAQ,IAAI,GAAG,EAAE,kBAAkB,iBAAiB,EAAE,CAAC;CACvE,SAAS,KAAK;EACZ,MAAM,IAAI,MACR,4BAA4B,YAAY,QAAQ,WAAW,QAAQ,EAAE,IAAI,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,KACxH,EAAE,OAAO,IAAI,CACf;CACF,UAAU;EACR,MAAM,KAAK,IAAI;CACjB;AACF"}
1
+ {"version":3,"file":"migrate.js","names":[],"sources":["../src/migrate.ts"],"sourcesContent":["// Runs the drizzle migrations shipped inside this package against the\n// installation's database. Fails loudly and legibly — this is the first\n// command every new installation runs.\nimport { fileURLToPath } from \"node:url\";\nimport pg from \"pg\";\nimport { drizzle } from \"drizzle-orm/node-postgres\";\nimport { migrate } from \"drizzle-orm/node-postgres/migrator\";\n\n/** drizzle/ sits at the package root: ../drizzle from src/ AND dist/. */\nexport function migrationsFolder(): string {\n return fileURLToPath(new URL(\"../drizzle/\", import.meta.url));\n}\n\nexport async function runMigrations(databaseUrl: string): Promise<void> {\n if (!databaseUrl) {\n throw new Error(\n \"DATABASE_URL is not set. Put it in your .env (e.g. postgres://postgres:postgres@localhost:8005/headless_lms) and re-run.\",\n );\n }\n const pool = new pg.Pool({ connectionString: databaseUrl, max: 1 });\n try {\n await migrate(drizzle(pool), { migrationsFolder: migrationsFolder() });\n } catch (err) {\n throw new Error(\n `Migration failed against ${databaseUrl.replace(/\\/\\/.*@/, \"//***@\")}: ${err instanceof Error ? err.message : String(err)}`,\n { cause: err },\n );\n } finally {\n await pool.end();\n }\n}\n"],"mappings":";;;;;;AASA,SAAgB,mBAA2B;CACzC,OAAO,cAAc,IAAI,IAAI,eAAe,OAAO,KAAK,GAAG,CAAC;AAC9D;AAEA,eAAsB,cAAc,aAAoC;CACtE,IAAI,CAAC,aACH,MAAM,IAAI,MACR,0HACF;CAEF,MAAM,OAAO,IAAI,GAAG,KAAK;EAAE,kBAAkB;EAAa,KAAK;CAAE,CAAC;CAClE,IAAI;EACF,MAAM,QAAQ,QAAQ,IAAI,GAAG,EAAE,kBAAkB,iBAAiB,EAAE,CAAC;CACvE,SAAS,KAAK;EACZ,MAAM,IAAI,MACR,4BAA4B,YAAY,QAAQ,WAAW,QAAQ,EAAE,IAAI,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,KACxH,EAAE,OAAO,IAAI,CACf;CACF,UAAU;EACR,MAAM,KAAK,IAAI;CACjB;AACF"}
@@ -9,7 +9,7 @@ declare class DrizzleAssetsRepository implements AssetsRepository {
9
9
  insert(orgId: string, asset: Asset): Promise<Asset>;
10
10
  list(orgId: string, query: AssetsQuery): Promise<Page<Asset>>;
11
11
  findById(orgId: string, id: string): Promise<Asset | null>;
12
- update(id: string, patch: Partial<Pick<Asset, 'size' | 'contentType' | 'status'>>): Promise<Asset | null>;
12
+ update(id: string, patch: Partial<Pick<Asset, "size" | "contentType" | "status">>): Promise<Asset | null>;
13
13
  delete(id: string): Promise<boolean>;
14
14
  }
15
15
  //#endregion
@@ -1,4 +1,5 @@
1
1
  import { assets } from "../schema/assets.js";
2
+ import { translateDbErrors } from "./pg-errors.js";
2
3
  import { and, asc, desc, eq, ilike, sql } from "drizzle-orm";
3
4
  import { noopLogger } from "@headless-lms/core/shared/logger";
4
5
  //#region src/repositories/assets.ts
@@ -13,7 +14,8 @@ function toAsset(row) {
13
14
  size: row.size,
14
15
  status: row.status,
15
16
  uploadedBy: row.uploadedBy,
16
- createdAt: row.createdAt
17
+ createdAt: row.createdAt,
18
+ updatedAt: row.updatedAt
17
19
  };
18
20
  }
19
21
  const SORT_COLUMNS = {
@@ -84,6 +86,7 @@ var DrizzleAssetsRepository = class {
84
86
  return (await this.db.delete(assets).where(eq(assets.id, id)).returning({ id: assets.id })).length > 0;
85
87
  }
86
88
  };
89
+ translateDbErrors(DrizzleAssetsRepository);
87
90
  //#endregion
88
91
  export { DrizzleAssetsRepository };
89
92
 
@@ -1 +1 @@
1
- {"version":3,"file":"assets.js","names":[],"sources":["../../src/repositories/assets.ts"],"sourcesContent":["// assets — Drizzle repository (implements the core outbound port).\nimport { and, eq, ilike, sql, asc, desc, type SQL } from 'drizzle-orm';\nimport type { AssetsRepository } from '@headless-lms/core/assets';\nimport type {\n Asset,\n AssetKind,\n AssetStatus,\n AssetsQuery,\n Logger,\n Page,\n} from '@headless-lms/core/types';\nimport { assets } from '../schema/assets.js';\nimport { noopLogger } from '@headless-lms/core/shared/logger';\nimport type { DbExecutor } from '../client.js';\n\ntype Row = typeof assets.$inferSelect;\n\nfunction toAsset(row: Row): Asset {\n return {\n orgId: row.orgId,\n id: row.id,\n key: row.key,\n kind: row.kind as AssetKind,\n filename: row.filename,\n contentType: row.contentType,\n size: row.size,\n status: row.status as AssetStatus,\n uploadedBy: row.uploadedBy,\n createdAt: row.createdAt,\n };\n}\n\n// Whitelisted sort columns; anything else falls back to newest-first.\nconst SORT_COLUMNS = {\n filename: assets.filename,\n size: assets.size,\n createdAt: assets.createdAt,\n} as const;\n\nexport class DrizzleAssetsRepository implements AssetsRepository {\n constructor(\n private readonly db: DbExecutor,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async insert(orgId: string, asset: Asset): Promise<Asset> {\n if (asset.orgId !== orgId) {\n throw new Error('asset org mismatch');\n }\n const [row] = await this.db\n .insert(assets)\n .values({\n id: asset.id,\n orgId: asset.orgId,\n key: asset.key,\n kind: asset.kind,\n filename: asset.filename,\n contentType: asset.contentType,\n size: asset.size,\n status: asset.status,\n uploadedBy: asset.uploadedBy,\n createdAt: new Date(asset.createdAt),\n })\n .returning();\n if (!row) {\n throw new Error('failed to insert asset');\n }\n return toAsset(row);\n }\n\n async list(orgId: string, query: AssetsQuery): Promise<Page<Asset>> {\n const filters: (SQL | undefined)[] = [eq(assets.orgId, orgId)];\n if (query.kind) {\n filters.push(eq(assets.kind, query.kind));\n }\n const q = query.search?.trim();\n if (q) {\n filters.push(ilike(assets.filename, `%${q}%`));\n }\n const where = and(...filters);\n\n const orderBy = (() => {\n if (!query.sort) {\n return desc(assets.createdAt);\n }\n const descending = query.sort.startsWith('-');\n const field = descending ? query.sort.slice(1) : query.sort;\n const col = SORT_COLUMNS[field as keyof typeof SORT_COLUMNS] ?? assets.createdAt;\n return descending ? desc(col) : asc(col);\n })();\n\n const offset = (query.page - 1) * query.pageSize;\n const rows = await this.db\n .select()\n .from(assets)\n .where(where)\n .orderBy(orderBy)\n .limit(query.pageSize)\n .offset(offset);\n\n const counted = await this.db\n .select({ value: sql<number>`count(*)::int` })\n .from(assets)\n .where(where);\n\n return {\n rows: rows.map(toAsset),\n total: counted[0]?.value ?? 0,\n page: query.page,\n pageSize: query.pageSize,\n };\n }\n\n async findById(orgId: string, id: string): Promise<Asset | null> {\n const [row] = await this.db\n .select()\n .from(assets)\n .where(and(eq(assets.orgId, orgId), eq(assets.id, id)))\n .limit(1);\n return row ? toAsset(row) : null;\n }\n\n async update(\n id: string,\n patch: Partial<Pick<Asset, 'size' | 'contentType' | 'status'>>,\n ): Promise<Asset | null> {\n const [row] = await this.db\n .update(assets)\n .set({\n ...(patch.size !== undefined ? { size: patch.size } : {}),\n ...(patch.contentType !== undefined ? { contentType: patch.contentType } : {}),\n ...(patch.status !== undefined ? { status: patch.status } : {}),\n })\n .where(eq(assets.id, id))\n .returning();\n return row ? toAsset(row) : null;\n }\n\n async delete(id: string): Promise<boolean> {\n const deleted = await this.db\n .delete(assets)\n .where(eq(assets.id, id))\n .returning({ id: assets.id });\n return deleted.length > 0;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"assets.js","names":[],"sources":["../../src/repositories/assets.ts"],"sourcesContent":["// assets — Drizzle repository (implements the core outbound port).\nimport { and, eq, ilike, sql, asc, desc, type SQL } from \"drizzle-orm\";\nimport type { AssetsRepository } from \"@headless-lms/core/assets\";\nimport type {\n Asset,\n AssetKind,\n AssetStatus,\n AssetsQuery,\n Logger,\n Page,\n} from \"@headless-lms/core/types\";\nimport { assets } from \"../schema/assets.js\";\nimport { noopLogger } from \"@headless-lms/core/shared/logger\";\nimport type { DbExecutor } from \"../client.js\";\nimport { translateDbErrors } from \"./pg-errors.js\";\n\ntype Row = typeof assets.$inferSelect;\n\nfunction toAsset(row: Row): Asset {\n return {\n orgId: row.orgId,\n id: row.id,\n key: row.key,\n kind: row.kind as AssetKind,\n filename: row.filename,\n contentType: row.contentType,\n size: row.size,\n status: row.status as AssetStatus,\n uploadedBy: row.uploadedBy,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\n// Whitelisted sort columns; anything else falls back to newest-first.\nconst SORT_COLUMNS = {\n filename: assets.filename,\n size: assets.size,\n createdAt: assets.createdAt,\n} as const;\n\nexport class DrizzleAssetsRepository implements AssetsRepository {\n constructor(\n private readonly db: DbExecutor,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async insert(orgId: string, asset: Asset): Promise<Asset> {\n if (asset.orgId !== orgId) {\n throw new Error(\"asset org mismatch\");\n }\n const [row] = await this.db\n .insert(assets)\n .values({\n id: asset.id,\n orgId: asset.orgId,\n key: asset.key,\n kind: asset.kind,\n filename: asset.filename,\n contentType: asset.contentType,\n size: asset.size,\n status: asset.status,\n uploadedBy: asset.uploadedBy,\n createdAt: new Date(asset.createdAt),\n })\n .returning();\n if (!row) {\n throw new Error(\"failed to insert asset\");\n }\n return toAsset(row);\n }\n\n async list(orgId: string, query: AssetsQuery): Promise<Page<Asset>> {\n const filters: (SQL | undefined)[] = [eq(assets.orgId, orgId)];\n if (query.kind) {\n filters.push(eq(assets.kind, query.kind));\n }\n const q = query.search?.trim();\n if (q) {\n filters.push(ilike(assets.filename, `%${q}%`));\n }\n const where = and(...filters);\n\n const orderBy = (() => {\n if (!query.sort) {\n return desc(assets.createdAt);\n }\n const descending = query.sort.startsWith(\"-\");\n const field = descending ? query.sort.slice(1) : query.sort;\n const col = SORT_COLUMNS[field as keyof typeof SORT_COLUMNS] ?? assets.createdAt;\n return descending ? desc(col) : asc(col);\n })();\n\n const offset = (query.page - 1) * query.pageSize;\n const rows = await this.db\n .select()\n .from(assets)\n .where(where)\n .orderBy(orderBy)\n .limit(query.pageSize)\n .offset(offset);\n\n const counted = await this.db\n .select({ value: sql<number>`count(*)::int` })\n .from(assets)\n .where(where);\n\n return {\n rows: rows.map(toAsset),\n total: counted[0]?.value ?? 0,\n page: query.page,\n pageSize: query.pageSize,\n };\n }\n\n async findById(orgId: string, id: string): Promise<Asset | null> {\n const [row] = await this.db\n .select()\n .from(assets)\n .where(and(eq(assets.orgId, orgId), eq(assets.id, id)))\n .limit(1);\n return row ? toAsset(row) : null;\n }\n\n async update(\n id: string,\n patch: Partial<Pick<Asset, \"size\" | \"contentType\" | \"status\">>,\n ): Promise<Asset | null> {\n const [row] = await this.db\n .update(assets)\n .set({\n ...(patch.size !== undefined ? { size: patch.size } : {}),\n ...(patch.contentType !== undefined ? { contentType: patch.contentType } : {}),\n ...(patch.status !== undefined ? { status: patch.status } : {}),\n })\n .where(eq(assets.id, id))\n .returning();\n return row ? toAsset(row) : null;\n }\n\n async delete(id: string): Promise<boolean> {\n const deleted = await this.db\n .delete(assets)\n .where(eq(assets.id, id))\n .returning({ id: assets.id });\n return deleted.length > 0;\n }\n}\ntranslateDbErrors(DrizzleAssetsRepository);\n"],"mappings":";;;;;AAkBA,SAAS,QAAQ,KAAiB;CAChC,OAAO;EACL,OAAO,IAAI;EACX,IAAI,IAAI;EACR,KAAK,IAAI;EACT,MAAM,IAAI;EACV,UAAU,IAAI;EACd,aAAa,IAAI;EACjB,MAAM,IAAI;EACV,QAAQ,IAAI;EACZ,YAAY,IAAI;EAChB,WAAW,IAAI;EACf,WAAW,IAAI;CACjB;AACF;AAGA,MAAM,eAAe;CACnB,UAAU,OAAO;CACjB,MAAM,OAAO;CACb,WAAW,OAAO;AACpB;AAEA,IAAa,0BAAb,MAAiE;CAE5C;CACA;CAFnB,YACE,IACA,SAAkC,YAClC;EAFiB,KAAA,KAAA;EACA,KAAA,SAAA;CAChB;CAEH,MAAM,OAAO,OAAe,OAA8B;EACxD,IAAI,MAAM,UAAU,OAClB,MAAM,IAAI,MAAM,oBAAoB;EAEtC,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,MAAM,CAAC,CACd,OAAO;GACN,IAAI,MAAM;GACV,OAAO,MAAM;GACb,KAAK,MAAM;GACX,MAAM,MAAM;GACZ,UAAU,MAAM;GAChB,aAAa,MAAM;GACnB,MAAM,MAAM;GACZ,QAAQ,MAAM;GACd,YAAY,MAAM;GAClB,WAAW,IAAI,KAAK,MAAM,SAAS;EACrC,CAAC,CAAC,CACD,UAAU;EACb,IAAI,CAAC,KACH,MAAM,IAAI,MAAM,wBAAwB;EAE1C,OAAO,QAAQ,GAAG;CACpB;CAEA,MAAM,KAAK,OAAe,OAA0C;EAClE,MAAM,UAA+B,CAAC,GAAG,OAAO,OAAO,KAAK,CAAC;EAC7D,IAAI,MAAM,MACR,QAAQ,KAAK,GAAG,OAAO,MAAM,MAAM,IAAI,CAAC;EAE1C,MAAM,IAAI,MAAM,QAAQ,KAAK;EAC7B,IAAI,GACF,QAAQ,KAAK,MAAM,OAAO,UAAU,IAAI,EAAE,EAAE,CAAC;EAE/C,MAAM,QAAQ,IAAI,GAAG,OAAO;EAE5B,MAAM,iBAAiB;GACrB,IAAI,CAAC,MAAM,MACT,OAAO,KAAK,OAAO,SAAS;GAE9B,MAAM,aAAa,MAAM,KAAK,WAAW,GAAG;GAC5C,MAAM,QAAQ,aAAa,MAAM,KAAK,MAAM,CAAC,IAAI,MAAM;GACvD,MAAM,MAAM,aAAa,UAAuC,OAAO;GACvE,OAAO,aAAa,KAAK,GAAG,IAAI,IAAI,GAAG;EACzC,EAAA,CAAG;EAEH,MAAM,UAAU,MAAM,OAAO,KAAK,MAAM;EACxC,MAAM,OAAO,MAAM,KAAK,GACrB,OAAO,CAAC,CACR,KAAK,MAAM,CAAC,CACZ,MAAM,KAAK,CAAC,CACZ,QAAQ,OAAO,CAAC,CAChB,MAAM,MAAM,QAAQ,CAAC,CACrB,OAAO,MAAM;EAEhB,MAAM,UAAU,MAAM,KAAK,GACxB,OAAO,EAAE,OAAO,GAAW,gBAAgB,CAAC,CAAC,CAC7C,KAAK,MAAM,CAAC,CACZ,MAAM,KAAK;EAEd,OAAO;GACL,MAAM,KAAK,IAAI,OAAO;GACtB,OAAO,QAAQ,EAAE,EAAE,SAAS;GAC5B,MAAM,MAAM;GACZ,UAAU,MAAM;EAClB;CACF;CAEA,MAAM,SAAS,OAAe,IAAmC;EAC/D,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,CAAC,CACR,KAAK,MAAM,CAAC,CACZ,MAAM,IAAI,GAAG,OAAO,OAAO,KAAK,GAAG,GAAG,OAAO,IAAI,EAAE,CAAC,CAAC,CAAC,CACtD,MAAM,CAAC;EACV,OAAO,MAAM,QAAQ,GAAG,IAAI;CAC9B;CAEA,MAAM,OACJ,IACA,OACuB;EACvB,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,MAAM,CAAC,CACd,IAAI;GACH,GAAI,MAAM,SAAS,KAAA,IAAY,EAAE,MAAM,MAAM,KAAK,IAAI,CAAC;GACvD,GAAI,MAAM,gBAAgB,KAAA,IAAY,EAAE,aAAa,MAAM,YAAY,IAAI,CAAC;GAC5E,GAAI,MAAM,WAAW,KAAA,IAAY,EAAE,QAAQ,MAAM,OAAO,IAAI,CAAC;EAC/D,CAAC,CAAC,CACD,MAAM,GAAG,OAAO,IAAI,EAAE,CAAC,CAAC,CACxB,UAAU;EACb,OAAO,MAAM,QAAQ,GAAG,IAAI;CAC9B;CAEA,MAAM,OAAO,IAA8B;EAKzC,QAAO,MAJe,KAAK,GACxB,OAAO,MAAM,CAAC,CACd,MAAM,GAAG,OAAO,IAAI,EAAE,CAAC,CAAC,CACxB,UAAU,EAAE,IAAI,OAAO,GAAG,CAAC,EAAA,CACf,SAAS;CAC1B;AACF;AACA,kBAAkB,uBAAuB"}
@@ -1,16 +1,16 @@
1
1
  import { DbExecutor } from "../client.js";
2
2
  import { Logger } from "@headless-lms/core/shared/ports";
3
- import { Automation, AutomationRun, AutomationRunsQuery, AutomationRunsRepository, AutomationsRepository, CreateAutomationInput, NewAutomationRun, Page, UpdateAutomationInput } from "@headless-lms/core/automations";
3
+ import { Automation, AutomationKind, AutomationRun, AutomationRunsQuery, AutomationRunsRepository, AutomationsRepository, NewAutomation, NewAutomationRun, Page, UpdateAutomationInput } from "@headless-lms/core/automations";
4
4
  //#region src/repositories/automations.d.ts
5
5
  declare class DrizzleAutomationsRepository implements AutomationsRepository {
6
6
  private readonly db;
7
7
  private readonly logger;
8
8
  constructor(db: DbExecutor, logger?: Logger);
9
- insert(orgId: string, input: CreateAutomationInput): Promise<Automation>;
9
+ insert(orgId: string, input: NewAutomation): Promise<Automation>;
10
10
  update(orgId: string, id: string, patch: UpdateAutomationInput): Promise<Automation | null>;
11
11
  delete(orgId: string, id: string): Promise<Automation | null>;
12
12
  findById(orgId: string, id: string): Promise<Automation | null>;
13
- listByOrg(orgId: string): Promise<Automation[]>;
13
+ listByOrg(orgId: string, kind?: AutomationKind): Promise<Automation[]>;
14
14
  listByTrigger(orgId: string, trigger: string): Promise<Automation[]>;
15
15
  }
16
16
  declare class DrizzleAutomationRunsRepository implements AutomationRunsRepository {
@@ -18,9 +18,11 @@ declare class DrizzleAutomationRunsRepository implements AutomationRunsRepositor
18
18
  private readonly logger;
19
19
  constructor(db: DbExecutor, logger?: Logger);
20
20
  insert(orgId: string, run: NewAutomationRun): Promise<AutomationRun | null>;
21
+ findById(orgId: string, id: string): Promise<AutomationRun | null>;
22
+ restart(orgId: string, id: string, startedAt: Date): Promise<AutomationRun | null>;
21
23
  recordOutcome(orgId: string, id: string, outcome: {
22
- status: AutomationRun['status'];
23
- actionResults: AutomationRun['actionResults'];
24
+ status: AutomationRun["status"];
25
+ actionResults: AutomationRun["actionResults"];
24
26
  finishedAt: Date;
25
27
  }): Promise<AutomationRun | null>;
26
28
  list(orgId: string, automationId: string, query: AutomationRunsQuery): Promise<Page<AutomationRun>>;
@@ -1,14 +1,16 @@
1
1
  import { automationRuns, automations } from "../schema/automations.js";
2
2
  import "../schema/index.js";
3
- import { and, asc, desc, eq, sql } from "drizzle-orm";
3
+ import { translateDbErrors } from "./pg-errors.js";
4
+ import { and, arrayContains, asc, desc, eq, ne, sql } from "drizzle-orm";
4
5
  import { noopLogger } from "@headless-lms/core/shared/logger";
5
6
  //#region src/repositories/automations.ts
6
7
  const automationSelection = {
7
8
  orgId: automations.orgId,
8
9
  id: automations.id,
9
10
  name: automations.name,
11
+ kind: automations.kind,
10
12
  description: automations.description,
11
- trigger: automations.trigger,
13
+ triggers: automations.triggers,
12
14
  actions: automations.actions,
13
15
  enabled: automations.enabled,
14
16
  createdAt: automations.createdAt,
@@ -19,8 +21,9 @@ function toAutomation(row) {
19
21
  orgId: row.orgId,
20
22
  id: row.id,
21
23
  name: row.name,
24
+ kind: row.kind,
22
25
  description: row.description,
23
- trigger: row.trigger,
26
+ triggers: row.triggers,
24
27
  actions: row.actions,
25
28
  enabled: row.enabled,
26
29
  createdAt: row.createdAt,
@@ -38,8 +41,9 @@ var DrizzleAutomationsRepository = class {
38
41
  const [inserted] = await this.db.insert(automations).values({
39
42
  orgId,
40
43
  name: input.name,
44
+ kind: input.kind,
41
45
  description: input.description,
42
- trigger: input.trigger,
46
+ triggers: input.triggers,
43
47
  actions: input.actions
44
48
  }).returning({ id: automations.id });
45
49
  if (!inserted) throw new Error("failed to insert automation");
@@ -51,7 +55,7 @@ var DrizzleAutomationsRepository = class {
51
55
  const set = { updatedAt: /* @__PURE__ */ new Date() };
52
56
  if (patch.name !== void 0) set.name = patch.name;
53
57
  if (patch.description !== void 0) set.description = patch.description;
54
- if (patch.trigger !== void 0) set.trigger = patch.trigger;
58
+ if (patch.triggers !== void 0) set.triggers = patch.triggers;
55
59
  if (patch.actions !== void 0) set.actions = patch.actions;
56
60
  if (patch.enabled !== void 0) set.enabled = patch.enabled;
57
61
  const [updated] = await this.db.update(automations).set(set).where(and(eq(automations.orgId, orgId), eq(automations.id, id))).returning({ id: automations.id });
@@ -66,11 +70,13 @@ var DrizzleAutomationsRepository = class {
66
70
  const [row] = await this.db.select(automationSelection).from(automations).where(and(eq(automations.orgId, orgId), eq(automations.id, id))).limit(1);
67
71
  return row ? toAutomation(row) : null;
68
72
  }
69
- async listByOrg(orgId) {
70
- return (await this.db.select(automationSelection).from(automations).where(eq(automations.orgId, orgId)).orderBy(desc(automations.createdAt))).map(toAutomation);
73
+ async listByOrg(orgId, kind) {
74
+ const conditions = [eq(automations.orgId, orgId)];
75
+ if (kind) conditions.push(eq(automations.kind, kind));
76
+ return (await this.db.select(automationSelection).from(automations).where(and(...conditions)).orderBy(desc(automations.createdAt))).map(toAutomation);
71
77
  }
72
78
  async listByTrigger(orgId, trigger) {
73
- return (await this.db.select(automationSelection).from(automations).where(and(eq(automations.orgId, orgId), eq(automations.trigger, trigger))).orderBy(desc(automations.createdAt))).map(toAutomation);
79
+ return (await this.db.select(automationSelection).from(automations).where(and(eq(automations.orgId, orgId), arrayContains(automations.triggers, [trigger]))).orderBy(desc(automations.createdAt))).map(toAutomation);
74
80
  }
75
81
  };
76
82
  const runSelection = {
@@ -83,7 +89,9 @@ const runSelection = {
83
89
  status: automationRuns.status,
84
90
  actionResults: automationRuns.actionResults,
85
91
  startedAt: automationRuns.startedAt,
86
- finishedAt: automationRuns.finishedAt
92
+ finishedAt: automationRuns.finishedAt,
93
+ createdAt: automationRuns.createdAt,
94
+ updatedAt: automationRuns.updatedAt
87
95
  };
88
96
  function toAutomationRun(row) {
89
97
  return {
@@ -96,7 +104,9 @@ function toAutomationRun(row) {
96
104
  status: row.status,
97
105
  actionResults: row.actionResults,
98
106
  startedAt: row.startedAt,
99
- finishedAt: row.finishedAt
107
+ finishedAt: row.finishedAt,
108
+ createdAt: row.createdAt,
109
+ updatedAt: row.updatedAt
100
110
  };
101
111
  }
102
112
  const sortColumns = {
@@ -129,6 +139,19 @@ var DrizzleAutomationRunsRepository = class {
129
139
  ] }).returning(runSelection);
130
140
  return inserted ? toAutomationRun(inserted) : null;
131
141
  }
142
+ async findById(orgId, id) {
143
+ const [row] = await this.db.select(runSelection).from(automationRuns).where(and(eq(automationRuns.orgId, orgId), eq(automationRuns.id, id))).limit(1);
144
+ return row ? toAutomationRun(row) : null;
145
+ }
146
+ async restart(orgId, id, startedAt) {
147
+ const [restarted] = await this.db.update(automationRuns).set({
148
+ status: "running",
149
+ actionResults: [],
150
+ startedAt,
151
+ finishedAt: null
152
+ }).where(and(eq(automationRuns.orgId, orgId), eq(automationRuns.id, id), ne(automationRuns.status, "running"))).returning(runSelection);
153
+ return restarted ? toAutomationRun(restarted) : null;
154
+ }
132
155
  async recordOutcome(orgId, id, outcome) {
133
156
  const [updated] = await this.db.update(automationRuns).set({
134
157
  status: outcome.status,
@@ -159,6 +182,8 @@ var DrizzleAutomationRunsRepository = class {
159
182
  };
160
183
  }
161
184
  };
185
+ translateDbErrors(DrizzleAutomationsRepository);
186
+ translateDbErrors(DrizzleAutomationRunsRepository);
162
187
  //#endregion
163
188
  export { DrizzleAutomationRunsRepository, DrizzleAutomationsRepository };
164
189
 
@@ -1 +1 @@
1
- {"version":3,"file":"automations.js","names":[],"sources":["../../src/repositories/automations.ts"],"sourcesContent":["// automations — Drizzle repositories (implement the core outbound ports).\n// Org-scoped: every method takes the domain `organizations.id` and constrains\n// its queries to that tenant. `Automation`/`AutomationRun` carry no `orgId`\n// column of their own in the domain type (mirrors the `Entitlement` shape) —\n// `AutomationRun` is the one exception, since it's the run's own `orgId`\n// field the service reads back off `handle`'s event.\nimport { and, asc, desc, eq, sql, type SQL } from 'drizzle-orm';\nimport type { DbExecutor } from '../client.js';\nimport type {\n Automation,\n AutomationRun,\n AutomationRunsQuery,\n AutomationRunsRepository,\n AutomationsRepository,\n CreateAutomationInput,\n NewAutomationRun,\n Page,\n UpdateAutomationInput,\n} from '@headless-lms/core/automations';\nimport { automations, automationRuns } from '../schema/index.js';\nimport type { Logger } from '@headless-lms/core/shared/ports';\nimport { noopLogger } from '@headless-lms/core/shared/logger';\n\nconst automationSelection = {\n orgId: automations.orgId,\n id: automations.id,\n name: automations.name,\n description: automations.description,\n trigger: automations.trigger,\n actions: automations.actions,\n enabled: automations.enabled,\n createdAt: automations.createdAt,\n updatedAt: automations.updatedAt,\n};\n\ninterface AutomationRow {\n orgId: string;\n id: string;\n name: string;\n description: string | null;\n trigger: string;\n actions: Automation['actions'];\n enabled: boolean;\n createdAt: Date;\n updatedAt: Date;\n}\n\nfunction toAutomation(row: AutomationRow): Automation {\n return {\n orgId: row.orgId,\n id: row.id,\n name: row.name,\n description: row.description,\n trigger: row.trigger,\n actions: row.actions,\n enabled: row.enabled,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\nexport class DrizzleAutomationsRepository implements AutomationsRepository {\n constructor(\n private readonly db: DbExecutor,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async insert(orgId: string, input: CreateAutomationInput): Promise<Automation> {\n const [inserted] = await this.db\n .insert(automations)\n .values({\n orgId,\n name: input.name,\n description: input.description,\n trigger: input.trigger,\n actions: input.actions,\n })\n .returning({ id: automations.id });\n if (!inserted) {\n throw new Error('failed to insert automation');\n }\n const created = await this.findById(orgId, inserted.id);\n if (!created) {\n throw new Error('failed to load created automation');\n }\n return created;\n }\n\n async update(orgId: string, id: string, patch: UpdateAutomationInput): Promise<Automation | null> {\n const set: Record<string, unknown> = { updatedAt: new Date() };\n if (patch.name !== undefined) {\n set.name = patch.name;\n }\n if (patch.description !== undefined) {\n set.description = patch.description;\n }\n if (patch.trigger !== undefined) {\n set.trigger = patch.trigger;\n }\n if (patch.actions !== undefined) {\n set.actions = patch.actions;\n }\n if (patch.enabled !== undefined) {\n set.enabled = patch.enabled;\n }\n\n const [updated] = await this.db\n .update(automations)\n .set(set)\n .where(and(eq(automations.orgId, orgId), eq(automations.id, id)))\n .returning({ id: automations.id });\n if (!updated) {\n return null;\n }\n return this.findById(orgId, id);\n }\n\n async delete(orgId: string, id: string): Promise<Automation | null> {\n const [deleted] = await this.db\n .delete(automations)\n .where(and(eq(automations.orgId, orgId), eq(automations.id, id)))\n .returning(automationSelection);\n return deleted ? toAutomation(deleted) : null;\n }\n\n async findById(orgId: string, id: string): Promise<Automation | null> {\n const [row] = await this.db\n .select(automationSelection)\n .from(automations)\n .where(and(eq(automations.orgId, orgId), eq(automations.id, id)))\n .limit(1);\n return row ? toAutomation(row) : null;\n }\n\n async listByOrg(orgId: string): Promise<Automation[]> {\n const rows = await this.db\n .select(automationSelection)\n .from(automations)\n .where(eq(automations.orgId, orgId))\n .orderBy(desc(automations.createdAt));\n return rows.map(toAutomation);\n }\n\n async listByTrigger(orgId: string, trigger: string): Promise<Automation[]> {\n const rows = await this.db\n .select(automationSelection)\n .from(automations)\n .where(and(eq(automations.orgId, orgId), eq(automations.trigger, trigger)))\n .orderBy(desc(automations.createdAt));\n return rows.map(toAutomation);\n }\n}\n\nconst runSelection = {\n id: automationRuns.id,\n orgId: automationRuns.orgId,\n automationId: automationRuns.automationId,\n trigger: automationRuns.trigger,\n eventId: automationRuns.eventId,\n event: automationRuns.event,\n status: automationRuns.status,\n actionResults: automationRuns.actionResults,\n startedAt: automationRuns.startedAt,\n finishedAt: automationRuns.finishedAt,\n};\n\ninterface RunRow {\n id: string;\n orgId: string;\n automationId: string;\n trigger: string;\n eventId: string;\n event: AutomationRun['event'];\n status: AutomationRun['status'];\n actionResults: AutomationRun['actionResults'];\n startedAt: Date;\n finishedAt: Date | null;\n}\n\nfunction toAutomationRun(row: RunRow): AutomationRun {\n return {\n id: row.id,\n orgId: row.orgId,\n automationId: row.automationId,\n trigger: row.trigger,\n eventId: row.eventId,\n event: row.event,\n status: row.status,\n actionResults: row.actionResults,\n startedAt: row.startedAt,\n finishedAt: row.finishedAt,\n };\n}\n\n// Sortable columns by the client-facing field name. `-field` = desc.\nconst sortColumns = {\n startedAt: automationRuns.startedAt,\n finishedAt: automationRuns.finishedAt,\n status: automationRuns.status,\n} as const;\n\nexport class DrizzleAutomationRunsRepository implements AutomationRunsRepository {\n constructor(\n private readonly db: DbExecutor,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async insert(orgId: string, run: NewAutomationRun): Promise<AutomationRun | null> {\n const [inserted] = await this.db\n .insert(automationRuns)\n .values({\n orgId,\n automationId: run.automationId,\n trigger: run.trigger,\n eventId: run.event.id,\n event: run.event,\n status: run.status,\n actionResults: run.actionResults,\n startedAt: run.startedAt,\n finishedAt: run.finishedAt,\n })\n // A redelivered trigger event hits the unique (org, automation, event) index; no row comes back.\n .onConflictDoNothing({\n target: [automationRuns.orgId, automationRuns.automationId, automationRuns.eventId],\n })\n .returning(runSelection);\n return inserted ? toAutomationRun(inserted) : null;\n }\n\n async recordOutcome(\n orgId: string,\n id: string,\n outcome: {\n status: AutomationRun['status'];\n actionResults: AutomationRun['actionResults'];\n finishedAt: Date;\n },\n ): Promise<AutomationRun | null> {\n const [updated] = await this.db\n .update(automationRuns)\n .set({\n status: outcome.status,\n actionResults: outcome.actionResults,\n finishedAt: outcome.finishedAt,\n })\n .where(and(eq(automationRuns.orgId, orgId), eq(automationRuns.id, id)))\n .returning(runSelection);\n return updated ? toAutomationRun(updated) : null;\n }\n\n async list(orgId: string, automationId: string, query: AutomationRunsQuery): Promise<Page<AutomationRun>> {\n const conditions: SQL[] = [\n eq(automationRuns.orgId, orgId),\n eq(automationRuns.automationId, automationId),\n ];\n if (query.status) {\n conditions.push(eq(automationRuns.status, query.status));\n }\n const where = and(...conditions);\n\n // Sort: `-field` for descending; default to most-recently started first.\n let orderBy: SQL;\n if (query.sort) {\n const isDesc = query.sort.startsWith('-');\n const field = (isDesc ? query.sort.slice(1) : query.sort) as keyof typeof sortColumns;\n const col = sortColumns[field] ?? automationRuns.startedAt;\n orderBy = isDesc ? desc(col) : asc(col);\n } else {\n orderBy = desc(automationRuns.startedAt);\n }\n\n const offset = (query.page - 1) * query.pageSize;\n\n const rows = await this.db\n .select(runSelection)\n .from(automationRuns)\n .where(where)\n .orderBy(orderBy)\n .limit(query.pageSize)\n .offset(offset);\n\n const [{ total } = { total: 0 }] = await this.db\n .select({ total: sql<number>`cast(count(*) as int)` })\n .from(automationRuns)\n .where(where);\n\n return {\n rows: rows.map(toAutomationRun),\n total,\n page: query.page,\n pageSize: query.pageSize,\n };\n }\n}\n"],"mappings":";;;;;AAuBA,MAAM,sBAAsB;CAC1B,OAAO,YAAY;CACnB,IAAI,YAAY;CAChB,MAAM,YAAY;CAClB,aAAa,YAAY;CACzB,SAAS,YAAY;CACrB,SAAS,YAAY;CACrB,SAAS,YAAY;CACrB,WAAW,YAAY;CACvB,WAAW,YAAY;AACzB;AAcA,SAAS,aAAa,KAAgC;CACpD,OAAO;EACL,OAAO,IAAI;EACX,IAAI,IAAI;EACR,MAAM,IAAI;EACV,aAAa,IAAI;EACjB,SAAS,IAAI;EACb,SAAS,IAAI;EACb,SAAS,IAAI;EACb,WAAW,IAAI;EACf,WAAW,IAAI;CACjB;AACF;AAEA,IAAa,+BAAb,MAA2E;CAEtD;CACA;CAFnB,YACE,IACA,SAAkC,YAClC;EAFiB,KAAA,KAAA;EACA,KAAA,SAAA;CAChB;CAEH,MAAM,OAAO,OAAe,OAAmD;EAC7E,MAAM,CAAC,YAAY,MAAM,KAAK,GAC3B,OAAO,WAAW,CAAC,CACnB,OAAO;GACN;GACA,MAAM,MAAM;GACZ,aAAa,MAAM;GACnB,SAAS,MAAM;GACf,SAAS,MAAM;EACjB,CAAC,CAAC,CACD,UAAU,EAAE,IAAI,YAAY,GAAG,CAAC;EACnC,IAAI,CAAC,UACH,MAAM,IAAI,MAAM,6BAA6B;EAE/C,MAAM,UAAU,MAAM,KAAK,SAAS,OAAO,SAAS,EAAE;EACtD,IAAI,CAAC,SACH,MAAM,IAAI,MAAM,mCAAmC;EAErD,OAAO;CACT;CAEA,MAAM,OAAO,OAAe,IAAY,OAA0D;EAChG,MAAM,MAA+B,EAAE,2BAAW,IAAI,KAAK,EAAE;EAC7D,IAAI,MAAM,SAAS,KAAA,GACjB,IAAI,OAAO,MAAM;EAEnB,IAAI,MAAM,gBAAgB,KAAA,GACxB,IAAI,cAAc,MAAM;EAE1B,IAAI,MAAM,YAAY,KAAA,GACpB,IAAI,UAAU,MAAM;EAEtB,IAAI,MAAM,YAAY,KAAA,GACpB,IAAI,UAAU,MAAM;EAEtB,IAAI,MAAM,YAAY,KAAA,GACpB,IAAI,UAAU,MAAM;EAGtB,MAAM,CAAC,WAAW,MAAM,KAAK,GAC1B,OAAO,WAAW,CAAC,CACnB,IAAI,GAAG,CAAC,CACR,MAAM,IAAI,GAAG,YAAY,OAAO,KAAK,GAAG,GAAG,YAAY,IAAI,EAAE,CAAC,CAAC,CAAC,CAChE,UAAU,EAAE,IAAI,YAAY,GAAG,CAAC;EACnC,IAAI,CAAC,SACH,OAAO;EAET,OAAO,KAAK,SAAS,OAAO,EAAE;CAChC;CAEA,MAAM,OAAO,OAAe,IAAwC;EAClE,MAAM,CAAC,WAAW,MAAM,KAAK,GAC1B,OAAO,WAAW,CAAC,CACnB,MAAM,IAAI,GAAG,YAAY,OAAO,KAAK,GAAG,GAAG,YAAY,IAAI,EAAE,CAAC,CAAC,CAAC,CAChE,UAAU,mBAAmB;EAChC,OAAO,UAAU,aAAa,OAAO,IAAI;CAC3C;CAEA,MAAM,SAAS,OAAe,IAAwC;EACpE,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,mBAAmB,CAAC,CAC3B,KAAK,WAAW,CAAC,CACjB,MAAM,IAAI,GAAG,YAAY,OAAO,KAAK,GAAG,GAAG,YAAY,IAAI,EAAE,CAAC,CAAC,CAAC,CAChE,MAAM,CAAC;EACV,OAAO,MAAM,aAAa,GAAG,IAAI;CACnC;CAEA,MAAM,UAAU,OAAsC;EAMpD,QAAO,MALY,KAAK,GACrB,OAAO,mBAAmB,CAAC,CAC3B,KAAK,WAAW,CAAC,CACjB,MAAM,GAAG,YAAY,OAAO,KAAK,CAAC,CAAC,CACnC,QAAQ,KAAK,YAAY,SAAS,CAAC,EAAA,CAC1B,IAAI,YAAY;CAC9B;CAEA,MAAM,cAAc,OAAe,SAAwC;EAMzE,QAAO,MALY,KAAK,GACrB,OAAO,mBAAmB,CAAC,CAC3B,KAAK,WAAW,CAAC,CACjB,MAAM,IAAI,GAAG,YAAY,OAAO,KAAK,GAAG,GAAG,YAAY,SAAS,OAAO,CAAC,CAAC,CAAC,CAC1E,QAAQ,KAAK,YAAY,SAAS,CAAC,EAAA,CAC1B,IAAI,YAAY;CAC9B;AACF;AAEA,MAAM,eAAe;CACnB,IAAI,eAAe;CACnB,OAAO,eAAe;CACtB,cAAc,eAAe;CAC7B,SAAS,eAAe;CACxB,SAAS,eAAe;CACxB,OAAO,eAAe;CACtB,QAAQ,eAAe;CACvB,eAAe,eAAe;CAC9B,WAAW,eAAe;CAC1B,YAAY,eAAe;AAC7B;AAeA,SAAS,gBAAgB,KAA4B;CACnD,OAAO;EACL,IAAI,IAAI;EACR,OAAO,IAAI;EACX,cAAc,IAAI;EAClB,SAAS,IAAI;EACb,SAAS,IAAI;EACb,OAAO,IAAI;EACX,QAAQ,IAAI;EACZ,eAAe,IAAI;EACnB,WAAW,IAAI;EACf,YAAY,IAAI;CAClB;AACF;AAGA,MAAM,cAAc;CAClB,WAAW,eAAe;CAC1B,YAAY,eAAe;CAC3B,QAAQ,eAAe;AACzB;AAEA,IAAa,kCAAb,MAAiF;CAE5D;CACA;CAFnB,YACE,IACA,SAAkC,YAClC;EAFiB,KAAA,KAAA;EACA,KAAA,SAAA;CAChB;CAEH,MAAM,OAAO,OAAe,KAAsD;EAChF,MAAM,CAAC,YAAY,MAAM,KAAK,GAC3B,OAAO,cAAc,CAAC,CACtB,OAAO;GACN;GACA,cAAc,IAAI;GAClB,SAAS,IAAI;GACb,SAAS,IAAI,MAAM;GACnB,OAAO,IAAI;GACX,QAAQ,IAAI;GACZ,eAAe,IAAI;GACnB,WAAW,IAAI;GACf,YAAY,IAAI;EAClB,CAAC,CAAC,CAED,oBAAoB,EACnB,QAAQ;GAAC,eAAe;GAAO,eAAe;GAAc,eAAe;EAAO,EACpF,CAAC,CAAC,CACD,UAAU,YAAY;EACzB,OAAO,WAAW,gBAAgB,QAAQ,IAAI;CAChD;CAEA,MAAM,cACJ,OACA,IACA,SAK+B;EAC/B,MAAM,CAAC,WAAW,MAAM,KAAK,GAC1B,OAAO,cAAc,CAAC,CACtB,IAAI;GACH,QAAQ,QAAQ;GAChB,eAAe,QAAQ;GACvB,YAAY,QAAQ;EACtB,CAAC,CAAC,CACD,MAAM,IAAI,GAAG,eAAe,OAAO,KAAK,GAAG,GAAG,eAAe,IAAI,EAAE,CAAC,CAAC,CAAC,CACtE,UAAU,YAAY;EACzB,OAAO,UAAU,gBAAgB,OAAO,IAAI;CAC9C;CAEA,MAAM,KAAK,OAAe,cAAsB,OAA0D;EACxG,MAAM,aAAoB,CACxB,GAAG,eAAe,OAAO,KAAK,GAC9B,GAAG,eAAe,cAAc,YAAY,CAC9C;EACA,IAAI,MAAM,QACR,WAAW,KAAK,GAAG,eAAe,QAAQ,MAAM,MAAM,CAAC;EAEzD,MAAM,QAAQ,IAAI,GAAG,UAAU;EAG/B,IAAI;EACJ,IAAI,MAAM,MAAM;GACd,MAAM,SAAS,MAAM,KAAK,WAAW,GAAG;GACxC,MAAM,QAAS,SAAS,MAAM,KAAK,MAAM,CAAC,IAAI,MAAM;GACpD,MAAM,MAAM,YAAY,UAAU,eAAe;GACjD,UAAU,SAAS,KAAK,GAAG,IAAI,IAAI,GAAG;EACxC,OACE,UAAU,KAAK,eAAe,SAAS;EAGzC,MAAM,UAAU,MAAM,OAAO,KAAK,MAAM;EAExC,MAAM,OAAO,MAAM,KAAK,GACrB,OAAO,YAAY,CAAC,CACpB,KAAK,cAAc,CAAC,CACpB,MAAM,KAAK,CAAC,CACZ,QAAQ,OAAO,CAAC,CAChB,MAAM,MAAM,QAAQ,CAAC,CACrB,OAAO,MAAM;EAEhB,MAAM,CAAC,EAAE,UAAU,EAAE,OAAO,EAAE,KAAK,MAAM,KAAK,GAC3C,OAAO,EAAE,OAAO,GAAW,wBAAwB,CAAC,CAAC,CACrD,KAAK,cAAc,CAAC,CACpB,MAAM,KAAK;EAEd,OAAO;GACL,MAAM,KAAK,IAAI,eAAe;GAC9B;GACA,MAAM,MAAM;GACZ,UAAU,MAAM;EAClB;CACF;AACF"}
1
+ {"version":3,"file":"automations.js","names":[],"sources":["../../src/repositories/automations.ts"],"sourcesContent":["// automations — Drizzle repositories (implement the core outbound ports).\n// Org-scoped: every method takes the domain `organizations.id` and constrains\n// its queries to that tenant. `Automation`/`AutomationRun` carry no `orgId`\n// column of their own in the domain type (mirrors the `Entitlement` shape) —\n// `AutomationRun` is the one exception, since it's the run's own `orgId`\n// field the service reads back off `handle`'s event.\nimport { and, arrayContains, asc, desc, eq, ne, sql, type SQL } from \"drizzle-orm\";\nimport type { DbExecutor } from \"../client.js\";\nimport type {\n Automation,\n AutomationKind,\n AutomationRun,\n AutomationRunsQuery,\n AutomationRunsRepository,\n AutomationsRepository,\n NewAutomation,\n NewAutomationRun,\n Page,\n UpdateAutomationInput,\n} from \"@headless-lms/core/automations\";\nimport { automations, automationRuns } from \"../schema/index.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\nconst automationSelection = {\n orgId: automations.orgId,\n id: automations.id,\n name: automations.name,\n kind: automations.kind,\n description: automations.description,\n triggers: automations.triggers,\n actions: automations.actions,\n enabled: automations.enabled,\n createdAt: automations.createdAt,\n updatedAt: automations.updatedAt,\n};\n\ninterface AutomationRow {\n orgId: string;\n id: string;\n name: string;\n kind: AutomationKind;\n description: string | null;\n triggers: string[];\n actions: Automation[\"actions\"];\n enabled: boolean;\n createdAt: Date;\n updatedAt: Date;\n}\n\nfunction toAutomation(row: AutomationRow): Automation {\n return {\n orgId: row.orgId,\n id: row.id,\n name: row.name,\n kind: row.kind,\n description: row.description,\n triggers: row.triggers,\n actions: row.actions,\n enabled: row.enabled,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\nexport class DrizzleAutomationsRepository implements AutomationsRepository {\n constructor(\n private readonly db: DbExecutor,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async insert(orgId: string, input: NewAutomation): Promise<Automation> {\n const [inserted] = await this.db\n .insert(automations)\n .values({\n orgId,\n name: input.name,\n kind: input.kind,\n description: input.description,\n triggers: input.triggers,\n actions: input.actions,\n })\n .returning({ id: automations.id });\n if (!inserted) {\n throw new Error(\"failed to insert automation\");\n }\n const created = await this.findById(orgId, inserted.id);\n if (!created) {\n throw new Error(\"failed to load created automation\");\n }\n return created;\n }\n\n async update(\n orgId: string,\n id: string,\n patch: UpdateAutomationInput,\n ): Promise<Automation | null> {\n const set: Record<string, unknown> = { updatedAt: new Date() };\n if (patch.name !== undefined) {\n set.name = patch.name;\n }\n if (patch.description !== undefined) {\n set.description = patch.description;\n }\n if (patch.triggers !== undefined) {\n set.triggers = patch.triggers;\n }\n if (patch.actions !== undefined) {\n set.actions = patch.actions;\n }\n if (patch.enabled !== undefined) {\n set.enabled = patch.enabled;\n }\n\n const [updated] = await this.db\n .update(automations)\n .set(set)\n .where(and(eq(automations.orgId, orgId), eq(automations.id, id)))\n .returning({ id: automations.id });\n if (!updated) {\n return null;\n }\n return this.findById(orgId, id);\n }\n\n async delete(orgId: string, id: string): Promise<Automation | null> {\n const [deleted] = await this.db\n .delete(automations)\n .where(and(eq(automations.orgId, orgId), eq(automations.id, id)))\n .returning(automationSelection);\n return deleted ? toAutomation(deleted) : null;\n }\n\n async findById(orgId: string, id: string): Promise<Automation | null> {\n const [row] = await this.db\n .select(automationSelection)\n .from(automations)\n .where(and(eq(automations.orgId, orgId), eq(automations.id, id)))\n .limit(1);\n return row ? toAutomation(row) : null;\n }\n\n async listByOrg(orgId: string, kind?: AutomationKind): Promise<Automation[]> {\n const conditions: SQL[] = [eq(automations.orgId, orgId)];\n if (kind) {\n conditions.push(eq(automations.kind, kind));\n }\n const rows = await this.db\n .select(automationSelection)\n .from(automations)\n .where(and(...conditions))\n .orderBy(desc(automations.createdAt));\n return rows.map(toAutomation);\n }\n\n async listByTrigger(orgId: string, trigger: string): Promise<Automation[]> {\n const rows = await this.db\n .select(automationSelection)\n .from(automations)\n .where(and(eq(automations.orgId, orgId), arrayContains(automations.triggers, [trigger])))\n .orderBy(desc(automations.createdAt));\n return rows.map(toAutomation);\n }\n}\n\nconst runSelection = {\n id: automationRuns.id,\n orgId: automationRuns.orgId,\n automationId: automationRuns.automationId,\n trigger: automationRuns.trigger,\n eventId: automationRuns.eventId,\n event: automationRuns.event,\n status: automationRuns.status,\n actionResults: automationRuns.actionResults,\n startedAt: automationRuns.startedAt,\n finishedAt: automationRuns.finishedAt,\n createdAt: automationRuns.createdAt,\n updatedAt: automationRuns.updatedAt,\n};\n\ninterface RunRow {\n id: string;\n orgId: string;\n automationId: string;\n trigger: string;\n eventId: string;\n event: AutomationRun[\"event\"];\n status: AutomationRun[\"status\"];\n actionResults: AutomationRun[\"actionResults\"];\n startedAt: Date;\n finishedAt: Date | null;\n createdAt: Date;\n updatedAt: Date;\n}\n\nfunction toAutomationRun(row: RunRow): AutomationRun {\n return {\n id: row.id,\n orgId: row.orgId,\n automationId: row.automationId,\n trigger: row.trigger,\n eventId: row.eventId,\n event: row.event,\n status: row.status,\n actionResults: row.actionResults,\n startedAt: row.startedAt,\n finishedAt: row.finishedAt,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\n// Sortable columns by the client-facing field name. `-field` = desc.\nconst sortColumns = {\n startedAt: automationRuns.startedAt,\n finishedAt: automationRuns.finishedAt,\n status: automationRuns.status,\n} as const;\n\nexport class DrizzleAutomationRunsRepository implements AutomationRunsRepository {\n constructor(\n private readonly db: DbExecutor,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async insert(orgId: string, run: NewAutomationRun): Promise<AutomationRun | null> {\n const [inserted] = await this.db\n .insert(automationRuns)\n .values({\n orgId,\n automationId: run.automationId,\n trigger: run.trigger,\n eventId: run.event.id,\n event: run.event,\n status: run.status,\n actionResults: run.actionResults,\n startedAt: run.startedAt,\n finishedAt: run.finishedAt,\n })\n // A redelivered trigger event hits the unique (org, automation, event) index; no row comes back.\n .onConflictDoNothing({\n target: [automationRuns.orgId, automationRuns.automationId, automationRuns.eventId],\n })\n .returning(runSelection);\n return inserted ? toAutomationRun(inserted) : null;\n }\n\n async findById(orgId: string, id: string): Promise<AutomationRun | null> {\n const [row] = await this.db\n .select(runSelection)\n .from(automationRuns)\n .where(and(eq(automationRuns.orgId, orgId), eq(automationRuns.id, id)))\n .limit(1);\n return row ? toAutomationRun(row) : null;\n }\n\n async restart(orgId: string, id: string, startedAt: Date): Promise<AutomationRun | null> {\n const [restarted] = await this.db\n .update(automationRuns)\n .set({ status: \"running\", actionResults: [], startedAt, finishedAt: null })\n .where(\n and(\n eq(automationRuns.orgId, orgId),\n eq(automationRuns.id, id),\n ne(automationRuns.status, \"running\"),\n ),\n )\n .returning(runSelection);\n return restarted ? toAutomationRun(restarted) : null;\n }\n\n async recordOutcome(\n orgId: string,\n id: string,\n outcome: {\n status: AutomationRun[\"status\"];\n actionResults: AutomationRun[\"actionResults\"];\n finishedAt: Date;\n },\n ): Promise<AutomationRun | null> {\n const [updated] = await this.db\n .update(automationRuns)\n .set({\n status: outcome.status,\n actionResults: outcome.actionResults,\n finishedAt: outcome.finishedAt,\n })\n .where(and(eq(automationRuns.orgId, orgId), eq(automationRuns.id, id)))\n .returning(runSelection);\n return updated ? toAutomationRun(updated) : null;\n }\n\n async list(\n orgId: string,\n automationId: string,\n query: AutomationRunsQuery,\n ): Promise<Page<AutomationRun>> {\n const conditions: SQL[] = [\n eq(automationRuns.orgId, orgId),\n eq(automationRuns.automationId, automationId),\n ];\n if (query.status) {\n conditions.push(eq(automationRuns.status, query.status));\n }\n const where = and(...conditions);\n\n // Sort: `-field` for descending; default to most-recently started first.\n let orderBy: SQL;\n if (query.sort) {\n const isDesc = query.sort.startsWith(\"-\");\n const field = (isDesc ? query.sort.slice(1) : query.sort) as keyof typeof sortColumns;\n const col = sortColumns[field] ?? automationRuns.startedAt;\n orderBy = isDesc ? desc(col) : asc(col);\n } else {\n orderBy = desc(automationRuns.startedAt);\n }\n\n const offset = (query.page - 1) * query.pageSize;\n\n const rows = await this.db\n .select(runSelection)\n .from(automationRuns)\n .where(where)\n .orderBy(orderBy)\n .limit(query.pageSize)\n .offset(offset);\n\n const [{ total } = { total: 0 }] = await this.db\n .select({ total: sql<number>`cast(count(*) as int)` })\n .from(automationRuns)\n .where(where);\n\n return {\n rows: rows.map(toAutomationRun),\n total,\n page: query.page,\n pageSize: query.pageSize,\n };\n }\n}\ntranslateDbErrors(DrizzleAutomationsRepository);\ntranslateDbErrors(DrizzleAutomationRunsRepository);\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { DbExecutor } from "../client.js";
2
2
  import { Logger } from "@headless-lms/core/shared/ports";
3
- import { Activity, AddDownloadAssetInput, ContentRepository, Course, CreateCourseInput, CreateDownloadInput, Download, DownloadAsset, ListCoursesQuery, ListDownloadsQuery, Module, Page, SaveActivityInput, UpdateCourseInput, UpdateDownloadInput } from "@headless-lms/core/content";
3
+ import { Activity, ActivityAsset, AddDownloadAssetInput, Bundle, BundleItem, ContentRepository, Course, CourseSettings, CreateBundleInput, CreateCourseInput, CreateDownloadInput, Download, DownloadAsset, ListBundlesQuery, ListCoursesQuery, ListDownloadsQuery, Module, Page, SaveActivityInput, UpdateBundleInput, UpdateCourseInput, UpdateDownloadInput } from "@headless-lms/core/content";
4
4
  //#region src/repositories/content.d.ts
5
5
  declare class DrizzleContentRepository implements ContentRepository {
6
6
  private readonly db;
@@ -9,13 +9,15 @@ declare class DrizzleContentRepository implements ContentRepository {
9
9
  /** On the root db this opens a transaction; tx-bound (ContentUnitOfWork) it
10
10
  * nests as a savepoint inside the surrounding transaction. */
11
11
  private tx;
12
- list(orgId: string, query: ListCoursesQuery): Promise<Page<Course>>;
13
- findById(orgId: string, id: string): Promise<Course | null>;
14
- create(orgId: string, input: CreateCourseInput, slug: string): Promise<Course>;
15
- update(orgId: string, id: string, patch: UpdateCourseInput): Promise<Course | null>;
16
- delete(orgId: string, id: string): Promise<boolean>;
17
- listForCourse(orgId: string, courseId: string): Promise<Module[]>;
12
+ listCourses(orgId: string, query: ListCoursesQuery): Promise<Page<Course>>;
13
+ findCourseById(orgId: string, id: string): Promise<Course | null>;
14
+ createCourse(orgId: string, input: CreateCourseInput, slug: string): Promise<Course>;
15
+ updateCourse(orgId: string, id: string, patch: UpdateCourseInput): Promise<Course | null>;
16
+ patchCourseSettings(orgId: string, id: string, value: Partial<CourseSettings>): Promise<CourseSettings>;
17
+ deleteCourse(orgId: string, id: string): Promise<boolean>;
18
+ listCourseModules(orgId: string, courseId: string): Promise<Module[]>;
18
19
  listActivitiesForCourse(orgId: string, courseId: string): Promise<Activity[]>;
20
+ listActivityAssetsForCourse(orgId: string, courseId: string): Promise<ActivityAsset[]>;
19
21
  findActivity(orgId: string, activityId: string): Promise<Activity | null>;
20
22
  findModule(orgId: string, moduleId: string): Promise<Module | null>;
21
23
  /** The course's full ordered module list. */
@@ -46,6 +48,18 @@ declare class DrizzleContentRepository implements ContentRepository {
46
48
  removeDownloadAsset(orgId: string, downloadId: string, assetId: string): Promise<DownloadAsset[]>;
47
49
  renameDownloadAsset(orgId: string, downloadId: string, assetId: string, displayName: string | null): Promise<DownloadAsset[]>;
48
50
  reorderDownloadAssets(orgId: string, downloadId: string, assetIds: string[]): Promise<DownloadAsset[]>;
51
+ listBundles(orgId: string, query: ListBundlesQuery): Promise<Page<Bundle>>;
52
+ findBundleById(orgId: string, id: string): Promise<Bundle | null>;
53
+ /** The bundle and its initial items are one write: an unknown content id
54
+ * rolls the bundle back rather than leaving it created and half-filled. */
55
+ createBundle(orgId: string, input: CreateBundleInput): Promise<Bundle>;
56
+ updateBundle(orgId: string, id: string, patch: UpdateBundleInput): Promise<Bundle | null>;
57
+ deleteBundle(orgId: string, id: string): Promise<boolean>;
58
+ listBundleItems(orgId: string, bundleId: string): Promise<BundleItem[]>;
59
+ /** Assert the bundle exists in the org; throw otherwise. */
60
+ private assertBundle;
61
+ addBundleItem(orgId: string, bundleId: string, contentId: string): Promise<BundleItem[]>;
62
+ removeBundleItem(orgId: string, bundleId: string, contentId: string): Promise<BundleItem[]>;
49
63
  }
50
64
  //#endregion
51
65
  export { DrizzleContentRepository };