@stacksjs/database 0.70.356 → 0.70.358

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 (2) hide show
  1. package/dist/custom/jobs.js +129 -2
  2. package/package.json +10 -10
@@ -1,4 +1,104 @@
1
- import{err,ok}from"@stacksjs/error-handling";import{existsSync,readdirSync,writeFileSync,mkdirSync}from"node:fs";import{join}from"node:path";import process from"node:process";import Database from"bun:sqlite";import{env as envVars}from"@stacksjs/env";import{resolveMigrationDirectory}from"../migration-path";function getDriver(){return envVars.DB_CONNECTION||"sqlite"}function getMigrationsPath(){return resolveMigrationDirectory(getDriver())}function hasJobsMigrationBeenCreated(){try{const migrationsPath=getMigrationsPath();if(!existsSync(migrationsPath))return!1;return readdirSync(migrationsPath).some((file)=>file.includes("create-jobs-table"))}catch{return!1}}async function jobBatchesTableExists(){const sqlitePath=getDatabasePath();if(existsSync(sqlitePath))try{const sqlite=new Database(sqlitePath),result=sqlite.query("SELECT name FROM sqlite_master WHERE type='table' AND name='job_batches'").get();sqlite.close();return result!==null}catch{}if(getDriver()!=="sqlite")try{const{db}=await import("../utils");await db.selectFrom("job_batches").select("id").limit(1).execute();return!0}catch{return!1}return!1}function getDatabasePath(){const appRoot=envVars.APP_ROOT||process.cwd();return join(appRoot,"database","stacks.sqlite")}async function jobsTableExists(){const driver=getDriver(),sqlitePath=getDatabasePath();if(existsSync(sqlitePath))try{const sqlite=new Database(sqlitePath),result=sqlite.query("SELECT name FROM sqlite_master WHERE type='table' AND name='jobs'").get();sqlite.close();return result!==null}catch{}if(driver!=="sqlite")try{const{db}=await import("../utils");await db.selectFrom("jobs").select("id").limit(1).execute();return!0}catch{return!1}return!1}export async function createJobsMigration(){try{const driver=getDriver();if(["sqlite","mysql","postgres"].includes(driver)){if(!hasJobsMigrationBeenCreated()){let migrationContent="";if(driver==="sqlite")migrationContent=`-- Create jobs table
1
+ import{err,ok}from"@stacksjs/error-handling";import{existsSync,readdirSync,writeFileSync,mkdirSync}from"node:fs";import{join}from"node:path";import process from"node:process";import Database from"bun:sqlite";import{env as envVars}from"@stacksjs/env";import{resolveMigrationDirectory}from"../migration-path";function getDriver(){return envVars.DB_CONNECTION||"sqlite"}function getMigrationsPath(){return resolveMigrationDirectory(getDriver())}function hasJobsMigrationBeenCreated(){try{const migrationsPath=getMigrationsPath();if(!existsSync(migrationsPath))return!1;return readdirSync(migrationsPath).some((file)=>file.includes("create-jobs-table"))}catch{return!1}}async function jobBatchesTableExists(){const sqlitePath=getDatabasePath();if(existsSync(sqlitePath))try{const sqlite=new Database(sqlitePath),result=sqlite.query("SELECT name FROM sqlite_master WHERE type='table' AND name='job_batches'").get();sqlite.close();return result!==null}catch{}if(getDriver()!=="sqlite")try{const{db}=await import("../utils");await db.selectFrom("job_batches").select("id").limit(1).execute();return!0}catch{return!1}return!1}function getDatabasePath(){const appRoot=envVars.APP_ROOT||process.cwd();return join(appRoot,"database","stacks.sqlite")}async function jobsTableExists(){const driver=getDriver(),sqlitePath=getDatabasePath();if(existsSync(sqlitePath))try{const sqlite=new Database(sqlitePath),result=sqlite.query("SELECT name FROM sqlite_master WHERE type='table' AND name='jobs'").get();sqlite.close();return result!==null}catch{}if(driver!=="sqlite")try{const{db}=await import("../utils");await db.selectFrom("jobs").select("id").limit(1).execute();return!0}catch{return!1}return!1}async function ensureQueueSupportTables(driver){const sqliteStatements=[`CREATE TABLE IF NOT EXISTS dead_letter_jobs (
2
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
3
+ uuid TEXT NOT NULL,
4
+ connection TEXT NOT NULL,
5
+ queue TEXT NOT NULL,
6
+ payload TEXT NOT NULL,
7
+ exception TEXT NOT NULL,
8
+ reason TEXT NOT NULL,
9
+ total_failures INTEGER NOT NULL DEFAULT 1,
10
+ first_failed_at DATETIME,
11
+ last_failed_at DATETIME,
12
+ dead_lettered_at DATETIME DEFAULT CURRENT_TIMESTAMP
13
+ )`,`CREATE TABLE IF NOT EXISTS job_quarantine (
14
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
15
+ job_name TEXT NOT NULL,
16
+ payload_hash TEXT NOT NULL,
17
+ failure_count INTEGER NOT NULL DEFAULT 0,
18
+ window_start DATETIME DEFAULT CURRENT_TIMESTAMP,
19
+ quarantined_at DATETIME,
20
+ UNIQUE(job_name, payload_hash)
21
+ )`,`CREATE TABLE IF NOT EXISTS queue_circuit_state (
22
+ queue_name TEXT PRIMARY KEY,
23
+ success_count INTEGER NOT NULL DEFAULT 0,
24
+ failure_count INTEGER NOT NULL DEFAULT 0,
25
+ window_start DATETIME DEFAULT CURRENT_TIMESTAMP,
26
+ paused_at DATETIME,
27
+ resume_at DATETIME
28
+ )`,`CREATE TABLE IF NOT EXISTS job_idempotency (
29
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
30
+ idempotency_key TEXT NOT NULL UNIQUE,
31
+ job_name TEXT NOT NULL,
32
+ queue TEXT NOT NULL DEFAULT 'default',
33
+ dispatched_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
34
+ )`];if(driver==="sqlite"){const sqlite=new Database(getDatabasePath());for(const statement of sqliteStatements)sqlite.run(statement);sqlite.close();return}const statements=driver==="mysql"?[`CREATE TABLE IF NOT EXISTS dead_letter_jobs (
35
+ id INT AUTO_INCREMENT PRIMARY KEY,
36
+ uuid VARCHAR(255) NOT NULL,
37
+ connection VARCHAR(255) NOT NULL,
38
+ queue VARCHAR(255) NOT NULL,
39
+ payload LONGTEXT NOT NULL,
40
+ exception LONGTEXT NOT NULL,
41
+ reason VARCHAR(64) NOT NULL,
42
+ total_failures INT NOT NULL DEFAULT 1,
43
+ first_failed_at TIMESTAMP NULL,
44
+ last_failed_at TIMESTAMP NULL,
45
+ dead_lettered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
46
+ )`,`CREATE TABLE IF NOT EXISTS job_quarantine (
47
+ id INT AUTO_INCREMENT PRIMARY KEY,
48
+ job_name VARCHAR(255) NOT NULL,
49
+ payload_hash VARCHAR(64) NOT NULL,
50
+ failure_count INT NOT NULL DEFAULT 0,
51
+ window_start TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
52
+ quarantined_at TIMESTAMP NULL,
53
+ UNIQUE KEY job_quarantine_unique (job_name, payload_hash)
54
+ )`,`CREATE TABLE IF NOT EXISTS queue_circuit_state (
55
+ queue_name VARCHAR(255) PRIMARY KEY,
56
+ success_count INT NOT NULL DEFAULT 0,
57
+ failure_count INT NOT NULL DEFAULT 0,
58
+ window_start TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
59
+ paused_at TIMESTAMP NULL,
60
+ resume_at TIMESTAMP NULL
61
+ )`,`CREATE TABLE IF NOT EXISTS job_idempotency (
62
+ id INT AUTO_INCREMENT PRIMARY KEY,
63
+ idempotency_key VARCHAR(255) NOT NULL,
64
+ job_name VARCHAR(255) NOT NULL,
65
+ queue VARCHAR(255) NOT NULL DEFAULT 'default',
66
+ dispatched_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
67
+ UNIQUE KEY job_idempotency_key_unique (idempotency_key)
68
+ )`]:[`CREATE TABLE IF NOT EXISTS dead_letter_jobs (
69
+ id SERIAL PRIMARY KEY,
70
+ uuid VARCHAR(255) NOT NULL,
71
+ connection VARCHAR(255) NOT NULL,
72
+ queue VARCHAR(255) NOT NULL,
73
+ payload TEXT NOT NULL,
74
+ exception TEXT NOT NULL,
75
+ reason VARCHAR(64) NOT NULL,
76
+ total_failures INTEGER NOT NULL DEFAULT 1,
77
+ first_failed_at TIMESTAMP,
78
+ last_failed_at TIMESTAMP,
79
+ dead_lettered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
80
+ )`,`CREATE TABLE IF NOT EXISTS job_quarantine (
81
+ id SERIAL PRIMARY KEY,
82
+ job_name VARCHAR(255) NOT NULL,
83
+ payload_hash VARCHAR(64) NOT NULL,
84
+ failure_count INTEGER NOT NULL DEFAULT 0,
85
+ window_start TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
86
+ quarantined_at TIMESTAMP,
87
+ UNIQUE (job_name, payload_hash)
88
+ )`,`CREATE TABLE IF NOT EXISTS queue_circuit_state (
89
+ queue_name VARCHAR(255) PRIMARY KEY,
90
+ success_count INTEGER NOT NULL DEFAULT 0,
91
+ failure_count INTEGER NOT NULL DEFAULT 0,
92
+ window_start TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
93
+ paused_at TIMESTAMP,
94
+ resume_at TIMESTAMP
95
+ )`,`CREATE TABLE IF NOT EXISTS job_idempotency (
96
+ id SERIAL PRIMARY KEY,
97
+ idempotency_key VARCHAR(255) NOT NULL UNIQUE,
98
+ job_name VARCHAR(255) NOT NULL,
99
+ queue VARCHAR(255) NOT NULL DEFAULT 'default',
100
+ dispatched_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
101
+ )`],{db}=await import("../utils");for(const statement of statements)await db.unsafe(statement).execute()}export async function createJobsMigration(){try{const driver=getDriver();if(["sqlite","mysql","postgres"].includes(driver)){if(!hasJobsMigrationBeenCreated()){let migrationContent="";if(driver==="sqlite")migrationContent=`-- Create jobs table
2
102
  CREATE TABLE IF NOT EXISTS jobs (
3
103
  id INTEGER PRIMARY KEY AUTOINCREMENT,
4
104
  queue TEXT NOT NULL DEFAULT 'default',
@@ -93,6 +193,16 @@ CREATE TABLE IF NOT EXISTS queue_circuit_state (
93
193
  paused_at DATETIME,
94
194
  resume_at DATETIME
95
195
  );
196
+
197
+ -- Dispatch idempotency (stacksjs/stacks#1872). The unique business key is
198
+ -- claimed before a queue push and released if the driver rejects it.
199
+ CREATE TABLE IF NOT EXISTS job_idempotency (
200
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
201
+ idempotency_key TEXT NOT NULL UNIQUE,
202
+ job_name TEXT NOT NULL,
203
+ queue TEXT NOT NULL DEFAULT 'default',
204
+ dispatched_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
205
+ );
96
206
  `;else if(driver==="mysql")migrationContent=`-- Create jobs table
97
207
  CREATE TABLE IF NOT EXISTS jobs (
98
208
  id INT AUTO_INCREMENT PRIMARY KEY,
@@ -173,6 +283,15 @@ CREATE TABLE IF NOT EXISTS queue_circuit_state (
173
283
  paused_at TIMESTAMP NULL,
174
284
  resume_at TIMESTAMP NULL
175
285
  );
286
+
287
+ CREATE TABLE IF NOT EXISTS job_idempotency (
288
+ id INT AUTO_INCREMENT PRIMARY KEY,
289
+ idempotency_key VARCHAR(255) NOT NULL,
290
+ job_name VARCHAR(255) NOT NULL,
291
+ queue VARCHAR(255) NOT NULL DEFAULT 'default',
292
+ dispatched_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
293
+ UNIQUE KEY job_idempotency_key_unique (idempotency_key)
294
+ );
176
295
  `;else if(driver==="postgres")migrationContent=`-- Create jobs table
177
296
  CREATE TABLE IF NOT EXISTS jobs (
178
297
  id SERIAL PRIMARY KEY,
@@ -253,6 +372,14 @@ CREATE TABLE IF NOT EXISTS queue_circuit_state (
253
372
  paused_at TIMESTAMP,
254
373
  resume_at TIMESTAMP
255
374
  );
375
+
376
+ CREATE TABLE IF NOT EXISTS job_idempotency (
377
+ id SERIAL PRIMARY KEY,
378
+ idempotency_key VARCHAR(255) NOT NULL UNIQUE,
379
+ job_name VARCHAR(255) NOT NULL,
380
+ queue VARCHAR(255) NOT NULL DEFAULT 'default',
381
+ dispatched_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
382
+ );
256
383
  `;const migrationFileName=`${new Date().getTime().toString()}-create-jobs-table.sql`,migrationsPath=getMigrationsPath();if(!existsSync(migrationsPath))mkdirSync(migrationsPath,{recursive:!0});const migrationFilePath=join(migrationsPath,migrationFileName);writeFileSync(migrationFilePath,migrationContent);console.log("\u2713 Created jobs migration file")}else console.log("\u2713 Jobs migration file already exists");if(!await jobsTableExists()){console.log(" Running migration...");const sqlitePath=getDatabasePath();if(driver==="sqlite"||existsSync(sqlitePath)){const sqlite=new Database(sqlitePath);sqlite.run(`CREATE TABLE IF NOT EXISTS jobs (
257
384
  id INTEGER PRIMARY KEY AUTOINCREMENT,
258
385
  queue TEXT NOT NULL DEFAULT 'default',
@@ -352,4 +479,4 @@ CREATE TABLE IF NOT EXISTS queue_circuit_state (
352
479
  cancelled_at TIMESTAMP,
353
480
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
354
481
  finished_at TIMESTAMP
355
- )`).execute()}console.log("\u2713 job_batches table created")}else console.log("\u2713 job_batches table already exists")}return ok("Migration created and executed.")}catch(error){return err(error instanceof Error?error:Error(String(error)))}}
482
+ )`).execute()}console.log("\u2713 job_batches table created")}else console.log("\u2713 job_batches table already exists");await ensureQueueSupportTables(driver);console.log("\u2713 Queue safeguard tables ready")}return ok("Migration created and executed.")}catch(error){return err(error instanceof Error?error:Error(String(error)))}}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/database",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.70.356",
5
+ "version": "0.70.358",
6
6
  "description": "The Stacks database integration.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -65,15 +65,15 @@
65
65
  "dynamodb-tooling": "^0.3.2"
66
66
  },
67
67
  "devDependencies": {
68
- "@stacksjs/cli": "0.70.356",
69
- "@stacksjs/config": "0.70.356",
70
- "@stacksjs/logging": "0.70.356",
71
- "@stacksjs/router": "0.70.356",
68
+ "@stacksjs/cli": "0.70.358",
69
+ "@stacksjs/config": "0.70.358",
70
+ "@stacksjs/logging": "0.70.358",
71
+ "@stacksjs/router": "0.70.358",
72
72
  "better-dx": "^0.2.17",
73
- "@stacksjs/path": "0.70.356",
74
- "@stacksjs/query-builder": "0.70.356",
75
- "@stacksjs/storage": "0.70.356",
76
- "@stacksjs/strings": "0.70.356",
77
- "@stacksjs/utils": "0.70.356"
73
+ "@stacksjs/path": "0.70.358",
74
+ "@stacksjs/query-builder": "0.70.358",
75
+ "@stacksjs/storage": "0.70.358",
76
+ "@stacksjs/strings": "0.70.358",
77
+ "@stacksjs/utils": "0.70.358"
78
78
  }
79
79
  }