workplane 0.4.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/migrate.js CHANGED
@@ -17,11 +17,14 @@ __export(sqlite_exports, {
17
17
  runInputEvents: () => runInputEvents2,
18
18
  runLogs: () => runLogs2,
19
19
  runs: () => runs2,
20
- tasks: () => tasks2
20
+ tasks: () => tasks2,
21
+ workplanRuns: () => workplanRuns2,
22
+ workplanSchedules: () => workplanSchedules2,
23
+ workplanStepResults: () => workplanStepResults2
21
24
  });
22
25
  import { isNull as isNull2, sql as sql3 } from "drizzle-orm";
23
26
  import { index as index2, integer as integer2, sqliteTable, text as text2, uniqueIndex as uniqueIndex2 } from "drizzle-orm/sqlite-core";
24
- var nowDefault, tasks2, nodes2, runs2, runLogs2, artifacts2, runInputEvents2;
27
+ var nowDefault, tasks2, nodes2, runs2, runLogs2, artifacts2, workplanSchedules2, workplanRuns2, workplanStepResults2, runInputEvents2;
25
28
  var init_sqlite = __esm({
26
29
  "../db/src/schema/sqlite.ts"() {
27
30
  "use strict";
@@ -83,6 +86,49 @@ var init_sqlite = __esm({
83
86
  }, (table) => ({
84
87
  runIdIdx: index2("idx_artifacts_run_id").on(table.runId)
85
88
  }));
89
+ workplanSchedules2 = sqliteTable("workplan_schedules", {
90
+ id: text2("id").primaryKey(),
91
+ planId: text2("plan_id").notNull(),
92
+ name: text2("name").notNull(),
93
+ cronExpression: text2("cron_expression").notNull(),
94
+ timezone: text2("timezone").notNull(),
95
+ inputs: text2("inputs", { mode: "json" }).$type().notNull().default({}),
96
+ enabled: integer2("enabled", { mode: "boolean" }).notNull().default(true),
97
+ lastRunAt: text2("last_run_at"),
98
+ nextRunAt: text2("next_run_at"),
99
+ createdBy: text2("created_by"),
100
+ createdAt: text2("created_at").notNull().default(nowDefault),
101
+ updatedAt: text2("updated_at").notNull().default(nowDefault)
102
+ }, (table) => ({
103
+ dueIdx: index2("idx_workplan_schedules_due").on(table.nextRunAt)
104
+ }));
105
+ workplanRuns2 = sqliteTable("workplan_runs", {
106
+ id: text2("id").primaryKey(),
107
+ scheduleId: text2("schedule_id").references(() => workplanSchedules2.id),
108
+ planId: text2("plan_id").notNull(),
109
+ planName: text2("plan_name").notNull(),
110
+ status: text2("status").notNull().default("running"),
111
+ idempotencyKey: text2("idempotency_key"),
112
+ createdAt: text2("created_at").notNull().default(nowDefault),
113
+ endedAt: text2("ended_at"),
114
+ error: text2("error")
115
+ }, (table) => ({
116
+ scheduleIdx: index2("idx_workplan_runs_schedule_id").on(table.scheduleId),
117
+ idempotencyUnique: uniqueIndex2("idx_workplan_runs_idempotency").on(table.idempotencyKey)
118
+ }));
119
+ workplanStepResults2 = sqliteTable("workplan_step_results", {
120
+ id: text2("id").primaryKey(),
121
+ workplanRunId: text2("workplan_run_id").notNull().references(() => workplanRuns2.id),
122
+ stepId: text2("step_id").notNull(),
123
+ stepName: text2("step_name").notNull(),
124
+ output: text2("output"),
125
+ exitCode: integer2("exit_code"),
126
+ durationMs: integer2("duration_ms"),
127
+ metadata: text2("metadata", { mode: "json" }).$type(),
128
+ createdAt: text2("created_at").notNull().default(nowDefault)
129
+ }, (table) => ({
130
+ runIdx: index2("idx_workplan_step_results_run_id").on(table.workplanRunId)
131
+ }));
86
132
  runInputEvents2 = sqliteTable("run_input_events", {
87
133
  id: integer2("id").primaryKey({ autoIncrement: true }),
88
134
  runId: text2("run_id").notNull().references(() => runs2.id),
@@ -151,7 +197,7 @@ function parseCsv(value) {
151
197
  // ../db/src/migration.ts
152
198
  import { mkdirSync as mkdirSync2 } from "fs";
153
199
  import { homedir as homedir3 } from "os";
154
- import { dirname as dirname2, join as join3, resolve } from "path";
200
+ import { dirname as dirname2, join as join3, resolve as resolve2 } from "path";
155
201
  import { fileURLToPath } from "url";
156
202
 
157
203
  // ../db/src/client.ts
@@ -164,7 +210,7 @@ import Database from "better-sqlite3";
164
210
  import { Pool } from "pg";
165
211
 
166
212
  // ../db/src/pg-store.ts
167
- import { and, asc, desc, eq, gt, sql as sql2 } from "drizzle-orm";
213
+ import { and, arrayContains, asc, desc, eq, gt, isNotNull, lte, sql as sql2 } from "drizzle-orm";
168
214
 
169
215
  // ../db/src/schema/pg.ts
170
216
  var pg_exports = {};
@@ -174,10 +220,13 @@ __export(pg_exports, {
174
220
  runInputEvents: () => runInputEvents,
175
221
  runLogs: () => runLogs,
176
222
  runs: () => runs,
177
- tasks: () => tasks
223
+ tasks: () => tasks,
224
+ workplanRuns: () => workplanRuns,
225
+ workplanSchedules: () => workplanSchedules,
226
+ workplanStepResults: () => workplanStepResults
178
227
  });
179
228
  import { isNull, sql } from "drizzle-orm";
180
- import { bigint, bigserial, index, integer, jsonb, pgTable, text, timestamp, uniqueIndex } from "drizzle-orm/pg-core";
229
+ import { bigint, bigserial, boolean, index, integer, jsonb, pgTable, text, timestamp, uniqueIndex } from "drizzle-orm/pg-core";
181
230
  var tasks = pgTable("tasks", {
182
231
  id: text("id").primaryKey(),
183
232
  kind: text("kind").notNull(),
@@ -235,6 +284,49 @@ var artifacts = pgTable("artifacts", {
235
284
  }, (table) => ({
236
285
  runIdIdx: index("idx_artifacts_run_id").on(table.runId)
237
286
  }));
287
+ var workplanSchedules = pgTable("workplan_schedules", {
288
+ id: text("id").primaryKey(),
289
+ planId: text("plan_id").notNull(),
290
+ name: text("name").notNull(),
291
+ cronExpression: text("cron_expression").notNull(),
292
+ timezone: text("timezone").notNull(),
293
+ inputs: jsonb("inputs").$type().notNull().default({}),
294
+ enabled: boolean("enabled").notNull().default(true),
295
+ lastRunAt: timestamp("last_run_at", { withTimezone: true }),
296
+ nextRunAt: timestamp("next_run_at", { withTimezone: true }),
297
+ createdBy: text("created_by"),
298
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
299
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
300
+ }, (table) => ({
301
+ dueIdx: index("idx_workplan_schedules_due").on(table.nextRunAt)
302
+ }));
303
+ var workplanRuns = pgTable("workplan_runs", {
304
+ id: text("id").primaryKey(),
305
+ scheduleId: text("schedule_id").references(() => workplanSchedules.id),
306
+ planId: text("plan_id").notNull(),
307
+ planName: text("plan_name").notNull(),
308
+ status: text("status").notNull().default("running"),
309
+ idempotencyKey: text("idempotency_key"),
310
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
311
+ endedAt: timestamp("ended_at", { withTimezone: true }),
312
+ error: text("error")
313
+ }, (table) => ({
314
+ scheduleIdx: index("idx_workplan_runs_schedule_id").on(table.scheduleId),
315
+ idempotencyUnique: uniqueIndex("idx_workplan_runs_idempotency").on(table.idempotencyKey)
316
+ }));
317
+ var workplanStepResults = pgTable("workplan_step_results", {
318
+ id: text("id").primaryKey(),
319
+ workplanRunId: text("workplan_run_id").notNull().references(() => workplanRuns.id),
320
+ stepId: text("step_id").notNull(),
321
+ stepName: text("step_name").notNull(),
322
+ output: text("output"),
323
+ exitCode: integer("exit_code"),
324
+ durationMs: integer("duration_ms"),
325
+ metadata: jsonb("metadata").$type(),
326
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
327
+ }, (table) => ({
328
+ runIdx: index("idx_workplan_step_results_run_id").on(table.workplanRunId)
329
+ }));
238
330
  var runInputEvents = pgTable("run_input_events", {
239
331
  id: bigserial("id", { mode: "number" }).primaryKey(),
240
332
  runId: text("run_id").notNull().references(() => runs.id),
@@ -249,7 +341,7 @@ var runInputEvents = pgTable("run_input_events", {
249
341
  }));
250
342
 
251
343
  // ../db/src/sqlite-store.ts
252
- import { and as and2, asc as asc2, desc as desc2, eq as eq2, gt as gt2 } from "drizzle-orm";
344
+ import { and as and2, asc as asc2, desc as desc2, eq as eq2, gt as gt2, lte as lte2, sql as sql4 } from "drizzle-orm";
253
345
  init_sqlite();
254
346
 
255
347
  // ../db/src/client.ts
@@ -266,6 +358,144 @@ function getDrizzle(pool) {
266
358
  return drizzlePg(pool, { schema: pg_exports });
267
359
  }
268
360
 
361
+ // ../db/src/baseline-migrations.ts
362
+ import { createHash } from "crypto";
363
+ import { readFileSync } from "fs";
364
+ import { resolve } from "path";
365
+ function readJournal(migrationsFolder) {
366
+ return JSON.parse(
367
+ readFileSync(resolve(migrationsFolder, "meta/_journal.json"), "utf8")
368
+ );
369
+ }
370
+ function migrationHash(migrationsFolder, tag) {
371
+ const sql5 = readFileSync(resolve(migrationsFolder, `${tag}.sql`), "utf8");
372
+ return createHash("sha256").update(sql5).digest("hex");
373
+ }
374
+ function baselineEntries(migrationsFolder, upToIdx) {
375
+ const journal = readJournal(migrationsFolder);
376
+ return journal.entries.filter((entry) => entry.idx <= upToIdx).map((entry) => ({
377
+ hash: migrationHash(migrationsFolder, entry.tag),
378
+ createdAt: entry.when,
379
+ tag: entry.tag
380
+ }));
381
+ }
382
+ async function tableExistsPg(pool, tableName) {
383
+ const result = await pool.query(
384
+ `SELECT EXISTS (
385
+ SELECT 1 FROM information_schema.tables
386
+ WHERE table_schema = 'public' AND table_name = $1
387
+ ) AS exists`,
388
+ [tableName]
389
+ );
390
+ return Boolean(result.rows[0]?.exists);
391
+ }
392
+ async function getLastMigrationCreatedAtPg(pool) {
393
+ await pool.query("CREATE SCHEMA IF NOT EXISTS drizzle");
394
+ await pool.query(`
395
+ CREATE TABLE IF NOT EXISTS drizzle.__drizzle_migrations (
396
+ id SERIAL PRIMARY KEY,
397
+ hash text NOT NULL,
398
+ created_at bigint
399
+ )
400
+ `);
401
+ const result = await pool.query(
402
+ "SELECT created_at FROM drizzle.__drizzle_migrations ORDER BY created_at DESC LIMIT 1"
403
+ );
404
+ const value = result.rows[0]?.created_at;
405
+ return value === null || value === void 0 ? null : Number(value);
406
+ }
407
+ async function insertBaselinePg(pool, entries) {
408
+ const inserted = [];
409
+ const lastCreatedAt = await getLastMigrationCreatedAtPg(pool);
410
+ for (const entry of entries) {
411
+ if (lastCreatedAt !== null && lastCreatedAt >= entry.createdAt) {
412
+ continue;
413
+ }
414
+ const existing = await pool.query(
415
+ "SELECT id FROM drizzle.__drizzle_migrations WHERE created_at = $1 LIMIT 1",
416
+ [entry.createdAt]
417
+ );
418
+ if (existing.rowCount) {
419
+ continue;
420
+ }
421
+ await pool.query(
422
+ 'INSERT INTO drizzle.__drizzle_migrations ("hash", "created_at") VALUES ($1, $2)',
423
+ [entry.hash, entry.createdAt]
424
+ );
425
+ inserted.push(entry.tag);
426
+ }
427
+ return inserted;
428
+ }
429
+ async function baselineLegacyPostgresMigrations(pool, migrationsFolder) {
430
+ const hasTasks = await tableExistsPg(pool, "tasks");
431
+ if (!hasTasks) {
432
+ return;
433
+ }
434
+ const hasWorkplanSchedules = await tableExistsPg(pool, "workplan_schedules");
435
+ const upToIdx = hasWorkplanSchedules ? 1 : 0;
436
+ const inserted = await insertBaselinePg(pool, baselineEntries(migrationsFolder, upToIdx));
437
+ if (inserted.length > 0) {
438
+ process.stdout.write(
439
+ `Baselined existing Postgres schema for migration(s): ${inserted.join(", ")}
440
+ `
441
+ );
442
+ }
443
+ }
444
+ function tableExistsSqlite(db, tableName) {
445
+ const row = db.prepare(
446
+ "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1"
447
+ ).get(tableName);
448
+ return Boolean(row);
449
+ }
450
+ function getLastMigrationCreatedAtSqlite(db) {
451
+ db.exec(`
452
+ CREATE TABLE IF NOT EXISTS __drizzle_migrations (
453
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
454
+ hash text NOT NULL,
455
+ created_at numeric
456
+ )
457
+ `);
458
+ const row = db.prepare(
459
+ "SELECT created_at FROM __drizzle_migrations ORDER BY created_at DESC LIMIT 1"
460
+ ).get();
461
+ return row?.created_at ?? null;
462
+ }
463
+ function insertBaselineSqlite(db, entries) {
464
+ const inserted = [];
465
+ const lastCreatedAt = getLastMigrationCreatedAtSqlite(db);
466
+ const insert = db.prepare(
467
+ 'INSERT INTO __drizzle_migrations ("hash", "created_at") VALUES (?, ?)'
468
+ );
469
+ const exists = db.prepare(
470
+ "SELECT 1 FROM __drizzle_migrations WHERE created_at = ? LIMIT 1"
471
+ );
472
+ for (const entry of entries) {
473
+ if (lastCreatedAt !== null && lastCreatedAt >= entry.createdAt) {
474
+ continue;
475
+ }
476
+ if (exists.get(entry.createdAt)) {
477
+ continue;
478
+ }
479
+ insert.run(entry.hash, entry.createdAt);
480
+ inserted.push(entry.tag);
481
+ }
482
+ return inserted;
483
+ }
484
+ function baselineLegacySqliteMigrations(db, migrationsFolder) {
485
+ if (!tableExistsSqlite(db, "tasks")) {
486
+ return;
487
+ }
488
+ const hasWorkplanSchedules = tableExistsSqlite(db, "workplan_schedules");
489
+ const upToIdx = hasWorkplanSchedules ? 1 : 0;
490
+ const inserted = insertBaselineSqlite(db, baselineEntries(migrationsFolder, upToIdx));
491
+ if (inserted.length > 0) {
492
+ process.stdout.write(
493
+ `Baselined existing SQLite schema for migration(s): ${inserted.join(", ")}
494
+ `
495
+ );
496
+ }
497
+ }
498
+
269
499
  // ../db/src/migration.ts
270
500
  var distDir = dirname2(fileURLToPath(import.meta.url));
271
501
  async function migratePostgres(databaseUrl) {
@@ -296,8 +526,10 @@ async function migratePostgres(databaseUrl) {
296
526
  await adminPool.end();
297
527
  }
298
528
  const pool = getPool(databaseUrl);
529
+ const migrationsFolder = resolve2(distDir, "migrations/pg");
530
+ await baselineLegacyPostgresMigrations(pool, migrationsFolder);
299
531
  const { migrate } = await import("drizzle-orm/node-postgres/migrator");
300
- await migrate(getDrizzle(pool), { migrationsFolder: resolve(distDir, "migrations/pg") });
532
+ await migrate(getDrizzle(pool), { migrationsFolder });
301
533
  await pool.end();
302
534
  process.stdout.write("Postgres migrations applied.\n");
303
535
  }
@@ -312,8 +544,10 @@ async function migrateSqlite(databaseUrl) {
312
544
  const sqlite = new Database2(filePath);
313
545
  sqlite.pragma("journal_mode = WAL");
314
546
  sqlite.pragma("synchronous = NORMAL");
547
+ const migrationsFolder = resolve2(distDir, "migrations/sqlite");
548
+ baselineLegacySqliteMigrations(sqlite, migrationsFolder);
315
549
  const db = drizzle(sqlite, { schema: sqliteSchema });
316
- migrate(db, { migrationsFolder: resolve(distDir, "migrations/sqlite") });
550
+ migrate(db, { migrationsFolder });
317
551
  sqlite.close();
318
552
  process.stdout.write(`SQLite migrations applied: ${filePath}
319
553
  `);