diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e34cf74d2..5964b7a54 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,7 @@ jobs: - services - jobs - database-jobs + - database-jobs-v1 - uuid - types - stamps diff --git a/packages/database-jobs-v1/.npmignore b/packages/database-jobs-v1/.npmignore new file mode 100644 index 000000000..cf8a45548 --- /dev/null +++ b/packages/database-jobs-v1/.npmignore @@ -0,0 +1,2 @@ +__tests__ +jest.config.js diff --git a/packages/database-jobs-v1/LICENSE b/packages/database-jobs-v1/LICENSE new file mode 100644 index 000000000..7b18c9183 --- /dev/null +++ b/packages/database-jobs-v1/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2025 Dan Lynch +Copyright (c) 2025 Constructive + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/database-jobs-v1/Makefile b/packages/database-jobs-v1/Makefile new file mode 100644 index 000000000..d6e1dee21 --- /dev/null +++ b/packages/database-jobs-v1/Makefile @@ -0,0 +1,6 @@ +EXTENSION = pgpm-database-jobs +DATA = sql/pgpm-database-jobs--0.15.3.sql + +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) diff --git a/packages/database-jobs-v1/README.md b/packages/database-jobs-v1/README.md new file mode 100644 index 000000000..1015d10e3 --- /dev/null +++ b/packages/database-jobs-v1/README.md @@ -0,0 +1,363 @@ +# @pgpm/database-jobs + +

+ +

+ +

+ + + + + +

+ +Database-specific job handling and queue management. + +## Overview + +`@pgpm/database-jobs` provides a complete PostgreSQL-based background job processing system with persistent queues, scheduled jobs, and worker management. This package implements a robust job queue system entirely within PostgreSQL, enabling reliable background task processing with features like job locking, retries, priorities, and cron-style scheduling. + +## Features + +- **Persistent Job Queue**: Store jobs in PostgreSQL with ACID guarantees +- **Job Scheduling**: Cron-style and rule-based job scheduling +- **Worker Management**: Multiple workers with job locking and expiry +- **Priority Queue**: Process jobs by priority and run time +- **Automatic Retries**: Configurable retry attempts with exponential backoff +- **Job Keys**: Upsert semantics for idempotent job creation +- **Queue Management**: Named queues with independent locking +- **Notifications**: PostgreSQL LISTEN/NOTIFY for real-time job processing + +## Installation + +If you have `pgpm` installed: + +```bash +pgpm install @pgpm/database-jobs +pgpm deploy +``` + +This is a quick way to get started. The sections below provide more detailed installation options. + +### Prerequisites + +```bash +# Install pgpm CLI +npm install -g pgpm + +# Start local Postgres (via Docker) and export env vars +pgpm docker start +eval "$(pgpm env)" +``` + +> **Tip:** Already running Postgres? Skip the Docker step and just export your `PG*` environment variables. + +### **Add to an Existing Package** + +```bash +# 1. Install the package +pgpm install @pgpm/database-jobs + +# 2. Deploy locally +pgpm deploy +``` + +### **Add to a New Project** + +```bash +# 1. Create a workspace +pgpm init workspace + +# 2. Create your first module +cd my-workspace +pgpm init + +# 3. Install a package +cd packages/my-module +pgpm install @pgpm/database-jobs + +# 4. Deploy everything +pgpm deploy --createdb --database mydb1 +``` + +## Core Concepts + +### Jobs Table + +The `app_jobs.jobs` table stores active jobs with the following key fields: +- `id`: Unique job identifier +- `database_id`: Database/tenant identifier +- `task_identifier`: Job type/handler name +- `payload`: JSON data for the job +- `priority`: Lower numbers = higher priority (default: 0) +- `run_at`: When the job should run +- `attempts`: Current attempt count +- `max_attempts`: Maximum retry attempts (default: 25) +- `locked_by`: Worker ID that locked this job +- `locked_at`: When the job was locked +- `key`: Optional unique key for upsert semantics + +### Scheduled Jobs Table + +The `app_jobs.scheduled_jobs` table stores recurring jobs with cron-style or rule-based scheduling. + +### Job Queues Table + +The `app_jobs.job_queues` table tracks queue statistics and locking state. + +## Usage + +### Adding Jobs + +```sql +-- Add a simple job +SELECT app_jobs.add_job( + db_id := '5b720132-17d5-424d-9bcb-ee7b17c13d43'::uuid, + identifier := 'send_email', + payload := '{"to": "user@example.com", "subject": "Hello"}'::json +); + +-- Add a job with priority and delayed execution +SELECT app_jobs.add_job( + db_id := '5b720132-17d5-424d-9bcb-ee7b17c13d43'::uuid, + identifier := 'generate_report', + payload := '{"report_id": 123}'::json, + run_at := now() + interval '1 hour', + priority := 10, + max_attempts := 5 +); + +-- Add a job with a unique key (upsert semantics) +SELECT app_jobs.add_job( + db_id := '5b720132-17d5-424d-9bcb-ee7b17c13d43'::uuid, + identifier := 'daily_summary', + payload := '{"date": "2025-01-15"}'::json, + job_key := 'daily_summary_2025_01_15', + queue_name := 'reports' +); +``` + +### Getting Jobs (Worker Side) + +```sql +-- Worker fetches next available job +SELECT * FROM app_jobs.get_job( + worker_id := 'worker-1', + task_identifiers := ARRAY['send_email', 'generate_report'], + job_expiry := interval '4 hours' +); + +-- Returns NULL if no jobs available +-- Returns job row if job was successfully locked +``` + +### Completing Jobs + +```sql +-- Mark job as complete +SELECT app_jobs.complete_job( + worker_id := 'worker-1', + job_id := 123 +); +``` + +### Failing Jobs + +```sql +-- Mark job as failed (will retry if attempts < max_attempts) +SELECT app_jobs.fail_job( + worker_id := 'worker-1', + job_id := 123, + error_message := 'Connection timeout' +); +``` + +### Scheduled Jobs + +```sql +-- Schedule a job with cron-style timing +INSERT INTO app_jobs.scheduled_jobs ( + database_id, + task_identifier, + payload, + schedule_info +) VALUES ( + '5b720132-17d5-424d-9bcb-ee7b17c13d43'::uuid, + 'cleanup_old_data', + '{"days": 30}'::json, + '{ + "hour": [2], + "minute": [0], + "dayOfWeek": [0, 1, 2, 3, 4, 5, 6] + }'::json +); + +-- Schedule a job with a rule (every minute for 3 minutes) +SELECT app_jobs.add_scheduled_job( + db_id := '5b720132-17d5-424d-9bcb-ee7b17c13d43'::uuid, + identifier := 'heartbeat', + payload := '{}'::json, + schedule_info := json_build_object( + 'start', now() + interval '10 seconds', + 'end', now() + interval '3 minutes', + 'rule', '*/1 * * * *' + ) +); + +-- Run a scheduled job (creates a job in the jobs table) +SELECT * FROM app_jobs.run_scheduled_job(scheduled_job_id := 1); +``` + +## Functions Reference + +### app_jobs.add_job(...) + +Adds a new job to the queue or updates an existing job if a key is provided. + +**Parameters:** +- `db_id` (uuid): Database/tenant identifier +- `identifier` (text): Job type/handler name +- `payload` (json): Job data (default: `{}`) +- `job_key` (text): Optional unique key for upsert (default: NULL) +- `queue_name` (text): Optional queue name (default: random UUID) +- `run_at` (timestamptz): When to run (default: now()) +- `max_attempts` (integer): Maximum retries (default: 25) +- `priority` (integer): Job priority (default: 0) + +**Returns:** `app_jobs.jobs` row + +**Behavior:** +- If `job_key` is provided and exists, updates the job (if not locked) +- If job is locked, removes the key and creates a new job +- Triggers notifications for workers + +### app_jobs.get_job(...) + +Fetches and locks the next available job for a worker. + +**Parameters:** +- `worker_id` (text): Unique worker identifier +- `task_identifiers` (text[]): Optional filter for job types (default: NULL = all) +- `job_expiry` (interval): How long before locked jobs expire (default: 4 hours) + +**Returns:** `app_jobs.jobs` row or NULL + +**Behavior:** +- Selects jobs by priority, run_at, and id +- Locks the job and its queue +- Increments attempt counter +- Uses `FOR UPDATE SKIP LOCKED` for concurrency + +### app_jobs.complete_job(...) + +Marks a job as successfully completed and removes it from the queue. + +**Parameters:** +- `worker_id` (text): Worker that processed the job +- `job_id` (bigint): Job identifier + +**Returns:** `app_jobs.jobs` row + +### app_jobs.fail_job(...) + +Marks a job as failed and schedules retry if attempts remain. + +**Parameters:** +- `worker_id` (text): Worker that processed the job +- `job_id` (bigint): Job identifier +- `error_message` (text): Error description (default: NULL) + +**Returns:** `app_jobs.jobs` row + +**Behavior:** +- Records error message +- Unlocks the job for retry if attempts < max_attempts +- Permanently fails if max_attempts reached + +### app_jobs.add_scheduled_job(...) + +Creates a scheduled job with cron-style or rule-based timing. + +**Parameters:** +- `db_id` (uuid): Database/tenant identifier +- `identifier` (text): Job type/handler name +- `payload` (json): Job data +- `schedule_info` (json): Scheduling configuration +- `job_key` (text): Optional unique key +- `queue_name` (text): Optional queue name +- `max_attempts` (integer): Maximum retries +- `priority` (integer): Job priority + +**Returns:** `app_jobs.scheduled_jobs` row + +### app_jobs.run_scheduled_job(...) + +Executes a scheduled job by creating a job in the jobs table. + +**Parameters:** +- `scheduled_job_id` (bigint): Scheduled job identifier + +**Returns:** `app_jobs.jobs` row + +## Job Processing Pattern + +```sql +-- Worker loop (simplified) +LOOP + -- 1. Get next job + SELECT * FROM app_jobs.get_job('worker-1', ARRAY['my_task']); + + -- 2. Process job + -- ... application logic ... + + -- 3. Mark as complete or failed + IF success THEN + SELECT app_jobs.complete_job('worker-1', job_id); + ELSE + SELECT app_jobs.fail_job('worker-1', job_id, error_msg); + END IF; +END LOOP; +``` + +## Triggers and Automation + +The package includes several triggers for automatic management: + +- **timestamps**: Automatically sets created_at/updated_at +- **notify_worker**: Sends LISTEN/NOTIFY events when jobs are added +- **increase_job_queue_count**: Updates queue statistics on insert +- **decrease_job_queue_count**: Updates queue statistics on delete/update + +## Dependencies + +- PGPM roles (anonymous, authenticated, administrator) +- `@pgpm/verify`: Verification utilities for database objects + +## Testing + +```bash +pnpm test +``` + +The test suite validates: +- Job creation and retrieval +- Scheduled job creation with cron and rule-based timing +- Job key upsert semantics +- Worker locking and concurrency + +## Related Tooling + +* [pgpm](https://github.com/constructive-io/constructive/tree/main/packages/pgpm): **🖥️ PostgreSQL Package Manager** for modular Postgres development. Works with database workspaces, scaffolding, migrations, seeding, and installing database packages. +* [pgsql-test](https://github.com/constructive-io/constructive/tree/main/packages/pgsql-test): **📊 Isolated testing environments** with per-test transaction rollbacks—ideal for integration tests, complex migrations, and RLS simulation. +* [supabase-test](https://github.com/constructive-io/constructive/tree/main/packages/supabase-test): **🧪 Supabase-native test harness** preconfigured for the local Supabase stack—per-test rollbacks, JWT/role context helpers, and CI/GitHub Actions ready. +* [graphile-test](https://github.com/constructive-io/constructive/tree/main/packages/graphile-test): **🔐 Authentication mocking** for Graphile-focused test helpers and emulating row-level security contexts. +* [pgsql-parser](https://github.com/constructive-io/pgsql-parser): **🔄 SQL conversion engine** that interprets and converts PostgreSQL syntax. +* [libpg-query-node](https://github.com/constructive-io/libpg-query-node): **🌉 Node.js bindings** for `libpg_query`, converting SQL into parse trees. +* [pg-proto-parser](https://github.com/constructive-io/pg-proto-parser): **📦 Protobuf parser** for parsing PostgreSQL Protocol Buffers definitions to generate TypeScript interfaces, utility functions, and JSON mappings for enums. + +## Disclaimer + +AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND. + +No developer or entity involved in creating this software will be liable for any claims or damages whatsoever associated with your use, inability to use, or your interaction with other users of the code, including any direct, indirect, incidental, special, exemplary, punitive or consequential damages, or loss of profits, cryptocurrencies, tokens, or anything else of value. diff --git a/packages/database-jobs-v1/__tests__/__snapshots__/jobs.test.ts.snap b/packages/database-jobs-v1/__tests__/__snapshots__/jobs.test.ts.snap new file mode 100644 index 000000000..78807f7ca --- /dev/null +++ b/packages/database-jobs-v1/__tests__/__snapshots__/jobs.test.ts.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`scheduled jobs schedule jobs 1`] = ` +{ + "attempts": 0, + "database_id": "5b720132-17d5-424d-9bcb-ee7b17c13d43", + "id": "1", + "key": null, + "last_error": null, + "locked_at": null, + "locked_by": null, + "max_attempts": 25, + "payload": { + "just": "run it", + }, + "priority": 0, + "task_identifier": "my_job", +} +`; diff --git a/packages/database-jobs-v1/__tests__/jobs.test.ts b/packages/database-jobs-v1/__tests__/jobs.test.ts new file mode 100644 index 000000000..453de4c04 --- /dev/null +++ b/packages/database-jobs-v1/__tests__/jobs.test.ts @@ -0,0 +1,138 @@ +import { getConnections, PgTestClient } from 'pgsql-test'; + +let pg: PgTestClient; +let teardown: () => Promise; + +const database_id = '5b720132-17d5-424d-9bcb-ee7b17c13d43'; +const objs: Record = {}; + +describe('scheduled jobs', () => { + beforeAll(async () => { + ({ pg, teardown } = await getConnections()); + }); + + afterAll(async () => { + await teardown(); + }); + + it('schedule jobs by cron', async () => { + const result = await pg.one( + `INSERT INTO app_jobs.scheduled_jobs (database_id, task_identifier, schedule_info) + VALUES ($1, $2, $3) + RETURNING *`, + [ + database_id, + 'my_job', + { + hour: Array.from({ length: 23 }, (_, i) => i), + minute: [0, 15, 30, 45], + dayOfWeek: Array.from({ length: 6 }, (_, i) => i) + } + ] + ); + objs.scheduled1 = result; + }); + + it('schedule jobs by rule', async () => { + const start = new Date(Date.now() + 10000); // 10s from now + const end = new Date(start.getTime() + 180000); // +3min + + const result = await pg.one( + `INSERT INTO app_jobs.scheduled_jobs (database_id, task_identifier, payload, schedule_info) + VALUES ($1, $2, $3, $4) + RETURNING *`, + [ + database_id, + 'my_job', + { just: 'run it' }, + { start, end, rule: '*/1 * * * *' } + ] + ); + objs.scheduled2 = result; + }); + + it('schedule jobs', async () => { + const [result] = await pg.any( + `SELECT * FROM app_jobs.run_scheduled_job($1)`, + [objs.scheduled2.id] + ); + + const { queue_name, run_at, created_at, updated_at, ...obj } = result; + expect(obj).toMatchSnapshot(); + }); + + it('schedule jobs with keys', async () => { + const start = new Date(Date.now() + 10000); // 10s + const end = new Date(start.getTime() + 180000); // +3min + + const [result] = await pg.any( + `SELECT * FROM app_jobs.add_scheduled_job( + db_id := $1::uuid, + identifier := $2::text, + payload := $3::json, + schedule_info := $4::json, + job_key := $5::text, + queue_name := $6::text, + max_attempts := $7::integer, + priority := $8::integer + )`, + [ + database_id, + 'my_job', + { just: 'run it' }, + { start, end, rule: '*/1 * * * *' }, + 'new_key', + null, + 25, + 0 + ] + ); + + const { + queue_name, + run_at, + created_at, + updated_at, + schedule_info: sch, + start: s1, + end: d1, + ...obj + } = result; + + const [result2] = await pg.any( + `SELECT * FROM app_jobs.add_scheduled_job( + db_id := $1, + identifier := $2, + payload := $3, + schedule_info := $4, + job_key := $5, + queue_name := $6, + max_attempts := $7, + priority := $8 + )`, + [ + database_id, + 'my_job', + { just: 'run it' }, + { start, end, rule: '*/1 * * * *' }, + 'new_key', + null, + 25, + 0 + ] + ); + + const { + queue_name: qn, + created_at: ca, + updated_at: ua, + schedule_info: sch2, + start: s, + end: e, + ...obj2 + } = result2; + + console.log('First insert:', obj); + console.log('Duplicate insert (job_key conflict):', obj2); + }); +}); \ No newline at end of file diff --git a/packages/database-jobs-v1/deploy/schemas/app_jobs/helpers/json_build_object_apply.sql b/packages/database-jobs-v1/deploy/schemas/app_jobs/helpers/json_build_object_apply.sql new file mode 100644 index 000000000..2a8352488 --- /dev/null +++ b/packages/database-jobs-v1/deploy/schemas/app_jobs/helpers/json_build_object_apply.sql @@ -0,0 +1,28 @@ +-- Deploy schemas/app_jobs/helpers/json_build_object_apply to pg +-- requires: schemas/app_jobs/schema + +BEGIN; +CREATE FUNCTION app_jobs.json_build_object_apply (arguments text[]) + RETURNS json + AS $$ +DECLARE + arg text; + _sql text; + _res json; + args text[]; +BEGIN + _sql = 'SELECT json_build_object('; + FOR arg IN + SELECT + unnest(arguments) + LOOP + args = array_append(args, format('''%s''', arg)); + END LOOP; + _sql = _sql || format('%s);', array_to_string(args, ',')); + EXECUTE _sql INTO _res; + RETURN _res; +END; +$$ +LANGUAGE 'plpgsql'; +COMMIT; + diff --git a/packages/database-jobs-v1/deploy/schemas/app_jobs/procedures/add_job.sql b/packages/database-jobs-v1/deploy/schemas/app_jobs/procedures/add_job.sql new file mode 100644 index 000000000..1645b1a5a --- /dev/null +++ b/packages/database-jobs-v1/deploy/schemas/app_jobs/procedures/add_job.sql @@ -0,0 +1,106 @@ +-- Deploy schemas/app_jobs/procedures/add_job to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/jobs/table +-- requires: schemas/app_jobs/tables/job_queues/table + +BEGIN; +CREATE FUNCTION app_jobs.add_job ( + db_id uuid, + identifier text, + payload json DEFAULT '{}' ::json, + job_key text DEFAULT NULL, + queue_name text DEFAULT NULL, + run_at timestamptz DEFAULT now(), + max_attempts integer DEFAULT 25, + priority integer DEFAULT 0 +) + RETURNS app_jobs.jobs + AS $$ +DECLARE + v_job app_jobs.jobs; +BEGIN + -- Bake actor_id into payload + payload := (coalesce(payload, '{}'::json)::jsonb || jsonb_build_object('actor_id', jwt_public.current_user_id()))::json; + + IF job_key IS NOT NULL THEN + -- Upsert job + INSERT INTO app_jobs.jobs ( + database_id, + task_identifier, + payload, + queue_name, + run_at, + max_attempts, + key, + priority + ) VALUES ( + db_id, + identifier, + coalesce(payload, + '{}'::json), + queue_name, + coalesce(run_at, now()), + coalesce(max_attempts, 25), + job_key, + coalesce(priority, 0) + ) + ON CONFLICT (key) + DO UPDATE SET + task_identifier = EXCLUDED.task_identifier, + payload = EXCLUDED.payload, + queue_name = EXCLUDED.queue_name, + max_attempts = EXCLUDED.max_attempts, + run_at = EXCLUDED.run_at, + priority = EXCLUDED.priority, + -- always reset error/retry state + attempts = 0, last_error = NULL + WHERE + jobs.locked_at IS NULL + RETURNING + * INTO v_job; + + -- If upsert succeeded (insert or update), return early + + IF NOT (v_job IS NULL) THEN + RETURN v_job; + END IF; + + -- Upsert failed -> there must be an existing job that is locked. Remove + -- existing key to allow a new one to be inserted, and prevent any + -- subsequent retries by bumping attempts to the max allowed. + + UPDATE + app_jobs.jobs + SET + KEY = NULL, + attempts = jobs.max_attempts + WHERE + KEY = job_key; + END IF; + + INSERT INTO app_jobs.jobs ( + database_id, + task_identifier, + payload, + queue_name, + run_at, + max_attempts, + priority + ) VALUES ( + db_id, + identifier, + payload, + queue_name, + run_at, + max_attempts, + priority + ) + RETURNING * INTO v_job; + + RETURN v_job; +END; +$$ +LANGUAGE 'plpgsql' VOLATILE SECURITY DEFINER; + +COMMIT; + diff --git a/packages/database-jobs-v1/deploy/schemas/app_jobs/procedures/add_scheduled_job.sql b/packages/database-jobs-v1/deploy/schemas/app_jobs/procedures/add_scheduled_job.sql new file mode 100644 index 000000000..1cc20d055 --- /dev/null +++ b/packages/database-jobs-v1/deploy/schemas/app_jobs/procedures/add_scheduled_job.sql @@ -0,0 +1,97 @@ +-- Deploy schemas/app_jobs/procedures/add_scheduled_job to pg + +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/scheduled_jobs/table + +BEGIN; + +CREATE FUNCTION app_jobs.add_scheduled_job( + db_id uuid, + identifier text, + payload json DEFAULT '{}'::json, + schedule_info json DEFAULT '{}'::json, + job_key text DEFAULT NULL, + queue_name text DEFAULT NULL, + max_attempts integer DEFAULT 25, + priority integer DEFAULT 0 +) + RETURNS app_jobs.scheduled_jobs + AS $$ +DECLARE + v_job app_jobs.scheduled_jobs; +BEGIN + IF job_key IS NOT NULL THEN + + -- Upsert job + INSERT INTO app_jobs.scheduled_jobs ( + database_id, + task_identifier, + payload, + queue_name, + schedule_info, + max_attempts, + key, + priority + ) VALUES ( + db_id, + identifier, + coalesce(payload, '{}'::json), + queue_name, + schedule_info, + coalesce(max_attempts, 25), + job_key, + coalesce(priority, 0) + ) + ON CONFLICT (key) + DO UPDATE SET + task_identifier = EXCLUDED.task_identifier, + payload = EXCLUDED.payload, + queue_name = EXCLUDED.queue_name, + max_attempts = EXCLUDED.max_attempts, + schedule_info = EXCLUDED.schedule_info, + priority = EXCLUDED.priority + WHERE + scheduled_jobs.locked_at IS NULL + RETURNING + * INTO v_job; + + -- If upsert succeeded (insert or update), return early + + IF NOT (v_job IS NULL) THEN + RETURN v_job; + END IF; + + -- Upsert failed -> there must be an existing scheduled job that is locked. Remove + -- and allow a new one to be inserted + + DELETE FROM + app_jobs.scheduled_jobs + WHERE + KEY = job_key; + END IF; + + INSERT INTO app_jobs.scheduled_jobs ( + database_id, + task_identifier, + payload, + queue_name, + schedule_info, + max_attempts, + priority + ) VALUES ( + db_id, + identifier, + payload, + queue_name, + schedule_info, + max_attempts, + priority + ) RETURNING * INTO v_job; + RETURN v_job; +END; +$$ +LANGUAGE 'plpgsql' +VOLATILE +SECURITY DEFINER; +COMMIT; + diff --git a/packages/database-jobs-v1/deploy/schemas/app_jobs/procedures/complete_job.sql b/packages/database-jobs-v1/deploy/schemas/app_jobs/procedures/complete_job.sql new file mode 100644 index 000000000..afafdbb2b --- /dev/null +++ b/packages/database-jobs-v1/deploy/schemas/app_jobs/procedures/complete_job.sql @@ -0,0 +1,32 @@ +-- Deploy schemas/app_jobs/procedures/complete_job to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/jobs/table +-- requires: schemas/app_jobs/tables/job_queues/table + +BEGIN; +CREATE FUNCTION app_jobs.complete_job (worker_id text, job_id bigint) + RETURNS app_jobs.jobs + LANGUAGE plpgsql + AS $$ +DECLARE + v_row app_jobs.jobs; +BEGIN + DELETE FROM app_jobs.jobs + WHERE id = job_id + RETURNING + * INTO v_row; + IF v_row.queue_name IS NOT NULL THEN + UPDATE + app_jobs.job_queues + SET + locked_by = NULL, + locked_at = NULL + WHERE + queue_name = v_row.queue_name + AND locked_by = worker_id; + END IF; + RETURN v_row; +END; +$$; +COMMIT; + diff --git a/packages/database-jobs-v1/deploy/schemas/app_jobs/procedures/complete_jobs.sql b/packages/database-jobs-v1/deploy/schemas/app_jobs/procedures/complete_jobs.sql new file mode 100644 index 000000000..1b14dffc5 --- /dev/null +++ b/packages/database-jobs-v1/deploy/schemas/app_jobs/procedures/complete_jobs.sql @@ -0,0 +1,19 @@ +-- Deploy schemas/app_jobs/procedures/complete_jobs to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/job_queues/table +-- requires: schemas/app_jobs/tables/jobs/table + +BEGIN; +CREATE FUNCTION app_jobs.complete_jobs (job_ids bigint[]) + RETURNS SETOF app_jobs.jobs + LANGUAGE sql + AS $$ + DELETE FROM app_jobs.jobs + WHERE id = ANY (job_ids) + AND (locked_by IS NULL + OR locked_at < NOW() - interval '4 hours') + RETURNING + *; +$$; +COMMIT; + diff --git a/packages/database-jobs-v1/deploy/schemas/app_jobs/procedures/do_notify.sql b/packages/database-jobs-v1/deploy/schemas/app_jobs/procedures/do_notify.sql new file mode 100644 index 000000000..82d92525a --- /dev/null +++ b/packages/database-jobs-v1/deploy/schemas/app_jobs/procedures/do_notify.sql @@ -0,0 +1,16 @@ +-- Deploy schemas/app_jobs/procedures/do_notify to pg +-- requires: schemas/app_jobs/schema + +BEGIN; +CREATE FUNCTION app_jobs.do_notify () + RETURNS TRIGGER + AS $$ +BEGIN + PERFORM + pg_notify(TG_ARGV[0], ''); + RETURN NEW; +END; +$$ +LANGUAGE plpgsql; +COMMIT; + diff --git a/packages/database-jobs-v1/deploy/schemas/app_jobs/procedures/fail_job.sql b/packages/database-jobs-v1/deploy/schemas/app_jobs/procedures/fail_job.sql new file mode 100644 index 000000000..a5b38c686 --- /dev/null +++ b/packages/database-jobs-v1/deploy/schemas/app_jobs/procedures/fail_job.sql @@ -0,0 +1,41 @@ +-- Deploy schemas/app_jobs/procedures/fail_job to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/jobs/table +-- requires: schemas/app_jobs/tables/job_queues/table + +BEGIN; +CREATE FUNCTION app_jobs.fail_job (worker_id text, job_id bigint, error_message text) + RETURNS app_jobs.jobs + LANGUAGE plpgsql + STRICT + AS $$ +DECLARE + v_row app_jobs.jobs; +BEGIN + UPDATE + app_jobs.jobs + SET + last_error = error_message, + run_at = greatest (now(), run_at) + (exp(least (attempts, 10))::text || ' seconds')::interval, + locked_by = NULL, + locked_at = NULL + WHERE + id = job_id + AND locked_by = worker_id + RETURNING + * INTO v_row; + IF v_row.queue_name IS NOT NULL THEN + UPDATE + app_jobs.job_queues + SET + locked_by = NULL, + locked_at = NULL + WHERE + queue_name = v_row.queue_name + AND locked_by = worker_id; + END IF; + RETURN v_row; +END; +$$; +COMMIT; + diff --git a/packages/database-jobs-v1/deploy/schemas/app_jobs/procedures/get_job.sql b/packages/database-jobs-v1/deploy/schemas/app_jobs/procedures/get_job.sql new file mode 100644 index 000000000..9c37d9c94 --- /dev/null +++ b/packages/database-jobs-v1/deploy/schemas/app_jobs/procedures/get_job.sql @@ -0,0 +1,92 @@ +-- Deploy schemas/app_jobs/procedures/get_job to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/job_queues/table +-- requires: schemas/app_jobs/tables/jobs/table + +BEGIN; +CREATE FUNCTION app_jobs.get_job (worker_id text, task_identifiers text[] DEFAULT NULL, job_expiry interval DEFAULT '4 hours') + RETURNS app_jobs.jobs + LANGUAGE plpgsql + AS $$ +DECLARE + v_job_id bigint; + v_queue_name text; + v_row app_jobs.jobs; + v_now timestamptz = now(); +BEGIN + + IF worker_id IS NULL THEN + RAISE exception 'INVALID_WORKER_ID'; + END IF; + + -- + + SELECT + jobs.queue_name, + jobs.id INTO v_queue_name, + v_job_id + FROM + app_jobs.jobs + WHERE (jobs.locked_at IS NULL + OR jobs.locked_at < (v_now - job_expiry)) + AND (jobs.queue_name IS NULL + OR EXISTS ( + SELECT + 1 + FROM + app_jobs.job_queues + WHERE + job_queues.queue_name = jobs.queue_name + AND (job_queues.locked_at IS NULL + OR job_queues.locked_at < (v_now - job_expiry)) + FOR UPDATE + SKIP LOCKED)) + AND run_at <= v_now + AND attempts < max_attempts + AND (task_identifiers IS NULL + OR task_identifier = ANY (task_identifiers)) + ORDER BY + priority ASC, + run_at ASC, + id ASC + LIMIT 1 + FOR UPDATE + SKIP LOCKED; + + -- + + IF v_job_id IS NULL THEN + RETURN NULL; + END IF; + + -- + + IF v_queue_name IS NOT NULL THEN + UPDATE + app_jobs.job_queues + SET + locked_by = worker_id, + locked_at = v_now + WHERE + job_queues.queue_name = v_queue_name; + END IF; + + -- + + UPDATE + app_jobs.jobs + SET + attempts = attempts + 1, + locked_by = worker_id, + locked_at = v_now + WHERE + id = v_job_id + RETURNING + * INTO v_row; + + -- + RETURN v_row; +END; +$$; +COMMIT; + diff --git a/packages/database-jobs-v1/deploy/schemas/app_jobs/procedures/get_scheduled_job.sql b/packages/database-jobs-v1/deploy/schemas/app_jobs/procedures/get_scheduled_job.sql new file mode 100644 index 000000000..b8fa5a664 --- /dev/null +++ b/packages/database-jobs-v1/deploy/schemas/app_jobs/procedures/get_scheduled_job.sql @@ -0,0 +1,61 @@ +-- Deploy schemas/app_jobs/procedures/get_scheduled_job to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/scheduled_jobs/table + +BEGIN; +CREATE FUNCTION app_jobs.get_scheduled_job (worker_id text, task_identifiers text[] DEFAULT NULL) + RETURNS app_jobs.scheduled_jobs + LANGUAGE plpgsql + AS $$ +DECLARE + v_job_id bigint; + v_row app_jobs.scheduled_jobs; +BEGIN + + -- + + IF worker_id IS NULL THEN + RAISE exception 'INVALID_WORKER_ID'; + END IF; + + -- + + SELECT + scheduled_jobs.id INTO v_job_id + FROM + app_jobs.scheduled_jobs + WHERE (scheduled_jobs.locked_at IS NULL) + AND (task_identifiers IS NULL + OR task_identifier = ANY (task_identifiers)) + ORDER BY + priority ASC, + id ASC + LIMIT 1 + FOR UPDATE + SKIP LOCKED; + + -- + + IF v_job_id IS NULL THEN + RETURN NULL; + END IF; + + -- + + UPDATE + app_jobs.scheduled_jobs + SET + locked_by = worker_id, + locked_at = NOW() + WHERE + id = v_job_id + RETURNING + * INTO v_row; + + -- + + RETURN v_row; +END; +$$; +COMMIT; + diff --git a/packages/database-jobs-v1/deploy/schemas/app_jobs/procedures/permanently_fail_jobs.sql b/packages/database-jobs-v1/deploy/schemas/app_jobs/procedures/permanently_fail_jobs.sql new file mode 100644 index 000000000..3c7328062 --- /dev/null +++ b/packages/database-jobs-v1/deploy/schemas/app_jobs/procedures/permanently_fail_jobs.sql @@ -0,0 +1,24 @@ +-- Deploy schemas/app_jobs/procedures/permanently_fail_jobs to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/job_queues/table +-- requires: schemas/app_jobs/tables/jobs/table + +BEGIN; +CREATE FUNCTION app_jobs.permanently_fail_jobs (job_ids bigint[], error_message text DEFAULT NULL) + RETURNS SETOF app_jobs.jobs + LANGUAGE sql + AS $$ + UPDATE + app_jobs.jobs + SET + last_error = coalesce(error_message, 'Manually marked as failed'), + attempts = max_attempts + WHERE + id = ANY (job_ids) + AND (locked_by IS NULL + OR locked_at < NOW() - interval '4 hours') + RETURNING + *; +$$; +COMMIT; + diff --git a/packages/database-jobs-v1/deploy/schemas/app_jobs/procedures/release_jobs.sql b/packages/database-jobs-v1/deploy/schemas/app_jobs/procedures/release_jobs.sql new file mode 100644 index 000000000..2fd063367 --- /dev/null +++ b/packages/database-jobs-v1/deploy/schemas/app_jobs/procedures/release_jobs.sql @@ -0,0 +1,34 @@ +-- Deploy schemas/app_jobs/procedures/release_jobs to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/jobs/table +-- requires: schemas/app_jobs/tables/job_queues/table + +BEGIN; +CREATE FUNCTION app_jobs.release_jobs (worker_id text) + RETURNS void + AS $$ +DECLARE +BEGIN + -- clear the job + UPDATE + app_jobs.jobs + SET + locked_at = NULL, + locked_by = NULL, + attempts = GREATEST (attempts - 1, 0) + WHERE + locked_by = worker_id; + -- clear the queue + UPDATE + app_jobs.job_queues + SET + locked_at = NULL, + locked_by = NULL + WHERE + locked_by = worker_id; +END; +$$ +LANGUAGE 'plpgsql' +VOLATILE; +COMMIT; + diff --git a/packages/database-jobs-v1/deploy/schemas/app_jobs/procedures/release_scheduled_jobs.sql b/packages/database-jobs-v1/deploy/schemas/app_jobs/procedures/release_scheduled_jobs.sql new file mode 100644 index 000000000..ec66b60a1 --- /dev/null +++ b/packages/database-jobs-v1/deploy/schemas/app_jobs/procedures/release_scheduled_jobs.sql @@ -0,0 +1,26 @@ +-- Deploy schemas/app_jobs/procedures/release_scheduled_jobs to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/scheduled_jobs/table + +BEGIN; +CREATE FUNCTION app_jobs.release_scheduled_jobs (worker_id text, ids bigint[] DEFAULT NULL) + RETURNS void + AS $$ +DECLARE +BEGIN + -- clear the scheduled job + UPDATE + app_jobs.scheduled_jobs s + SET + locked_at = NULL, + locked_by = NULL + WHERE + locked_by = worker_id + AND (ids IS NULL + OR s.id = ANY (ids)); +END; +$$ +LANGUAGE 'plpgsql' +VOLATILE; +COMMIT; + diff --git a/packages/database-jobs-v1/deploy/schemas/app_jobs/procedures/reschedule_jobs.sql b/packages/database-jobs-v1/deploy/schemas/app_jobs/procedures/reschedule_jobs.sql new file mode 100644 index 000000000..b39d884db --- /dev/null +++ b/packages/database-jobs-v1/deploy/schemas/app_jobs/procedures/reschedule_jobs.sql @@ -0,0 +1,26 @@ +-- Deploy schemas/app_jobs/procedures/reschedule_jobs to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/jobs/table + +BEGIN; +-- NOTE this should be renamed to reset_jobs to avoid confusion of scheduled jobs +CREATE FUNCTION app_jobs.reschedule_jobs (job_ids bigint[], run_at timestamptz DEFAULT NULL, priority integer DEFAULT NULL, attempts integer DEFAULT NULL, max_attempts integer DEFAULT NULL) + RETURNS SETOF app_jobs.jobs + LANGUAGE sql + AS $$ + UPDATE + app_jobs.jobs + SET + run_at = coalesce(reschedule_jobs.run_at, jobs.run_at), + priority = coalesce(reschedule_jobs.priority, jobs.priority), + attempts = coalesce(reschedule_jobs.attempts, jobs.attempts), + max_attempts = coalesce(reschedule_jobs.max_attempts, jobs.max_attempts) + WHERE + id = ANY (job_ids) + AND (locked_by IS NULL + OR locked_at < NOW() - interval '4 hours') + RETURNING + *; +$$; +COMMIT; + diff --git a/packages/database-jobs-v1/deploy/schemas/app_jobs/procedures/run_scheduled_job.sql b/packages/database-jobs-v1/deploy/schemas/app_jobs/procedures/run_scheduled_job.sql new file mode 100644 index 000000000..41c258b35 --- /dev/null +++ b/packages/database-jobs-v1/deploy/schemas/app_jobs/procedures/run_scheduled_job.sql @@ -0,0 +1,78 @@ +-- Deploy schemas/app_jobs/procedures/run_scheduled_job to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/jobs/table +-- requires: schemas/app_jobs/tables/scheduled_jobs/table + +BEGIN; +CREATE FUNCTION app_jobs.run_scheduled_job (id bigint, job_expiry interval DEFAULT '1 hours') + RETURNS app_jobs.jobs + AS $$ +DECLARE + j app_jobs.jobs; + last_id bigint; + lkd_by text; +BEGIN + -- check last scheduled + SELECT + last_scheduled_id + FROM + app_jobs.scheduled_jobs s + WHERE + s.id = run_scheduled_job.id INTO last_id; + + -- if it's been scheduled check if it's been run + + IF (last_id IS NOT NULL) THEN + SELECT + locked_by + FROM + app_jobs.jobs js + WHERE + js.id = last_id + AND (js.locked_at IS NULL -- never been run + OR js.locked_at >= (NOW() - job_expiry) + -- still running within a safe interval +) INTO lkd_by; + IF (FOUND) THEN + RAISE EXCEPTION 'ALREADY_SCHEDULED'; + END IF; + END IF; + + -- insert new job + INSERT INTO app_jobs.jobs ( + database_id, + queue_name, + task_identifier, + payload, + priority, + max_attempts, + key + ) SELECT + database_id, + queue_name, + task_identifier, + payload, + priority, + max_attempts, + key + FROM + app_jobs.scheduled_jobs s + WHERE + s.id = run_scheduled_job.id + RETURNING + * INTO j; + -- update the scheduled job + UPDATE + app_jobs.scheduled_jobs s + SET + last_scheduled = NOW(), + last_scheduled_id = j.id + WHERE + s.id = run_scheduled_job.id; + RETURN j; +END; +$$ +LANGUAGE 'plpgsql' +VOLATILE; +COMMIT; + diff --git a/packages/database-jobs-v1/deploy/schemas/app_jobs/schema.sql b/packages/database-jobs-v1/deploy/schemas/app_jobs/schema.sql new file mode 100644 index 000000000..86a5b4eeb --- /dev/null +++ b/packages/database-jobs-v1/deploy/schemas/app_jobs/schema.sql @@ -0,0 +1,7 @@ +-- Deploy schemas/app_jobs/schema to pg +BEGIN; +CREATE SCHEMA IF NOT EXISTS app_jobs; +GRANT USAGE ON SCHEMA app_jobs TO administrator; +ALTER DEFAULT PRIVILEGES IN SCHEMA app_jobs GRANT EXECUTE ON FUNCTIONS TO administrator; +COMMIT; + diff --git a/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/job_queues/grants/grant_select_insert_update_delete_to_administrator.sql b/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/job_queues/grants/grant_select_insert_update_delete_to_administrator.sql new file mode 100644 index 000000000..6f0fc3be9 --- /dev/null +++ b/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/job_queues/grants/grant_select_insert_update_delete_to_administrator.sql @@ -0,0 +1,12 @@ +-- Deploy schemas/app_jobs/tables/job_queues/grants/grant_select_insert_update_delete_to_administrator to pg + +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/job_queues/table + +BEGIN; + +-- TODO make sure to require any policies on this table! + +GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE app_jobs.job_queues TO administrator; + +COMMIT; diff --git a/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/job_queues/indexes/job_queues_locked_by_idx.sql b/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/job_queues/indexes/job_queues_locked_by_idx.sql new file mode 100644 index 000000000..cc78f18a5 --- /dev/null +++ b/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/job_queues/indexes/job_queues_locked_by_idx.sql @@ -0,0 +1,8 @@ +-- Deploy schemas/app_jobs/tables/job_queues/indexes/job_queues_locked_by_idx to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/job_queues/table + +BEGIN; +CREATE INDEX job_queues_locked_by_idx ON app_jobs.job_queues (locked_by); +COMMIT; + diff --git a/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/job_queues/table.sql b/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/job_queues/table.sql new file mode 100644 index 000000000..dd003c348 --- /dev/null +++ b/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/job_queues/table.sql @@ -0,0 +1,19 @@ +-- Deploy schemas/app_jobs/tables/job_queues/table to pg +-- requires: schemas/app_jobs/schema + +BEGIN; +CREATE TABLE app_jobs.job_queues ( + queue_name text NOT NULL PRIMARY KEY, + job_count int DEFAULT 0 NOT NULL, + locked_at timestamptz, + locked_by text +); + +COMMENT ON TABLE app_jobs.job_queues IS 'Queue metadata: tracks job counts and locking state for each named queue'; +COMMENT ON COLUMN app_jobs.job_queues.queue_name IS 'Unique name identifying this queue'; +COMMENT ON COLUMN app_jobs.job_queues.job_count IS 'Number of pending jobs in this queue'; +COMMENT ON COLUMN app_jobs.job_queues.locked_at IS 'Timestamp when this queue was locked for batch processing'; +COMMENT ON COLUMN app_jobs.job_queues.locked_by IS 'Identifier of the worker that currently holds the queue lock'; + +COMMIT; + diff --git a/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/jobs/grants/grant_select_insert_update_delete_to_administrator.sql b/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/jobs/grants/grant_select_insert_update_delete_to_administrator.sql new file mode 100644 index 000000000..11a3ac344 --- /dev/null +++ b/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/jobs/grants/grant_select_insert_update_delete_to_administrator.sql @@ -0,0 +1,12 @@ +-- Deploy schemas/app_jobs/tables/jobs/grants/grant_select_insert_update_delete_to_administrator to pg + +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/jobs/table + +BEGIN; + +-- TODO make sure to require any policies on this table! + +GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE app_jobs.jobs TO administrator; + +COMMIT; diff --git a/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/jobs/indexes/jobs_locked_by_idx.sql b/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/jobs/indexes/jobs_locked_by_idx.sql new file mode 100644 index 000000000..d41680373 --- /dev/null +++ b/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/jobs/indexes/jobs_locked_by_idx.sql @@ -0,0 +1,8 @@ +-- Deploy schemas/app_jobs/tables/jobs/indexes/jobs_locked_by_idx to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/jobs/table + +BEGIN; +CREATE INDEX jobs_locked_by_idx ON app_jobs.jobs (locked_by); +COMMIT; + diff --git a/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql b/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql new file mode 100644 index 000000000..78b03ff13 --- /dev/null +++ b/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql @@ -0,0 +1,8 @@ +-- Deploy schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/jobs/table + +BEGIN; +CREATE INDEX priority_run_at_id_idx ON app_jobs.jobs (priority, run_at, id); +COMMIT; + diff --git a/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/jobs/table.sql b/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/jobs/table.sql new file mode 100644 index 000000000..48ea7c146 --- /dev/null +++ b/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/jobs/table.sql @@ -0,0 +1,43 @@ +-- Deploy schemas/app_jobs/tables/jobs/table to pg +-- requires: schemas/app_jobs/schema + +BEGIN; +CREATE TABLE app_jobs.jobs ( + id bigserial PRIMARY KEY, + database_id uuid NOT NULL, + queue_name text DEFAULT (public.gen_random_uuid ()) ::text, + task_identifier text NOT NULL, + payload json DEFAULT '{}' ::json NOT NULL, + priority integer DEFAULT 0 NOT NULL, + run_at timestamptz DEFAULT now() NOT NULL, + attempts integer DEFAULT 0 NOT NULL, + max_attempts integer DEFAULT 25 NOT NULL, + key text, + last_error text, + locked_at timestamptz, + locked_by text, + CHECK (length(key) < 513), + CHECK (length(task_identifier) < 127), + CHECK (max_attempts > 0), + CHECK (length(queue_name) < 127), + CHECK (length(locked_by) > 3), + UNIQUE (key) +); + +COMMENT ON TABLE app_jobs.jobs IS 'Background job queue with database scoping: each row is a pending or in-progress task for a specific database'; +COMMENT ON COLUMN app_jobs.jobs.id IS 'Auto-incrementing job identifier'; +COMMENT ON COLUMN app_jobs.jobs.database_id IS 'Database this job belongs to, for multi-tenant job isolation'; +COMMENT ON COLUMN app_jobs.jobs.queue_name IS 'Name of the queue this job belongs to; used for worker routing and concurrency control'; +COMMENT ON COLUMN app_jobs.jobs.task_identifier IS 'Identifier for the task type (maps to a worker handler function)'; +COMMENT ON COLUMN app_jobs.jobs.payload IS 'JSON payload of arguments passed to the task handler'; +COMMENT ON COLUMN app_jobs.jobs.priority IS 'Execution priority; lower numbers run first (default 0)'; +COMMENT ON COLUMN app_jobs.jobs.run_at IS 'Earliest time this job should be executed; used for delayed/scheduled execution'; +COMMENT ON COLUMN app_jobs.jobs.attempts IS 'Number of times this job has been attempted so far'; +COMMENT ON COLUMN app_jobs.jobs.max_attempts IS 'Maximum retry attempts before the job is considered permanently failed'; +COMMENT ON COLUMN app_jobs.jobs.key IS 'Optional unique deduplication key; prevents duplicate jobs with the same key'; +COMMENT ON COLUMN app_jobs.jobs.last_error IS 'Error message from the most recent failed attempt'; +COMMENT ON COLUMN app_jobs.jobs.locked_at IS 'Timestamp when a worker locked this job for processing'; +COMMENT ON COLUMN app_jobs.jobs.locked_by IS 'Identifier of the worker that currently holds the lock'; + +COMMIT; + diff --git a/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count.sql b/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count.sql new file mode 100644 index 000000000..c87bf7bce --- /dev/null +++ b/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count.sql @@ -0,0 +1,45 @@ +-- Deploy schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/jobs/table + +BEGIN; +CREATE FUNCTION app_jobs.tg_decrease_job_queue_count () + RETURNS TRIGGER + AS $$ +DECLARE + v_new_job_count int; +BEGIN + UPDATE + app_jobs.job_queues + SET + job_count = job_queues.job_count - 1 + WHERE + queue_name = OLD.queue_name + RETURNING + job_count INTO v_new_job_count; + IF v_new_job_count <= 0 THEN + DELETE FROM app_jobs.job_queues + WHERE queue_name = OLD.queue_name + AND job_count <= 0; + END IF; + RETURN OLD; +END; +$$ +LANGUAGE 'plpgsql' +VOLATILE; + +CREATE TRIGGER decrease_job_queue_count_on_delete + AFTER DELETE ON app_jobs.jobs + FOR EACH ROW + WHEN ((OLD.queue_name IS NOT NULL)) + EXECUTE PROCEDURE app_jobs.tg_decrease_job_queue_count (); + +-- only a person would do this... +CREATE TRIGGER decrease_job_queue_count_on_update + AFTER UPDATE OF queue_name ON app_jobs.jobs + FOR EACH ROW + WHEN (((NEW.queue_name IS DISTINCT FROM OLD.queue_name) AND (OLD.queue_name IS NOT NULL))) + EXECUTE PROCEDURE app_jobs.tg_decrease_job_queue_count (); + +COMMIT; + diff --git a/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count.sql b/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count.sql new file mode 100644 index 000000000..b25b3f1a7 --- /dev/null +++ b/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count.sql @@ -0,0 +1,32 @@ +-- Deploy schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/jobs/table + +BEGIN; +CREATE FUNCTION app_jobs.tg_increase_job_queue_count () + RETURNS TRIGGER + AS $$ +BEGIN + INSERT INTO app_jobs.job_queues (queue_name, job_count) + VALUES (NEW.queue_name, 1) + ON CONFLICT (queue_name) + DO UPDATE SET + job_count = job_queues.job_count + 1; + RETURN NEW; +END; +$$ +LANGUAGE 'plpgsql' +VOLATILE; +CREATE TRIGGER _500_increase_job_queue_count_on_insert + AFTER INSERT ON app_jobs.jobs + FOR EACH ROW + WHEN ((NEW.queue_name IS NOT NULL)) + EXECUTE PROCEDURE app_jobs.tg_increase_job_queue_count (); +-- only a person would do this +CREATE TRIGGER _500_increase_job_queue_count_on_update + AFTER UPDATE OF queue_name ON app_jobs.jobs + FOR EACH ROW + WHEN (((NEW.queue_name IS DISTINCT FROM OLD.queue_name) AND (NEW.queue_name IS NOT NULL))) + EXECUTE PROCEDURE app_jobs.tg_increase_job_queue_count (); +COMMIT; + diff --git a/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql b/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql new file mode 100644 index 000000000..9e6cec54c --- /dev/null +++ b/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql @@ -0,0 +1,13 @@ +-- Deploy schemas/app_jobs/tables/jobs/triggers/notify_worker to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/jobs/table +-- requires: schemas/app_jobs/procedures/do_notify +-- requires: schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count + +BEGIN; +CREATE TRIGGER _900_notify_worker + AFTER INSERT ON app_jobs.jobs + FOR EACH ROW + EXECUTE PROCEDURE app_jobs.do_notify ('jobs:insert'); +COMMIT; + diff --git a/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/jobs/triggers/timestamps.sql b/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/jobs/triggers/timestamps.sql new file mode 100644 index 000000000..b26296953 --- /dev/null +++ b/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/jobs/triggers/timestamps.sql @@ -0,0 +1,20 @@ +-- Deploy schemas/app_jobs/tables/jobs/triggers/timestamps to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/jobs/table +-- requires: schemas/app_jobs/triggers/tg_update_timestamps + +BEGIN; +ALTER TABLE app_jobs.jobs + ADD COLUMN created_at timestamptz; +ALTER TABLE app_jobs.jobs + ALTER COLUMN created_at SET DEFAULT NOW(); +ALTER TABLE app_jobs.jobs + ADD COLUMN updated_at timestamptz; +ALTER TABLE app_jobs.jobs + ALTER COLUMN updated_at SET DEFAULT NOW(); +CREATE TRIGGER _100_update_jobs_modtime_tg + BEFORE UPDATE OR INSERT ON app_jobs.jobs + FOR EACH ROW + EXECUTE PROCEDURE app_jobs.tg_update_timestamps (); +COMMIT; + diff --git a/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/scheduled_jobs/grants/grant_select_insert_update_delete_to_administrator.sql b/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/scheduled_jobs/grants/grant_select_insert_update_delete_to_administrator.sql new file mode 100644 index 000000000..914166bd4 --- /dev/null +++ b/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/scheduled_jobs/grants/grant_select_insert_update_delete_to_administrator.sql @@ -0,0 +1,12 @@ +-- Deploy schemas/app_jobs/tables/scheduled_jobs/grants/grant_select_insert_update_delete_to_administrator to pg + +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/scheduled_jobs/table + +BEGIN; + +-- TODO make sure to require any policies on this table! + +GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE app_jobs.scheduled_jobs TO administrator; + +COMMIT; diff --git a/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_locked_by_idx.sql b/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_locked_by_idx.sql new file mode 100644 index 000000000..d222737ff --- /dev/null +++ b/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_locked_by_idx.sql @@ -0,0 +1,8 @@ +-- Deploy schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_locked_by_idx to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/scheduled_jobs/table + +BEGIN; +CREATE INDEX scheduled_jobs_locked_by_idx ON app_jobs.scheduled_jobs (locked_by); +COMMIT; + diff --git a/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_priority_id_idx.sql b/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_priority_id_idx.sql new file mode 100644 index 000000000..9bd548796 --- /dev/null +++ b/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_priority_id_idx.sql @@ -0,0 +1,8 @@ +-- Deploy schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_priority_id_idx to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/scheduled_jobs/table + +BEGIN; +CREATE INDEX scheduled_jobs_priority_id_idx ON app_jobs.scheduled_jobs (priority, id); +COMMIT; + diff --git a/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/scheduled_jobs/table.sql b/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/scheduled_jobs/table.sql new file mode 100644 index 000000000..75d81c9e3 --- /dev/null +++ b/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/scheduled_jobs/table.sql @@ -0,0 +1,43 @@ +-- Deploy schemas/app_jobs/tables/scheduled_jobs/table to pg +-- requires: schemas/app_jobs/schema + +BEGIN; +CREATE TABLE app_jobs.scheduled_jobs ( + id bigserial PRIMARY KEY, + database_id uuid NOT NULL, + queue_name text DEFAULT (public.gen_random_uuid ()) ::text, + task_identifier text NOT NULL, + payload json DEFAULT '{}' ::json NOT NULL, + priority integer DEFAULT 0 NOT NULL, + max_attempts integer DEFAULT 25 NOT NULL, + key text, + locked_at timestamptz, + locked_by text, + schedule_info json NOT NULL, + last_scheduled timestamptz, + last_scheduled_id bigint, + CHECK (length(key) < 513), + CHECK (length(task_identifier) < 127), + CHECK (max_attempts > 0), + CHECK (length(queue_name) < 127), + CHECK (length(locked_by) > 3), + UNIQUE (key) +); + +COMMENT ON TABLE app_jobs.scheduled_jobs IS 'Recurring/cron-style job definitions with database scoping: each row spawns jobs on a schedule for a specific database'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.id IS 'Auto-incrementing scheduled job identifier'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.database_id IS 'Database this scheduled job belongs to, for multi-tenant isolation'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.queue_name IS 'Name of the queue spawned jobs are placed into'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.task_identifier IS 'Task type identifier for spawned jobs'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.payload IS 'JSON payload passed to each spawned job'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.priority IS 'Priority assigned to spawned jobs (lower = higher priority)'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.max_attempts IS 'Max retry attempts for spawned jobs'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.key IS 'Optional unique deduplication key'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.locked_at IS 'Timestamp when the scheduler locked this record for processing'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.locked_by IS 'Identifier of the scheduler worker holding the lock'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.schedule_info IS 'JSON schedule configuration (e.g. cron expression, interval)'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.last_scheduled IS 'Timestamp when a job was last spawned from this schedule'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.last_scheduled_id IS 'ID of the last job spawned from this schedule'; + +COMMIT; + diff --git a/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/scheduled_jobs/triggers/notify_scheduled_job.sql b/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/scheduled_jobs/triggers/notify_scheduled_job.sql new file mode 100644 index 000000000..51e17d4c0 --- /dev/null +++ b/packages/database-jobs-v1/deploy/schemas/app_jobs/tables/scheduled_jobs/triggers/notify_scheduled_job.sql @@ -0,0 +1,12 @@ +-- Deploy schemas/app_jobs/tables/scheduled_jobs/triggers/notify_scheduled_job to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/scheduled_jobs/table +-- requires: schemas/app_jobs/procedures/do_notify + +BEGIN; +CREATE TRIGGER _900_notify_scheduled_job + AFTER INSERT ON app_jobs.scheduled_jobs + FOR EACH ROW + EXECUTE PROCEDURE app_jobs.do_notify ('scheduled_jobs:insert'); +COMMIT; + diff --git a/packages/database-jobs-v1/deploy/schemas/app_jobs/triggers/tg_add_job_with_fields.sql b/packages/database-jobs-v1/deploy/schemas/app_jobs/triggers/tg_add_job_with_fields.sql new file mode 100644 index 000000000..a83a8b833 --- /dev/null +++ b/packages/database-jobs-v1/deploy/schemas/app_jobs/triggers/tg_add_job_with_fields.sql @@ -0,0 +1,50 @@ +-- Deploy schemas/app_jobs/triggers/tg_add_job_with_fields to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/helpers/json_build_object_apply + +BEGIN; +CREATE FUNCTION app_jobs.trigger_job_with_fields () + RETURNS TRIGGER + AS $$ +DECLARE + arg text; + fn text; + i int; + args text[]; +BEGIN + FOR i IN + SELECT + * + FROM + generate_series(1, TG_NARGS) g (i) + LOOP + IF (i = 1) THEN + fn = TG_ARGV[i - 1]; + ELSE + args = array_append(args, TG_ARGV[i - 1]); + IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN + EXECUTE format('SELECT ($1).%s::text', TG_ARGV[i - 1]) + USING NEW INTO arg; + END IF; + IF (TG_OP = 'DELETE') THEN + EXECUTE format('SELECT ($1).%s::text', TG_ARGV[i - 1]) + USING OLD INTO arg; + END IF; + args = array_append(args, arg); + END IF; + END LOOP; + PERFORM + app_jobs.add_job (jwt_private.current_database_id(), fn, app_jobs.json_build_object_apply (args)); + IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN + RETURN NEW; + END IF; + IF (TG_OP = 'DELETE') THEN + RETURN OLD; + END IF; +END; +$$ +LANGUAGE plpgsql +VOLATILE +SECURITY DEFINER; +COMMIT; + diff --git a/packages/database-jobs-v1/deploy/schemas/app_jobs/triggers/tg_add_job_with_row.sql b/packages/database-jobs-v1/deploy/schemas/app_jobs/triggers/tg_add_job_with_row.sql new file mode 100644 index 000000000..bb619156b --- /dev/null +++ b/packages/database-jobs-v1/deploy/schemas/app_jobs/triggers/tg_add_job_with_row.sql @@ -0,0 +1,26 @@ +-- Deploy schemas/app_jobs/triggers/tg_add_job_with_row to pg +-- requires: schemas/app_jobs/schema + +BEGIN; +CREATE FUNCTION app_jobs.tg_add_job_with_row () + RETURNS TRIGGER + AS $$ +BEGIN + IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN + PERFORM + app_jobs.add_job (jwt_private.current_database_id(), TG_ARGV[0], to_json(NEW)); + RETURN NEW; + END IF; + IF (TG_OP = 'DELETE') THEN + PERFORM + app_jobs.add_job (jwt_private.current_database_id(), TG_ARGV[0], to_json(OLD)); + RETURN OLD; + END IF; +END; +$$ +LANGUAGE plpgsql +VOLATILE +SECURITY DEFINER; +COMMENT ON FUNCTION app_jobs.tg_add_job_with_row IS E'Useful shortcut to create a job on insert or update. Pass the task name as the trigger argument, and the record data will automatically be available on the JSON payload.'; +COMMIT; + diff --git a/packages/database-jobs-v1/deploy/schemas/app_jobs/triggers/tg_add_job_with_row_id.sql b/packages/database-jobs-v1/deploy/schemas/app_jobs/triggers/tg_add_job_with_row_id.sql new file mode 100644 index 000000000..faf7c78d3 --- /dev/null +++ b/packages/database-jobs-v1/deploy/schemas/app_jobs/triggers/tg_add_job_with_row_id.sql @@ -0,0 +1,27 @@ +-- Deploy schemas/app_jobs/triggers/tg_add_job_with_row_id to pg + +-- requires: schemas/app_jobs/schema + +BEGIN; +CREATE FUNCTION app_jobs.tg_add_job_with_row_id () + RETURNS TRIGGER + AS $$ +BEGIN + IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN + PERFORM + app_jobs.add_job (jwt_private.current_database_id(), tg_argv[0], json_build_object('id', NEW.id)); + RETURN NEW; + END IF; + IF (TG_OP = 'DELETE') THEN + PERFORM + app_jobs.add_job (jwt_private.current_database_id(), tg_argv[0], json_build_object('id', OLD.id)); + RETURN OLD; + END IF; +END; +$$ +LANGUAGE plpgsql +VOLATILE +SECURITY DEFINER; +COMMENT ON FUNCTION app_jobs.tg_add_job_with_row_id IS E'Useful shortcut to create a job on insert or update. Pass the task name as the trigger argument, and the record id will automatically be available on the JSON payload.'; +COMMIT; + diff --git a/packages/database-jobs-v1/deploy/schemas/app_jobs/triggers/tg_update_timestamps.sql b/packages/database-jobs-v1/deploy/schemas/app_jobs/triggers/tg_update_timestamps.sql new file mode 100644 index 000000000..e74c4abfc --- /dev/null +++ b/packages/database-jobs-v1/deploy/schemas/app_jobs/triggers/tg_update_timestamps.sql @@ -0,0 +1,21 @@ +-- Deploy schemas/app_jobs/triggers/tg_update_timestamps to pg +-- requires: schemas/app_jobs/schema + +BEGIN; +CREATE FUNCTION app_jobs.tg_update_timestamps () + RETURNS TRIGGER + AS $$ +BEGIN + IF TG_OP = 'INSERT' THEN + NEW.created_at = NOW(); + NEW.updated_at = NOW(); + ELSIF TG_OP = 'UPDATE' THEN + NEW.created_at = OLD.created_at; + NEW.updated_at = greatest (now(), OLD.updated_at + interval '1 millisecond'); + END IF; + RETURN NEW; +END; +$$ +LANGUAGE 'plpgsql'; +COMMIT; + diff --git a/packages/database-jobs-v1/jest.config.js b/packages/database-jobs-v1/jest.config.js new file mode 100644 index 000000000..e20e7efb5 --- /dev/null +++ b/packages/database-jobs-v1/jest.config.js @@ -0,0 +1,15 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + + // Match both __tests__ and colocated test files + testMatch: ['**/?(*.)+(test|spec).{ts,tsx,js,jsx}'], + + // Ignore build artifacts and type declarations + testPathIgnorePatterns: ['/dist/', '\\.d\\.ts$'], + modulePathIgnorePatterns: ['/dist/'], + watchPathIgnorePatterns: ['/dist/'], + + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], +}; diff --git a/packages/database-jobs-v1/package.json b/packages/database-jobs-v1/package.json new file mode 100644 index 000000000..eb7d458f0 --- /dev/null +++ b/packages/database-jobs-v1/package.json @@ -0,0 +1,37 @@ +{ + "name": "@pgpm/database-jobs-v1", + "version": "0.21.0", + "description": "Database-specific job handling and queue management", + "author": "Dan Lynch ", + "contributors": [ + "Constructive " + ], + "keywords": [ + "postgresql", + "pgpm", + "jobs", + "queue" + ], + "publishConfig": { + "access": "public" + }, + "scripts": { + "bundle": "pgpm package", + "test": "jest", + "test:watch": "jest --watch" + }, + "devDependencies": { + "pgpm": "^4.16.6" + }, + "dependencies": { + "@pgpm/verify": "workspace:*" + }, + "repository": { + "type": "git", + "url": "https://github.com/constructive-io/pgpm-modules" + }, + "homepage": "https://github.com/constructive-io/pgpm-modules", + "bugs": { + "url": "https://github.com/constructive-io/pgpm-modules/issues" + } +} diff --git a/packages/database-jobs-v1/pgpm-database-jobs-v1.control b/packages/database-jobs-v1/pgpm-database-jobs-v1.control new file mode 100644 index 000000000..3754fe24d --- /dev/null +++ b/packages/database-jobs-v1/pgpm-database-jobs-v1.control @@ -0,0 +1,8 @@ +# pgpm-database-jobs-v1 extension +comment = 'pgpm-database-jobs-v1 extension' +default_version = '0.15.5' +module_pathname = '$libdir/pgpm-database-jobs-v1' +requires = 'plpgsql,pgcrypto,pgpm-verify' +relocatable = false +superuser = false + diff --git a/packages/database-jobs-v1/pgpm.plan b/packages/database-jobs-v1/pgpm.plan new file mode 100644 index 000000000..76ce5034c --- /dev/null +++ b/packages/database-jobs-v1/pgpm.plan @@ -0,0 +1,38 @@ +%syntax-version=1.0.0 +%project=pgpm-database-jobs-v1 +%uri=pgpm-database-jobs-v1 +schemas/app_jobs/schema [pgpm-verify:@0.1.0] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/schema +schemas/app_jobs/triggers/tg_update_timestamps [schemas/app_jobs/schema] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/triggers/tg_update_timestamps +schemas/app_jobs/triggers/tg_add_job_with_row_id [schemas/app_jobs/schema] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/triggers/tg_add_job_with_row_id +schemas/app_jobs/triggers/tg_add_job_with_row [schemas/app_jobs/schema] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/triggers/tg_add_job_with_row +schemas/app_jobs/helpers/json_build_object_apply [schemas/app_jobs/schema] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/helpers/json_build_object_apply +schemas/app_jobs/triggers/tg_add_job_with_fields [schemas/app_jobs/schema schemas/app_jobs/helpers/json_build_object_apply] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/triggers/tg_add_job_with_fields +schemas/app_jobs/tables/scheduled_jobs/table [schemas/app_jobs/schema] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/tables/scheduled_jobs/table +schemas/app_jobs/procedures/do_notify [schemas/app_jobs/schema] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/procedures/do_notify +schemas/app_jobs/tables/scheduled_jobs/triggers/notify_scheduled_job [schemas/app_jobs/schema schemas/app_jobs/tables/scheduled_jobs/table schemas/app_jobs/procedures/do_notify] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/tables/scheduled_jobs/triggers/notify_scheduled_job +schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_priority_id_idx [schemas/app_jobs/schema schemas/app_jobs/tables/scheduled_jobs/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_priority_id_idx +schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_locked_by_idx [schemas/app_jobs/schema schemas/app_jobs/tables/scheduled_jobs/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_locked_by_idx +schemas/app_jobs/tables/scheduled_jobs/grants/grant_select_insert_update_delete_to_administrator [schemas/app_jobs/schema schemas/app_jobs/tables/scheduled_jobs/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/tables/scheduled_jobs/grants/grant_select_insert_update_delete_to_administrator +schemas/app_jobs/tables/jobs/table [schemas/app_jobs/schema] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/tables/jobs/table +schemas/app_jobs/tables/jobs/triggers/timestamps [schemas/app_jobs/schema schemas/app_jobs/tables/jobs/table schemas/app_jobs/triggers/tg_update_timestamps] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/tables/jobs/triggers/timestamps +schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count [schemas/app_jobs/schema schemas/app_jobs/tables/jobs/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count +schemas/app_jobs/tables/jobs/triggers/notify_worker [schemas/app_jobs/schema schemas/app_jobs/tables/jobs/table schemas/app_jobs/procedures/do_notify schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/tables/jobs/triggers/notify_worker +schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count [schemas/app_jobs/schema schemas/app_jobs/tables/jobs/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count +schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx [schemas/app_jobs/schema schemas/app_jobs/tables/jobs/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx +schemas/app_jobs/tables/jobs/indexes/jobs_locked_by_idx [schemas/app_jobs/schema schemas/app_jobs/tables/jobs/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/tables/jobs/indexes/jobs_locked_by_idx +schemas/app_jobs/tables/jobs/grants/grant_select_insert_update_delete_to_administrator [schemas/app_jobs/schema schemas/app_jobs/tables/jobs/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/tables/jobs/grants/grant_select_insert_update_delete_to_administrator +schemas/app_jobs/tables/job_queues/table [schemas/app_jobs/schema] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/tables/job_queues/table +schemas/app_jobs/tables/job_queues/indexes/job_queues_locked_by_idx [schemas/app_jobs/schema schemas/app_jobs/tables/job_queues/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/tables/job_queues/indexes/job_queues_locked_by_idx +schemas/app_jobs/tables/job_queues/grants/grant_select_insert_update_delete_to_administrator [schemas/app_jobs/schema schemas/app_jobs/tables/job_queues/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/tables/job_queues/grants/grant_select_insert_update_delete_to_administrator +schemas/app_jobs/procedures/run_scheduled_job [schemas/app_jobs/schema schemas/app_jobs/tables/jobs/table schemas/app_jobs/tables/scheduled_jobs/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/procedures/run_scheduled_job +schemas/app_jobs/procedures/reschedule_jobs [schemas/app_jobs/schema schemas/app_jobs/tables/jobs/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/procedures/reschedule_jobs +schemas/app_jobs/procedures/release_scheduled_jobs [schemas/app_jobs/schema schemas/app_jobs/tables/scheduled_jobs/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/procedures/release_scheduled_jobs +schemas/app_jobs/procedures/release_jobs [schemas/app_jobs/schema schemas/app_jobs/tables/jobs/table schemas/app_jobs/tables/job_queues/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/procedures/release_jobs +schemas/app_jobs/procedures/permanently_fail_jobs [schemas/app_jobs/schema schemas/app_jobs/tables/job_queues/table schemas/app_jobs/tables/jobs/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/procedures/permanently_fail_jobs +schemas/app_jobs/procedures/get_scheduled_job [schemas/app_jobs/schema schemas/app_jobs/tables/scheduled_jobs/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/procedures/get_scheduled_job +schemas/app_jobs/procedures/get_job [schemas/app_jobs/schema schemas/app_jobs/tables/job_queues/table schemas/app_jobs/tables/jobs/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/procedures/get_job +schemas/app_jobs/procedures/fail_job [schemas/app_jobs/schema schemas/app_jobs/tables/jobs/table schemas/app_jobs/tables/job_queues/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/procedures/fail_job +schemas/app_jobs/procedures/complete_jobs [schemas/app_jobs/schema schemas/app_jobs/tables/job_queues/table schemas/app_jobs/tables/jobs/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/procedures/complete_jobs +schemas/app_jobs/procedures/complete_job [schemas/app_jobs/schema schemas/app_jobs/tables/jobs/table schemas/app_jobs/tables/job_queues/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/procedures/complete_job +schemas/app_jobs/procedures/add_scheduled_job [schemas/app_jobs/schema schemas/app_jobs/tables/scheduled_jobs/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/procedures/add_scheduled_job +schemas/app_jobs/procedures/add_job [schemas/app_jobs/schema schemas/app_jobs/tables/jobs/table schemas/app_jobs/tables/job_queues/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/procedures/add_job diff --git a/packages/database-jobs-v1/revert/schemas/app_jobs/helpers/json_build_object_apply.sql b/packages/database-jobs-v1/revert/schemas/app_jobs/helpers/json_build_object_apply.sql new file mode 100644 index 000000000..b1778898c --- /dev/null +++ b/packages/database-jobs-v1/revert/schemas/app_jobs/helpers/json_build_object_apply.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/helpers/json_build_object_apply from pg + +BEGIN; + +DROP FUNCTION app_jobs.json_build_object_apply; + +COMMIT; diff --git a/packages/database-jobs-v1/revert/schemas/app_jobs/procedures/add_job.sql b/packages/database-jobs-v1/revert/schemas/app_jobs/procedures/add_job.sql new file mode 100644 index 000000000..44a65ae8d --- /dev/null +++ b/packages/database-jobs-v1/revert/schemas/app_jobs/procedures/add_job.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/procedures/add_job from pg + +BEGIN; + +DROP FUNCTION app_jobs.add_job; + +COMMIT; diff --git a/packages/database-jobs-v1/revert/schemas/app_jobs/procedures/add_scheduled_job.sql b/packages/database-jobs-v1/revert/schemas/app_jobs/procedures/add_scheduled_job.sql new file mode 100644 index 000000000..882a98f9d --- /dev/null +++ b/packages/database-jobs-v1/revert/schemas/app_jobs/procedures/add_scheduled_job.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/procedures/add_scheduled_job from pg + +BEGIN; + +DROP FUNCTION app_jobs.add_scheduled_job; + +COMMIT; diff --git a/packages/database-jobs-v1/revert/schemas/app_jobs/procedures/complete_job.sql b/packages/database-jobs-v1/revert/schemas/app_jobs/procedures/complete_job.sql new file mode 100644 index 000000000..7c0ea9dfa --- /dev/null +++ b/packages/database-jobs-v1/revert/schemas/app_jobs/procedures/complete_job.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/procedures/complete_job from pg + +BEGIN; + +DROP FUNCTION app_jobs.complete_job; + +COMMIT; diff --git a/packages/database-jobs-v1/revert/schemas/app_jobs/procedures/complete_jobs.sql b/packages/database-jobs-v1/revert/schemas/app_jobs/procedures/complete_jobs.sql new file mode 100644 index 000000000..3db9150e3 --- /dev/null +++ b/packages/database-jobs-v1/revert/schemas/app_jobs/procedures/complete_jobs.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/procedures/complete_jobs from pg + +BEGIN; + +DROP FUNCTION app_jobs.complete_jobs; + +COMMIT; diff --git a/packages/database-jobs-v1/revert/schemas/app_jobs/procedures/do_notify.sql b/packages/database-jobs-v1/revert/schemas/app_jobs/procedures/do_notify.sql new file mode 100644 index 000000000..58a8138a1 --- /dev/null +++ b/packages/database-jobs-v1/revert/schemas/app_jobs/procedures/do_notify.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/procedures/do_notify from pg + +BEGIN; + +DROP FUNCTION app_jobs.do_notify; + +COMMIT; diff --git a/packages/database-jobs-v1/revert/schemas/app_jobs/procedures/fail_job.sql b/packages/database-jobs-v1/revert/schemas/app_jobs/procedures/fail_job.sql new file mode 100644 index 000000000..ed96e401e --- /dev/null +++ b/packages/database-jobs-v1/revert/schemas/app_jobs/procedures/fail_job.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/procedures/fail_job from pg + +BEGIN; + +DROP FUNCTION app_jobs.fail_job; + +COMMIT; diff --git a/packages/database-jobs-v1/revert/schemas/app_jobs/procedures/get_job.sql b/packages/database-jobs-v1/revert/schemas/app_jobs/procedures/get_job.sql new file mode 100644 index 000000000..469f6b4da --- /dev/null +++ b/packages/database-jobs-v1/revert/schemas/app_jobs/procedures/get_job.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/procedures/get_job from pg + +BEGIN; + +DROP FUNCTION app_jobs.get_job; + +COMMIT; diff --git a/packages/database-jobs-v1/revert/schemas/app_jobs/procedures/get_scheduled_job.sql b/packages/database-jobs-v1/revert/schemas/app_jobs/procedures/get_scheduled_job.sql new file mode 100644 index 000000000..f41f8fdb4 --- /dev/null +++ b/packages/database-jobs-v1/revert/schemas/app_jobs/procedures/get_scheduled_job.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/procedures/get_scheduled_job from pg + +BEGIN; + +DROP FUNCTION app_jobs.get_scheduled_job; + +COMMIT; diff --git a/packages/database-jobs-v1/revert/schemas/app_jobs/procedures/permanently_fail_jobs.sql b/packages/database-jobs-v1/revert/schemas/app_jobs/procedures/permanently_fail_jobs.sql new file mode 100644 index 000000000..f0299ea82 --- /dev/null +++ b/packages/database-jobs-v1/revert/schemas/app_jobs/procedures/permanently_fail_jobs.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/procedures/permanently_fail_jobs from pg + +BEGIN; + +DROP FUNCTION app_jobs.permanently_fail_jobs; + +COMMIT; diff --git a/packages/database-jobs-v1/revert/schemas/app_jobs/procedures/release_jobs.sql b/packages/database-jobs-v1/revert/schemas/app_jobs/procedures/release_jobs.sql new file mode 100644 index 000000000..8ece69ef1 --- /dev/null +++ b/packages/database-jobs-v1/revert/schemas/app_jobs/procedures/release_jobs.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/procedures/release_jobs from pg + +BEGIN; + +DROP FUNCTION app_jobs.release_jobs; + +COMMIT; diff --git a/packages/database-jobs-v1/revert/schemas/app_jobs/procedures/release_scheduled_jobs.sql b/packages/database-jobs-v1/revert/schemas/app_jobs/procedures/release_scheduled_jobs.sql new file mode 100644 index 000000000..a16e6e9a4 --- /dev/null +++ b/packages/database-jobs-v1/revert/schemas/app_jobs/procedures/release_scheduled_jobs.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/procedures/release_scheduled_jobs from pg + +BEGIN; + +DROP FUNCTION app_jobs.release_scheduled_jobs; + +COMMIT; diff --git a/packages/database-jobs-v1/revert/schemas/app_jobs/procedures/reschedule_jobs.sql b/packages/database-jobs-v1/revert/schemas/app_jobs/procedures/reschedule_jobs.sql new file mode 100644 index 000000000..34a441716 --- /dev/null +++ b/packages/database-jobs-v1/revert/schemas/app_jobs/procedures/reschedule_jobs.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/procedures/reschedule_jobs from pg + +BEGIN; + +DROP FUNCTION app_jobs.reschedule_jobs; + +COMMIT; diff --git a/packages/database-jobs-v1/revert/schemas/app_jobs/procedures/run_scheduled_job.sql b/packages/database-jobs-v1/revert/schemas/app_jobs/procedures/run_scheduled_job.sql new file mode 100644 index 000000000..77886fc04 --- /dev/null +++ b/packages/database-jobs-v1/revert/schemas/app_jobs/procedures/run_scheduled_job.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/procedures/run_scheduled_job from pg + +BEGIN; + +DROP FUNCTION app_jobs.run_scheduled_job; + +COMMIT; diff --git a/packages/database-jobs-v1/revert/schemas/app_jobs/schema.sql b/packages/database-jobs-v1/revert/schemas/app_jobs/schema.sql new file mode 100644 index 000000000..2b238d0fe --- /dev/null +++ b/packages/database-jobs-v1/revert/schemas/app_jobs/schema.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/schema from pg + +BEGIN; + +DROP SCHEMA app_jobs; + +COMMIT; diff --git a/packages/database-jobs-v1/revert/schemas/app_jobs/tables/job_queues/grants/grant_select_insert_update_delete_to_administrator.sql b/packages/database-jobs-v1/revert/schemas/app_jobs/tables/job_queues/grants/grant_select_insert_update_delete_to_administrator.sql new file mode 100644 index 000000000..06a833789 --- /dev/null +++ b/packages/database-jobs-v1/revert/schemas/app_jobs/tables/job_queues/grants/grant_select_insert_update_delete_to_administrator.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/tables/job_queues/grants/grant_select_insert_update_delete_to_administrator from pg + +BEGIN; + +REVOKE SELECT, INSERT, UPDATE, DELETE ON TABLE app_jobs.job_queues FROM administrator; + +COMMIT; diff --git a/packages/database-jobs-v1/revert/schemas/app_jobs/tables/job_queues/indexes/job_queues_locked_by_idx.sql b/packages/database-jobs-v1/revert/schemas/app_jobs/tables/job_queues/indexes/job_queues_locked_by_idx.sql new file mode 100644 index 000000000..20290a2a1 --- /dev/null +++ b/packages/database-jobs-v1/revert/schemas/app_jobs/tables/job_queues/indexes/job_queues_locked_by_idx.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/tables/job_queues/indexes/job_queues_locked_by_idx from pg + +BEGIN; + +DROP INDEX app_jobs.job_queues_locked_by_idx; + +COMMIT; diff --git a/packages/database-jobs-v1/revert/schemas/app_jobs/tables/job_queues/table.sql b/packages/database-jobs-v1/revert/schemas/app_jobs/tables/job_queues/table.sql new file mode 100644 index 000000000..79c62cbcc --- /dev/null +++ b/packages/database-jobs-v1/revert/schemas/app_jobs/tables/job_queues/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/tables/job_queues/table from pg + +BEGIN; + +DROP TABLE app_jobs.job_queues; + +COMMIT; diff --git a/packages/database-jobs-v1/revert/schemas/app_jobs/tables/jobs/grants/grant_select_insert_update_delete_to_administrator.sql b/packages/database-jobs-v1/revert/schemas/app_jobs/tables/jobs/grants/grant_select_insert_update_delete_to_administrator.sql new file mode 100644 index 000000000..c67b07e27 --- /dev/null +++ b/packages/database-jobs-v1/revert/schemas/app_jobs/tables/jobs/grants/grant_select_insert_update_delete_to_administrator.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/tables/jobs/grants/grant_select_insert_update_delete_to_administrator from pg + +BEGIN; + +REVOKE SELECT, INSERT, UPDATE, DELETE ON TABLE app_jobs.jobs FROM administrator; + +COMMIT; diff --git a/packages/database-jobs-v1/revert/schemas/app_jobs/tables/jobs/indexes/jobs_locked_by_idx.sql b/packages/database-jobs-v1/revert/schemas/app_jobs/tables/jobs/indexes/jobs_locked_by_idx.sql new file mode 100644 index 000000000..f26cb13e2 --- /dev/null +++ b/packages/database-jobs-v1/revert/schemas/app_jobs/tables/jobs/indexes/jobs_locked_by_idx.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/tables/jobs/indexes/jobs_locked_by_idx from pg + +BEGIN; + +DROP INDEX app_jobs.jobs_locked_by_idx; + +COMMIT; diff --git a/packages/database-jobs-v1/revert/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql b/packages/database-jobs-v1/revert/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql new file mode 100644 index 000000000..24fa09ac6 --- /dev/null +++ b/packages/database-jobs-v1/revert/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx from pg + +BEGIN; + +DROP INDEX app_jobs.priority_run_at_id_idx; + +COMMIT; diff --git a/packages/database-jobs-v1/revert/schemas/app_jobs/tables/jobs/table.sql b/packages/database-jobs-v1/revert/schemas/app_jobs/tables/jobs/table.sql new file mode 100644 index 000000000..b4156ad3e --- /dev/null +++ b/packages/database-jobs-v1/revert/schemas/app_jobs/tables/jobs/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/tables/jobs/table from pg + +BEGIN; + +DROP TABLE app_jobs.jobs; + +COMMIT; diff --git a/packages/database-jobs-v1/revert/schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count.sql b/packages/database-jobs-v1/revert/schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count.sql new file mode 100644 index 000000000..bf4f88c6f --- /dev/null +++ b/packages/database-jobs-v1/revert/schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count from pg +BEGIN; +DROP TRIGGER decrease_job_queue_count_on_delete ON app_jobs.jobs; +DROP TRIGGER decrease_job_queue_count_on_update ON app_jobs.jobs; +DROP FUNCTION app_jobs.tg_decrease_job_queue_count; +COMMIT; + diff --git a/packages/database-jobs-v1/revert/schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count.sql b/packages/database-jobs-v1/revert/schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count.sql new file mode 100644 index 000000000..5098a6517 --- /dev/null +++ b/packages/database-jobs-v1/revert/schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count from pg +BEGIN; +DROP TRIGGER _500_increase_job_queue_count_on_insert ON app_jobs.jobs; +DROP TRIGGER _500_increase_job_queue_count_on_update ON app_jobs.jobs; +DROP FUNCTION app_jobs.tg_increase_job_queue_count; +COMMIT; + diff --git a/packages/database-jobs-v1/revert/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql b/packages/database-jobs-v1/revert/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql new file mode 100644 index 000000000..612a02874 --- /dev/null +++ b/packages/database-jobs-v1/revert/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql @@ -0,0 +1,5 @@ +-- Revert schemas/app_jobs/tables/jobs/triggers/notify_worker from pg +BEGIN; +DROP TRIGGER _900_notify_worker ON app_jobs.jobs; +COMMIT; + diff --git a/packages/database-jobs-v1/revert/schemas/app_jobs/tables/jobs/triggers/timestamps.sql b/packages/database-jobs-v1/revert/schemas/app_jobs/tables/jobs/triggers/timestamps.sql new file mode 100644 index 000000000..7dc2f048c --- /dev/null +++ b/packages/database-jobs-v1/revert/schemas/app_jobs/tables/jobs/triggers/timestamps.sql @@ -0,0 +1,9 @@ +-- Revert schemas/app_jobs/tables/jobs/triggers/timestamps from pg +BEGIN; +ALTER TABLE app_jobs.jobs + DROP COLUMN created_at; +ALTER TABLE app_jobs.jobs + DROP COLUMN updated_at; +DROP TRIGGER _100_update_jobs_modtime_tg ON app_jobs.jobs; +COMMIT; + diff --git a/packages/database-jobs-v1/revert/schemas/app_jobs/tables/scheduled_jobs/grants/grant_select_insert_update_delete_to_administrator.sql b/packages/database-jobs-v1/revert/schemas/app_jobs/tables/scheduled_jobs/grants/grant_select_insert_update_delete_to_administrator.sql new file mode 100644 index 000000000..0990e98d6 --- /dev/null +++ b/packages/database-jobs-v1/revert/schemas/app_jobs/tables/scheduled_jobs/grants/grant_select_insert_update_delete_to_administrator.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/tables/scheduled_jobs/grants/grant_select_insert_update_delete_to_administrator from pg + +BEGIN; + +REVOKE SELECT, INSERT, UPDATE, DELETE ON TABLE app_jobs.scheduled_jobs FROM administrator; + +COMMIT; diff --git a/packages/database-jobs-v1/revert/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_locked_by_idx.sql b/packages/database-jobs-v1/revert/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_locked_by_idx.sql new file mode 100644 index 000000000..5ff1e6d50 --- /dev/null +++ b/packages/database-jobs-v1/revert/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_locked_by_idx.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_locked_by_idx from pg + +BEGIN; + +DROP INDEX app_jobs.scheduled_jobs_locked_by_idx; + +COMMIT; diff --git a/packages/database-jobs-v1/revert/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_priority_id_idx.sql b/packages/database-jobs-v1/revert/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_priority_id_idx.sql new file mode 100644 index 000000000..be4b58783 --- /dev/null +++ b/packages/database-jobs-v1/revert/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_priority_id_idx.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_priority_id_idx from pg + +BEGIN; + +DROP INDEX app_jobs.scheduled_jobs_priority_id_idx; + +COMMIT; diff --git a/packages/database-jobs-v1/revert/schemas/app_jobs/tables/scheduled_jobs/table.sql b/packages/database-jobs-v1/revert/schemas/app_jobs/tables/scheduled_jobs/table.sql new file mode 100644 index 000000000..3a06f0da8 --- /dev/null +++ b/packages/database-jobs-v1/revert/schemas/app_jobs/tables/scheduled_jobs/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/tables/scheduled_jobs/table from pg + +BEGIN; + +DROP TABLE app_jobs.scheduled_jobs; + +COMMIT; diff --git a/packages/database-jobs-v1/revert/schemas/app_jobs/tables/scheduled_jobs/triggers/notify_scheduled_job.sql b/packages/database-jobs-v1/revert/schemas/app_jobs/tables/scheduled_jobs/triggers/notify_scheduled_job.sql new file mode 100644 index 000000000..5c1852c7a --- /dev/null +++ b/packages/database-jobs-v1/revert/schemas/app_jobs/tables/scheduled_jobs/triggers/notify_scheduled_job.sql @@ -0,0 +1,8 @@ +-- Revert schemas/app_jobs/tables/scheduled_jobs/triggers/notify_scheduled_job from pg + +BEGIN; + +DROP TRIGGER _900_notify_scheduled_job ON app_jobs.scheduled_jobs; + + +COMMIT; diff --git a/packages/database-jobs-v1/revert/schemas/app_jobs/triggers/tg_add_job_with_fields.sql b/packages/database-jobs-v1/revert/schemas/app_jobs/triggers/tg_add_job_with_fields.sql new file mode 100644 index 000000000..5384edfc3 --- /dev/null +++ b/packages/database-jobs-v1/revert/schemas/app_jobs/triggers/tg_add_job_with_fields.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/triggers/tg_add_job_with_fields from pg + +BEGIN; + +DROP FUNCTION app_jobs.trigger_job_with_fields; + +COMMIT; diff --git a/packages/database-jobs-v1/revert/schemas/app_jobs/triggers/tg_add_job_with_row.sql b/packages/database-jobs-v1/revert/schemas/app_jobs/triggers/tg_add_job_with_row.sql new file mode 100644 index 000000000..9d6b68a2c --- /dev/null +++ b/packages/database-jobs-v1/revert/schemas/app_jobs/triggers/tg_add_job_with_row.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/triggers/tg_add_job_with_row from pg + +BEGIN; + +DROP FUNCTION app_jobs.tg_add_job_with_row; + +COMMIT; diff --git a/packages/database-jobs-v1/revert/schemas/app_jobs/triggers/tg_add_job_with_row_id.sql b/packages/database-jobs-v1/revert/schemas/app_jobs/triggers/tg_add_job_with_row_id.sql new file mode 100644 index 000000000..1f0fb04be --- /dev/null +++ b/packages/database-jobs-v1/revert/schemas/app_jobs/triggers/tg_add_job_with_row_id.sql @@ -0,0 +1,5 @@ +-- Revert schemas/app_jobs/triggers/tg_add_job_with_row_id from pg +BEGIN; +DROP FUNCTION app_jobs.tg_add_job_with_row_id; +COMMIT; + diff --git a/packages/database-jobs-v1/revert/schemas/app_jobs/triggers/tg_update_timestamps.sql b/packages/database-jobs-v1/revert/schemas/app_jobs/triggers/tg_update_timestamps.sql new file mode 100644 index 000000000..37378b148 --- /dev/null +++ b/packages/database-jobs-v1/revert/schemas/app_jobs/triggers/tg_update_timestamps.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/triggers/tg_update_timestamps from pg + +BEGIN; + +DROP FUNCTION app_jobs.tg_update_timestamps; + +COMMIT; diff --git a/packages/database-jobs/sql/pgpm-database-jobs--0.15.3.sql b/packages/database-jobs-v1/sql/pgpm-database-jobs--0.15.3.sql similarity index 100% rename from packages/database-jobs/sql/pgpm-database-jobs--0.15.3.sql rename to packages/database-jobs-v1/sql/pgpm-database-jobs--0.15.3.sql diff --git a/packages/database-jobs-v1/verify/schemas/app_jobs/helpers/json_build_object_apply.sql b/packages/database-jobs-v1/verify/schemas/app_jobs/helpers/json_build_object_apply.sql new file mode 100644 index 000000000..e05072cfb --- /dev/null +++ b/packages/database-jobs-v1/verify/schemas/app_jobs/helpers/json_build_object_apply.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/helpers/json_build_object_apply on pg + +BEGIN; + +SELECT verify_function ('app_jobs.json_build_object_apply'); + +ROLLBACK; diff --git a/packages/database-jobs-v1/verify/schemas/app_jobs/procedures/add_job.sql b/packages/database-jobs-v1/verify/schemas/app_jobs/procedures/add_job.sql new file mode 100644 index 000000000..c841e7d04 --- /dev/null +++ b/packages/database-jobs-v1/verify/schemas/app_jobs/procedures/add_job.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/procedures/add_job on pg + +BEGIN; + +SELECT verify_function ('app_jobs.add_job'); + +ROLLBACK; diff --git a/packages/database-jobs-v1/verify/schemas/app_jobs/procedures/add_scheduled_job.sql b/packages/database-jobs-v1/verify/schemas/app_jobs/procedures/add_scheduled_job.sql new file mode 100644 index 000000000..a2f7d4815 --- /dev/null +++ b/packages/database-jobs-v1/verify/schemas/app_jobs/procedures/add_scheduled_job.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/procedures/add_scheduled_job on pg + +BEGIN; + +SELECT verify_function ('app_jobs.add_scheduled_job'); + +ROLLBACK; diff --git a/packages/database-jobs-v1/verify/schemas/app_jobs/procedures/complete_job.sql b/packages/database-jobs-v1/verify/schemas/app_jobs/procedures/complete_job.sql new file mode 100644 index 000000000..4bd179aee --- /dev/null +++ b/packages/database-jobs-v1/verify/schemas/app_jobs/procedures/complete_job.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/procedures/complete_job on pg + +BEGIN; + +SELECT verify_function ('app_jobs.complete_job'); + +ROLLBACK; diff --git a/packages/database-jobs-v1/verify/schemas/app_jobs/procedures/complete_jobs.sql b/packages/database-jobs-v1/verify/schemas/app_jobs/procedures/complete_jobs.sql new file mode 100644 index 000000000..aa9a5a457 --- /dev/null +++ b/packages/database-jobs-v1/verify/schemas/app_jobs/procedures/complete_jobs.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/procedures/complete_jobs on pg + +BEGIN; + +SELECT verify_function ('app_jobs.complete_jobs'); + +ROLLBACK; diff --git a/packages/database-jobs-v1/verify/schemas/app_jobs/procedures/do_notify.sql b/packages/database-jobs-v1/verify/schemas/app_jobs/procedures/do_notify.sql new file mode 100644 index 000000000..df64a9f48 --- /dev/null +++ b/packages/database-jobs-v1/verify/schemas/app_jobs/procedures/do_notify.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/procedures/do_notify on pg + +BEGIN; + +SELECT verify_function ('app_jobs.do_notify'); + +ROLLBACK; diff --git a/packages/database-jobs-v1/verify/schemas/app_jobs/procedures/fail_job.sql b/packages/database-jobs-v1/verify/schemas/app_jobs/procedures/fail_job.sql new file mode 100644 index 000000000..b9c65b489 --- /dev/null +++ b/packages/database-jobs-v1/verify/schemas/app_jobs/procedures/fail_job.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/procedures/fail_job on pg + +BEGIN; + +SELECT verify_function ('app_jobs.fail_job'); + +ROLLBACK; diff --git a/packages/database-jobs-v1/verify/schemas/app_jobs/procedures/get_job.sql b/packages/database-jobs-v1/verify/schemas/app_jobs/procedures/get_job.sql new file mode 100644 index 000000000..86170be11 --- /dev/null +++ b/packages/database-jobs-v1/verify/schemas/app_jobs/procedures/get_job.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/procedures/get_job on pg + +BEGIN; + +SELECT verify_function ('app_jobs.get_job'); + +ROLLBACK; diff --git a/packages/database-jobs-v1/verify/schemas/app_jobs/procedures/get_scheduled_job.sql b/packages/database-jobs-v1/verify/schemas/app_jobs/procedures/get_scheduled_job.sql new file mode 100644 index 000000000..bb7e58d7f --- /dev/null +++ b/packages/database-jobs-v1/verify/schemas/app_jobs/procedures/get_scheduled_job.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/procedures/get_scheduled_job on pg + +BEGIN; + +SELECT verify_function ('app_jobs.get_scheduled_job'); + +ROLLBACK; diff --git a/packages/database-jobs-v1/verify/schemas/app_jobs/procedures/permanently_fail_jobs.sql b/packages/database-jobs-v1/verify/schemas/app_jobs/procedures/permanently_fail_jobs.sql new file mode 100644 index 000000000..dfd8852f0 --- /dev/null +++ b/packages/database-jobs-v1/verify/schemas/app_jobs/procedures/permanently_fail_jobs.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/procedures/permanently_fail_jobs on pg + +BEGIN; + +SELECT verify_function ('app_jobs.permanently_fail_jobs'); + +ROLLBACK; diff --git a/packages/database-jobs-v1/verify/schemas/app_jobs/procedures/release_jobs.sql b/packages/database-jobs-v1/verify/schemas/app_jobs/procedures/release_jobs.sql new file mode 100644 index 000000000..70004e7e1 --- /dev/null +++ b/packages/database-jobs-v1/verify/schemas/app_jobs/procedures/release_jobs.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/procedures/release_jobs on pg + +BEGIN; + +SELECT verify_function ('app_jobs.release_jobs'); + +ROLLBACK; diff --git a/packages/database-jobs-v1/verify/schemas/app_jobs/procedures/release_scheduled_jobs.sql b/packages/database-jobs-v1/verify/schemas/app_jobs/procedures/release_scheduled_jobs.sql new file mode 100644 index 000000000..5b9b5929a --- /dev/null +++ b/packages/database-jobs-v1/verify/schemas/app_jobs/procedures/release_scheduled_jobs.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/procedures/release_scheduled_jobs on pg + +BEGIN; + +SELECT verify_function ('app_jobs.release_scheduled_jobs'); + +ROLLBACK; diff --git a/packages/database-jobs-v1/verify/schemas/app_jobs/procedures/reschedule_jobs.sql b/packages/database-jobs-v1/verify/schemas/app_jobs/procedures/reschedule_jobs.sql new file mode 100644 index 000000000..80ab587b3 --- /dev/null +++ b/packages/database-jobs-v1/verify/schemas/app_jobs/procedures/reschedule_jobs.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/procedures/reschedule_jobs on pg + +BEGIN; + +SELECT verify_function ('app_jobs.reschedule_jobs'); + +ROLLBACK; diff --git a/packages/database-jobs-v1/verify/schemas/app_jobs/procedures/run_scheduled_job.sql b/packages/database-jobs-v1/verify/schemas/app_jobs/procedures/run_scheduled_job.sql new file mode 100644 index 000000000..02257023b --- /dev/null +++ b/packages/database-jobs-v1/verify/schemas/app_jobs/procedures/run_scheduled_job.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/procedures/run_scheduled_job on pg + +BEGIN; + +SELECT verify_function ('app_jobs.run_scheduled_job'); + +ROLLBACK; diff --git a/packages/database-jobs-v1/verify/schemas/app_jobs/schema.sql b/packages/database-jobs-v1/verify/schemas/app_jobs/schema.sql new file mode 100644 index 000000000..5e0b19d49 --- /dev/null +++ b/packages/database-jobs-v1/verify/schemas/app_jobs/schema.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/schema on pg + +BEGIN; + +SELECT verify_schema ('app_jobs'); + +ROLLBACK; diff --git a/packages/database-jobs-v1/verify/schemas/app_jobs/tables/job_queues/grants/grant_select_insert_update_delete_to_administrator.sql b/packages/database-jobs-v1/verify/schemas/app_jobs/tables/job_queues/grants/grant_select_insert_update_delete_to_administrator.sql new file mode 100644 index 000000000..d645d8558 --- /dev/null +++ b/packages/database-jobs-v1/verify/schemas/app_jobs/tables/job_queues/grants/grant_select_insert_update_delete_to_administrator.sql @@ -0,0 +1,10 @@ +-- Verify schemas/app_jobs/tables/job_queues/grants/grant_select_insert_update_delete_to_administrator on pg + +BEGIN; + + SELECT has_table_privilege('administrator', 'app_jobs.job_queues', 'SELECT'); + SELECT has_table_privilege('administrator', 'app_jobs.job_queues', 'INSERT'); + SELECT has_table_privilege('administrator', 'app_jobs.job_queues', 'UPDATE'); + SELECT has_table_privilege('administrator', 'app_jobs.job_queues', 'DELETE'); + +ROLLBACK; diff --git a/packages/database-jobs-v1/verify/schemas/app_jobs/tables/job_queues/indexes/job_queues_locked_by_idx.sql b/packages/database-jobs-v1/verify/schemas/app_jobs/tables/job_queues/indexes/job_queues_locked_by_idx.sql new file mode 100644 index 000000000..bb3786608 --- /dev/null +++ b/packages/database-jobs-v1/verify/schemas/app_jobs/tables/job_queues/indexes/job_queues_locked_by_idx.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/tables/job_queues/indexes/job_queues_locked_by_idx on pg + +BEGIN; + +SELECT verify_index ('app_jobs.job_queues', 'job_queues_locked_by_idx'); + +ROLLBACK; diff --git a/packages/database-jobs-v1/verify/schemas/app_jobs/tables/job_queues/table.sql b/packages/database-jobs-v1/verify/schemas/app_jobs/tables/job_queues/table.sql new file mode 100644 index 000000000..3a5e4b1cd --- /dev/null +++ b/packages/database-jobs-v1/verify/schemas/app_jobs/tables/job_queues/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/tables/job_queues/table on pg + +BEGIN; + +SELECT verify_table ('app_jobs.job_queues'); + +ROLLBACK; diff --git a/packages/database-jobs-v1/verify/schemas/app_jobs/tables/jobs/grants/grant_select_insert_update_delete_to_administrator.sql b/packages/database-jobs-v1/verify/schemas/app_jobs/tables/jobs/grants/grant_select_insert_update_delete_to_administrator.sql new file mode 100644 index 000000000..6255d7164 --- /dev/null +++ b/packages/database-jobs-v1/verify/schemas/app_jobs/tables/jobs/grants/grant_select_insert_update_delete_to_administrator.sql @@ -0,0 +1,10 @@ +-- Verify schemas/app_jobs/tables/jobs/grants/grant_select_insert_update_delete_to_administrator on pg + +BEGIN; + + SELECT has_table_privilege('administrator', 'app_jobs.jobs', 'SELECT'); + SELECT has_table_privilege('administrator', 'app_jobs.jobs', 'INSERT'); + SELECT has_table_privilege('administrator', 'app_jobs.jobs', 'UPDATE'); + SELECT has_table_privilege('administrator', 'app_jobs.jobs', 'DELETE'); + +ROLLBACK; diff --git a/packages/database-jobs-v1/verify/schemas/app_jobs/tables/jobs/indexes/jobs_locked_by_idx.sql b/packages/database-jobs-v1/verify/schemas/app_jobs/tables/jobs/indexes/jobs_locked_by_idx.sql new file mode 100644 index 000000000..3635677ae --- /dev/null +++ b/packages/database-jobs-v1/verify/schemas/app_jobs/tables/jobs/indexes/jobs_locked_by_idx.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/tables/jobs/indexes/jobs_locked_by_idx on pg + +BEGIN; + +SELECT verify_index ('app_jobs.jobs', 'jobs_locked_by_idx'); + +ROLLBACK; diff --git a/packages/database-jobs-v1/verify/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql b/packages/database-jobs-v1/verify/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql new file mode 100644 index 000000000..2fc8b4ccd --- /dev/null +++ b/packages/database-jobs-v1/verify/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx on pg + +BEGIN; + +SELECT verify_index ('app_jobs.jobs', 'priority_run_at_id_idx'); + +ROLLBACK; diff --git a/packages/database-jobs-v1/verify/schemas/app_jobs/tables/jobs/table.sql b/packages/database-jobs-v1/verify/schemas/app_jobs/tables/jobs/table.sql new file mode 100644 index 000000000..aaa0584da --- /dev/null +++ b/packages/database-jobs-v1/verify/schemas/app_jobs/tables/jobs/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/tables/jobs/table on pg + +BEGIN; + +SELECT verify_table ('app_jobs.jobs'); + +ROLLBACK; diff --git a/packages/database-jobs-v1/verify/schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count.sql b/packages/database-jobs-v1/verify/schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count.sql new file mode 100644 index 000000000..97b717d08 --- /dev/null +++ b/packages/database-jobs-v1/verify/schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count.sql @@ -0,0 +1,10 @@ +-- Verify schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count on pg +BEGIN; +SELECT + verify_function ('app_jobs.tg_decrease_job_queue_count'); +SELECT + verify_trigger ('app_jobs.decrease_job_queue_count_on_delete'); +SELECT + verify_trigger ('app_jobs.decrease_job_queue_count_on_update'); +ROLLBACK; + diff --git a/packages/database-jobs-v1/verify/schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count.sql b/packages/database-jobs-v1/verify/schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count.sql new file mode 100644 index 000000000..a6e89dd17 --- /dev/null +++ b/packages/database-jobs-v1/verify/schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count.sql @@ -0,0 +1,10 @@ +-- Verify schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count on pg +BEGIN; +SELECT + verify_function ('app_jobs.tg_increase_job_queue_count'); +SELECT + verify_trigger ('app_jobs._500_increase_job_queue_count_on_insert'); +SELECT + verify_trigger ('app_jobs._500_increase_job_queue_count_on_update'); +ROLLBACK; + diff --git a/packages/database-jobs-v1/verify/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql b/packages/database-jobs-v1/verify/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql new file mode 100644 index 000000000..dc0436efa --- /dev/null +++ b/packages/database-jobs-v1/verify/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql @@ -0,0 +1,6 @@ +-- Verify schemas/app_jobs/tables/jobs/triggers/notify_worker on pg +BEGIN; +SELECT + verify_trigger ('app_jobs._900_notify_worker'); +ROLLBACK; + diff --git a/packages/database-jobs-v1/verify/schemas/app_jobs/tables/jobs/triggers/timestamps.sql b/packages/database-jobs-v1/verify/schemas/app_jobs/tables/jobs/triggers/timestamps.sql new file mode 100644 index 000000000..ed9466a37 --- /dev/null +++ b/packages/database-jobs-v1/verify/schemas/app_jobs/tables/jobs/triggers/timestamps.sql @@ -0,0 +1,16 @@ +-- Verify schemas/app_jobs/tables/jobs/triggers/timestamps on pg +BEGIN; +SELECT + created_at +FROM + app_jobs.jobs +LIMIT 1; +SELECT + updated_at +FROM + app_jobs.jobs +LIMIT 1; +SELECT + verify_trigger ('app_jobs._100_update_jobs_modtime_tg'); +ROLLBACK; + diff --git a/packages/database-jobs-v1/verify/schemas/app_jobs/tables/scheduled_jobs/grants/grant_select_insert_update_delete_to_administrator.sql b/packages/database-jobs-v1/verify/schemas/app_jobs/tables/scheduled_jobs/grants/grant_select_insert_update_delete_to_administrator.sql new file mode 100644 index 000000000..c4aa4eb6c --- /dev/null +++ b/packages/database-jobs-v1/verify/schemas/app_jobs/tables/scheduled_jobs/grants/grant_select_insert_update_delete_to_administrator.sql @@ -0,0 +1,10 @@ +-- Verify schemas/app_jobs/tables/scheduled_jobs/grants/grant_select_insert_update_delete_to_administrator on pg + +BEGIN; + + SELECT has_table_privilege('administrator', 'app_jobs.scheduled_jobs', 'SELECT'); + SELECT has_table_privilege('administrator', 'app_jobs.scheduled_jobs', 'INSERT'); + SELECT has_table_privilege('administrator', 'app_jobs.scheduled_jobs', 'UPDATE'); + SELECT has_table_privilege('administrator', 'app_jobs.scheduled_jobs', 'DELETE'); + +ROLLBACK; diff --git a/packages/database-jobs-v1/verify/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_locked_by_idx.sql b/packages/database-jobs-v1/verify/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_locked_by_idx.sql new file mode 100644 index 000000000..34ee9f117 --- /dev/null +++ b/packages/database-jobs-v1/verify/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_locked_by_idx.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_locked_by_idx on pg + +BEGIN; + +SELECT verify_index ('app_jobs.scheduled_jobs', 'scheduled_jobs_locked_by_idx'); + +ROLLBACK; diff --git a/packages/database-jobs-v1/verify/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_priority_id_idx.sql b/packages/database-jobs-v1/verify/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_priority_id_idx.sql new file mode 100644 index 000000000..d26a68223 --- /dev/null +++ b/packages/database-jobs-v1/verify/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_priority_id_idx.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_priority_id_idx on pg + +BEGIN; + +SELECT verify_index ('app_jobs.scheduled_jobs', 'scheduled_jobs_priority_id_idx'); + +ROLLBACK; diff --git a/packages/database-jobs-v1/verify/schemas/app_jobs/tables/scheduled_jobs/table.sql b/packages/database-jobs-v1/verify/schemas/app_jobs/tables/scheduled_jobs/table.sql new file mode 100644 index 000000000..065f427b1 --- /dev/null +++ b/packages/database-jobs-v1/verify/schemas/app_jobs/tables/scheduled_jobs/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/tables/scheduled_jobs/table on pg + +BEGIN; + +SELECT verify_table ('app_jobs.scheduled_jobs'); + +ROLLBACK; diff --git a/packages/database-jobs-v1/verify/schemas/app_jobs/tables/scheduled_jobs/triggers/notify_scheduled_job.sql b/packages/database-jobs-v1/verify/schemas/app_jobs/tables/scheduled_jobs/triggers/notify_scheduled_job.sql new file mode 100644 index 000000000..599c63a3a --- /dev/null +++ b/packages/database-jobs-v1/verify/schemas/app_jobs/tables/scheduled_jobs/triggers/notify_scheduled_job.sql @@ -0,0 +1,8 @@ +-- Verify schemas/app_jobs/tables/scheduled_jobs/triggers/notify_scheduled_job on pg + +BEGIN; + + +SELECT verify_trigger ('app_jobs._900_notify_scheduled_job'); + +ROLLBACK; diff --git a/packages/database-jobs-v1/verify/schemas/app_jobs/triggers/tg_add_job_with_fields.sql b/packages/database-jobs-v1/verify/schemas/app_jobs/triggers/tg_add_job_with_fields.sql new file mode 100644 index 000000000..9b36e4f2e --- /dev/null +++ b/packages/database-jobs-v1/verify/schemas/app_jobs/triggers/tg_add_job_with_fields.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/triggers/tg_add_job_with_fields on pg + +BEGIN; + +SELECT verify_function ('app_jobs.trigger_job_with_fields'); + +ROLLBACK; diff --git a/packages/database-jobs-v1/verify/schemas/app_jobs/triggers/tg_add_job_with_row.sql b/packages/database-jobs-v1/verify/schemas/app_jobs/triggers/tg_add_job_with_row.sql new file mode 100644 index 000000000..bdf8cc7eb --- /dev/null +++ b/packages/database-jobs-v1/verify/schemas/app_jobs/triggers/tg_add_job_with_row.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/triggers/tg_add_job_with_row on pg + +BEGIN; + +SELECT verify_function ('app_jobs.tg_add_job_with_row'); + +ROLLBACK; diff --git a/packages/database-jobs-v1/verify/schemas/app_jobs/triggers/tg_add_job_with_row_id.sql b/packages/database-jobs-v1/verify/schemas/app_jobs/triggers/tg_add_job_with_row_id.sql new file mode 100644 index 000000000..72b5a7b90 --- /dev/null +++ b/packages/database-jobs-v1/verify/schemas/app_jobs/triggers/tg_add_job_with_row_id.sql @@ -0,0 +1,6 @@ +-- Verify schemas/app_jobs/triggers/tg_add_job_with_row_id on pg +BEGIN; +SELECT + verify_function ('app_jobs.tg_add_job_with_row_id'); +ROLLBACK; + diff --git a/packages/database-jobs-v1/verify/schemas/app_jobs/triggers/tg_update_timestamps.sql b/packages/database-jobs-v1/verify/schemas/app_jobs/triggers/tg_update_timestamps.sql new file mode 100644 index 000000000..fd53ed3e9 --- /dev/null +++ b/packages/database-jobs-v1/verify/schemas/app_jobs/triggers/tg_update_timestamps.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/triggers/tg_update_timestamps on pg + +BEGIN; + +SELECT verify_function ('app_jobs.tg_update_timestamps'); + +ROLLBACK; diff --git a/packages/database-jobs/__tests__/__snapshots__/jobs.test.ts.snap b/packages/database-jobs/__tests__/__snapshots__/jobs.test.ts.snap index 78807f7ca..2a9114b79 100644 --- a/packages/database-jobs/__tests__/__snapshots__/jobs.test.ts.snap +++ b/packages/database-jobs/__tests__/__snapshots__/jobs.test.ts.snap @@ -2,9 +2,11 @@ exports[`scheduled jobs schedule jobs 1`] = ` { + "actor_id": null, "attempts": 0, "database_id": "5b720132-17d5-424d-9bcb-ee7b17c13d43", "id": "1", + "is_available": true, "key": null, "last_error": null, "locked_at": null, diff --git a/packages/database-jobs/__tests__/jobs.test.ts b/packages/database-jobs/__tests__/jobs.test.ts index 453de4c04..44297918a 100644 --- a/packages/database-jobs/__tests__/jobs.test.ts +++ b/packages/database-jobs/__tests__/jobs.test.ts @@ -65,19 +65,20 @@ describe('scheduled jobs', () => { const start = new Date(Date.now() + 10000); // 10s const end = new Date(start.getTime() + 180000); // +3min + // Set JWT claims for the session (required by add_scheduled_job) + await pg.any(`SELECT set_config('jwt.claims.database_id', $1, false)`, [database_id]); + const [result] = await pg.any( `SELECT * FROM app_jobs.add_scheduled_job( - db_id := $1::uuid, - identifier := $2::text, - payload := $3::json, - schedule_info := $4::json, - job_key := $5::text, - queue_name := $6::text, - max_attempts := $7::integer, - priority := $8::integer + identifier := $1::text, + payload := $2::json, + schedule_info := $3::json, + job_key := $4::text, + queue_name := $5::text, + max_attempts := $6::integer, + priority := $7::integer )`, [ - database_id, 'my_job', { just: 'run it' }, { start, end, rule: '*/1 * * * *' }, @@ -101,17 +102,15 @@ describe('scheduled jobs', () => { const [result2] = await pg.any( `SELECT * FROM app_jobs.add_scheduled_job( - db_id := $1, - identifier := $2, - payload := $3, - schedule_info := $4, - job_key := $5, - queue_name := $6, - max_attempts := $7, - priority := $8 + identifier := $1, + payload := $2, + schedule_info := $3, + job_key := $4, + queue_name := $5, + max_attempts := $6, + priority := $7 )`, [ - database_id, 'my_job', { just: 'run it' }, { start, end, rule: '*/1 * * * *' }, diff --git a/packages/database-jobs/deploy/schemas/app_jobs/procedures/add_job.sql b/packages/database-jobs/deploy/schemas/app_jobs/procedures/add_job.sql index 1645b1a5a..8165d50eb 100644 --- a/packages/database-jobs/deploy/schemas/app_jobs/procedures/add_job.sql +++ b/packages/database-jobs/deploy/schemas/app_jobs/procedures/add_job.sql @@ -2,10 +2,11 @@ -- requires: schemas/app_jobs/schema -- requires: schemas/app_jobs/tables/jobs/table -- requires: schemas/app_jobs/tables/job_queues/table +-- requires: pgpm-jwt-claims:schemas/jwt_private/procedures/current_database_id +-- requires: pgpm-jwt-claims:schemas/jwt_public/procedures/current_user_id BEGIN; CREATE FUNCTION app_jobs.add_job ( - db_id uuid, identifier text, payload json DEFAULT '{}' ::json, job_key text DEFAULT NULL, @@ -18,14 +19,18 @@ CREATE FUNCTION app_jobs.add_job ( AS $$ DECLARE v_job app_jobs.jobs; + v_database_id uuid; + v_actor_id uuid; BEGIN - -- Bake actor_id into payload - payload := (coalesce(payload, '{}'::json)::jsonb || jsonb_build_object('actor_id', jwt_public.current_user_id()))::json; + -- Read context from JWT claims + v_database_id := jwt_private.current_database_id(); + v_actor_id := jwt_public.current_user_id(); IF job_key IS NOT NULL THEN - -- Upsert job + -- Upsert job INSERT INTO app_jobs.jobs ( database_id, + actor_id, task_identifier, payload, queue_name, @@ -34,52 +39,51 @@ BEGIN key, priority ) VALUES ( - db_id, + v_database_id, + v_actor_id, identifier, - coalesce(payload, - '{}'::json), + coalesce(payload, '{}'::json), queue_name, coalesce(run_at, now()), coalesce(max_attempts, 25), job_key, coalesce(priority, 0) - ) - ON CONFLICT (key) - DO UPDATE SET + ) + ON CONFLICT (key) + DO UPDATE SET task_identifier = EXCLUDED.task_identifier, payload = EXCLUDED.payload, queue_name = EXCLUDED.queue_name, max_attempts = EXCLUDED.max_attempts, run_at = EXCLUDED.run_at, - priority = EXCLUDED.priority, - -- always reset error/retry state - attempts = 0, last_error = NULL - WHERE - jobs.locked_at IS NULL - RETURNING - * INTO v_job; + priority = EXCLUDED.priority, + -- always reset error/retry state + attempts = 0, last_error = NULL + WHERE + jobs.locked_at IS NULL + RETURNING + * INTO v_job; - -- If upsert succeeded (insert or update), return early - - IF NOT (v_job IS NULL) THEN - RETURN v_job; - END IF; + -- If upsert succeeded (insert or update), return early + IF NOT (v_job IS NULL) THEN + RETURN v_job; + END IF; - -- Upsert failed -> there must be an existing job that is locked. Remove - -- existing key to allow a new one to be inserted, and prevent any + -- Upsert failed -> there must be an existing job that is locked. Remove + -- existing key to allow a new one to be inserted, and prevent any -- subsequent retries by bumping attempts to the max allowed. - - UPDATE - app_jobs.jobs - SET - KEY = NULL, - attempts = jobs.max_attempts - WHERE - KEY = job_key; + UPDATE + app_jobs.jobs + SET + key = NULL, + attempts = jobs.max_attempts + WHERE + key = job_key; END IF; INSERT INTO app_jobs.jobs ( database_id, + actor_id, task_identifier, payload, queue_name, @@ -87,7 +91,8 @@ BEGIN max_attempts, priority ) VALUES ( - db_id, + v_database_id, + v_actor_id, identifier, payload, queue_name, @@ -103,4 +108,3 @@ $$ LANGUAGE 'plpgsql' VOLATILE SECURITY DEFINER; COMMIT; - diff --git a/packages/database-jobs/deploy/schemas/app_jobs/procedures/add_scheduled_job.sql b/packages/database-jobs/deploy/schemas/app_jobs/procedures/add_scheduled_job.sql index 1cc20d055..c5c730c9d 100644 --- a/packages/database-jobs/deploy/schemas/app_jobs/procedures/add_scheduled_job.sql +++ b/packages/database-jobs/deploy/schemas/app_jobs/procedures/add_scheduled_job.sql @@ -2,11 +2,12 @@ -- requires: schemas/app_jobs/schema -- requires: schemas/app_jobs/tables/scheduled_jobs/table +-- requires: pgpm-jwt-claims:schemas/jwt_private/procedures/current_database_id +-- requires: pgpm-jwt-claims:schemas/jwt_public/procedures/current_user_id BEGIN; CREATE FUNCTION app_jobs.add_scheduled_job( - db_id uuid, identifier text, payload json DEFAULT '{}'::json, schedule_info json DEFAULT '{}'::json, @@ -19,12 +20,18 @@ CREATE FUNCTION app_jobs.add_scheduled_job( AS $$ DECLARE v_job app_jobs.scheduled_jobs; + v_database_id uuid; + v_actor_id uuid; BEGIN + v_database_id := jwt_private.current_database_id(); + v_actor_id := jwt_public.current_user_id(); + IF job_key IS NOT NULL THEN - -- Upsert job + -- Upsert job INSERT INTO app_jobs.scheduled_jobs ( database_id, + actor_id, task_identifier, payload, queue_name, @@ -33,7 +40,8 @@ BEGIN key, priority ) VALUES ( - db_id, + v_database_id, + v_actor_id, identifier, coalesce(payload, '{}'::json), queue_name, @@ -41,37 +49,38 @@ BEGIN coalesce(max_attempts, 25), job_key, coalesce(priority, 0) - ) - ON CONFLICT (key) - DO UPDATE SET + ) + ON CONFLICT (key) + DO UPDATE SET task_identifier = EXCLUDED.task_identifier, payload = EXCLUDED.payload, queue_name = EXCLUDED.queue_name, max_attempts = EXCLUDED.max_attempts, schedule_info = EXCLUDED.schedule_info, priority = EXCLUDED.priority - WHERE - scheduled_jobs.locked_at IS NULL - RETURNING - * INTO v_job; + WHERE + scheduled_jobs.locked_at IS NULL + RETURNING + * INTO v_job; + + -- If upsert succeeded (insert or update), return early - -- If upsert succeeded (insert or update), return early - - IF NOT (v_job IS NULL) THEN - RETURN v_job; - END IF; + IF NOT (v_job IS NULL) THEN + RETURN v_job; + END IF; - -- Upsert failed -> there must be an existing scheduled job that is locked. Remove + -- Upsert failed -> there must be an existing scheduled job that is locked. Remove -- and allow a new one to be inserted - DELETE FROM - app_jobs.scheduled_jobs - WHERE - KEY = job_key; + DELETE FROM + app_jobs.scheduled_jobs + WHERE + KEY = job_key; END IF; INSERT INTO app_jobs.scheduled_jobs ( database_id, + actor_id, task_identifier, payload, queue_name, @@ -79,7 +88,8 @@ BEGIN max_attempts, priority ) VALUES ( - db_id, + v_database_id, + v_actor_id, identifier, payload, queue_name, diff --git a/packages/database-jobs/deploy/schemas/app_jobs/procedures/force_unlock_workers.sql b/packages/database-jobs/deploy/schemas/app_jobs/procedures/force_unlock_workers.sql new file mode 100644 index 000000000..c28392cac --- /dev/null +++ b/packages/database-jobs/deploy/schemas/app_jobs/procedures/force_unlock_workers.sql @@ -0,0 +1,20 @@ +-- Deploy schemas/app_jobs/procedures/force_unlock_workers to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/jobs/table +-- requires: schemas/app_jobs/tables/job_queues/table + +BEGIN; +CREATE FUNCTION app_jobs.force_unlock_workers (worker_ids text[]) + RETURNS void + LANGUAGE sql + VOLATILE + AS $$ + UPDATE app_jobs.jobs + SET locked_at = NULL, locked_by = NULL + WHERE locked_by = ANY (worker_ids); + + UPDATE app_jobs.job_queues + SET locked_at = NULL, locked_by = NULL + WHERE locked_by = ANY (worker_ids); +$$; +COMMIT; diff --git a/packages/database-jobs/deploy/schemas/app_jobs/procedures/get_job.sql b/packages/database-jobs/deploy/schemas/app_jobs/procedures/get_job.sql index 9c37d9c94..e3ac03fb1 100644 --- a/packages/database-jobs/deploy/schemas/app_jobs/procedures/get_job.sql +++ b/packages/database-jobs/deploy/schemas/app_jobs/procedures/get_job.sql @@ -4,7 +4,11 @@ -- requires: schemas/app_jobs/tables/jobs/table BEGIN; -CREATE FUNCTION app_jobs.get_job (worker_id text, task_identifiers text[] DEFAULT NULL, job_expiry interval DEFAULT '4 hours') +CREATE FUNCTION app_jobs.get_job ( + worker_id text, + task_identifiers text[] DEFAULT NULL, + job_expiry interval DEFAULT '4 hours' +) RETURNS app_jobs.jobs LANGUAGE plpgsql AS $$ @@ -14,79 +18,51 @@ DECLARE v_row app_jobs.jobs; v_now timestamptz = now(); BEGIN - IF worker_id IS NULL THEN - RAISE exception 'INVALID_WORKER_ID'; + RAISE EXCEPTION 'INVALID_WORKER_ID'; END IF; - -- - - SELECT - jobs.queue_name, - jobs.id INTO v_queue_name, - v_job_id - FROM - app_jobs.jobs - WHERE (jobs.locked_at IS NULL - OR jobs.locked_at < (v_now - job_expiry)) + SELECT jobs.queue_name, jobs.id + INTO v_queue_name, v_job_id + FROM app_jobs.jobs + WHERE is_available = true + AND (jobs.locked_at IS NULL + OR jobs.locked_at < (v_now - job_expiry)) AND (jobs.queue_name IS NULL - OR EXISTS ( - SELECT - 1 - FROM - app_jobs.job_queues - WHERE - job_queues.queue_name = jobs.queue_name - AND (job_queues.locked_at IS NULL - OR job_queues.locked_at < (v_now - job_expiry)) - FOR UPDATE - SKIP LOCKED)) + OR jobs.queue_name IN ( + SELECT jq.queue_name + FROM app_jobs.job_queues jq + WHERE (jq.locked_at IS NULL + OR jq.locked_at < (v_now - job_expiry)) + FOR UPDATE SKIP LOCKED + )) AND run_at <= v_now AND attempts < max_attempts AND (task_identifiers IS NULL OR task_identifier = ANY (task_identifiers)) - ORDER BY - priority ASC, - run_at ASC, - id ASC + ORDER BY priority ASC, run_at ASC, id ASC LIMIT 1 - FOR UPDATE - SKIP LOCKED; - - -- + FOR UPDATE SKIP LOCKED; IF v_job_id IS NULL THEN RETURN NULL; END IF; - -- - IF v_queue_name IS NOT NULL THEN - UPDATE - app_jobs.job_queues - SET - locked_by = worker_id, - locked_at = v_now - WHERE - job_queues.queue_name = v_queue_name; + UPDATE app_jobs.job_queues + SET locked_by = worker_id, locked_at = v_now + WHERE job_queues.queue_name = v_queue_name; END IF; - -- - - UPDATE - app_jobs.jobs + UPDATE app_jobs.jobs SET attempts = attempts + 1, locked_by = worker_id, locked_at = v_now - WHERE - id = v_job_id - RETURNING - * INTO v_row; + WHERE id = v_job_id + RETURNING * INTO v_row; - -- RETURN v_row; END; $$; COMMIT; - diff --git a/packages/database-jobs/deploy/schemas/app_jobs/procedures/remove_job.sql b/packages/database-jobs/deploy/schemas/app_jobs/procedures/remove_job.sql new file mode 100644 index 000000000..b6688afeb --- /dev/null +++ b/packages/database-jobs/deploy/schemas/app_jobs/procedures/remove_job.sql @@ -0,0 +1,34 @@ +-- Deploy schemas/app_jobs/procedures/remove_job to pg +-- requires: schemas/app_jobs/schema +-- requires: schemas/app_jobs/tables/jobs/table + +BEGIN; +CREATE FUNCTION app_jobs.remove_job (job_key text) + RETURNS app_jobs.jobs + LANGUAGE plpgsql + STRICT + AS $$ +DECLARE + v_job app_jobs.jobs; +BEGIN + DELETE FROM app_jobs.jobs + WHERE key = job_key + AND (locked_at IS NULL + OR locked_at < NOW() - interval '4 hours') + RETURNING * INTO v_job; + + IF NOT (v_job IS NULL) THEN + RETURN v_job; + END IF; + + UPDATE app_jobs.jobs + SET + key = NULL, + attempts = jobs.max_attempts + WHERE key = job_key + RETURNING * INTO v_job; + + RETURN v_job; +END; +$$; +COMMIT; diff --git a/packages/database-jobs/deploy/schemas/app_jobs/procedures/run_scheduled_job.sql b/packages/database-jobs/deploy/schemas/app_jobs/procedures/run_scheduled_job.sql index 41c258b35..7a4d58310 100644 --- a/packages/database-jobs/deploy/schemas/app_jobs/procedures/run_scheduled_job.sql +++ b/packages/database-jobs/deploy/schemas/app_jobs/procedures/run_scheduled_job.sql @@ -41,6 +41,7 @@ BEGIN -- insert new job INSERT INTO app_jobs.jobs ( database_id, + actor_id, queue_name, task_identifier, payload, @@ -49,6 +50,7 @@ BEGIN key ) SELECT database_id, + actor_id, queue_name, task_identifier, payload, diff --git a/packages/database-jobs/deploy/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql b/packages/database-jobs/deploy/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql index 78b03ff13..35bf1d6e6 100644 --- a/packages/database-jobs/deploy/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql +++ b/packages/database-jobs/deploy/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql @@ -3,6 +3,15 @@ -- requires: schemas/app_jobs/tables/jobs/table BEGIN; -CREATE INDEX priority_run_at_id_idx ON app_jobs.jobs (priority, run_at, id); -COMMIT; +CREATE INDEX jobs_main_index + ON app_jobs.jobs USING btree (priority, run_at) + INCLUDE (id, queue_name) + WHERE (is_available = true); + +CREATE INDEX jobs_no_queue_index + ON app_jobs.jobs USING btree (priority, run_at) + INCLUDE (id) + WHERE (is_available = true AND queue_name IS NULL); + +COMMIT; diff --git a/packages/database-jobs/deploy/schemas/app_jobs/tables/jobs/table.sql b/packages/database-jobs/deploy/schemas/app_jobs/tables/jobs/table.sql index 48ea7c146..3083bb68f 100644 --- a/packages/database-jobs/deploy/schemas/app_jobs/tables/jobs/table.sql +++ b/packages/database-jobs/deploy/schemas/app_jobs/tables/jobs/table.sql @@ -4,8 +4,9 @@ BEGIN; CREATE TABLE app_jobs.jobs ( id bigserial PRIMARY KEY, - database_id uuid NOT NULL, - queue_name text DEFAULT (public.gen_random_uuid ()) ::text, + database_id uuid, + actor_id uuid, + queue_name text DEFAULT NULL, task_identifier text NOT NULL, payload json DEFAULT '{}' ::json NOT NULL, priority integer DEFAULT 0 NOT NULL, @@ -16,17 +17,19 @@ CREATE TABLE app_jobs.jobs ( last_error text, locked_at timestamptz, locked_by text, + is_available boolean GENERATED ALWAYS AS ((locked_at IS NULL) AND (attempts < max_attempts)) STORED NOT NULL, CHECK (length(key) < 513), CHECK (length(task_identifier) < 127), - CHECK (max_attempts > 0), + CHECK (max_attempts >= 1), CHECK (length(queue_name) < 127), CHECK (length(locked_by) > 3), UNIQUE (key) ); -COMMENT ON TABLE app_jobs.jobs IS 'Background job queue with database scoping: each row is a pending or in-progress task for a specific database'; +COMMENT ON TABLE app_jobs.jobs IS 'Background job queue: each row is a pending or in-progress task, optionally scoped to a database'; COMMENT ON COLUMN app_jobs.jobs.id IS 'Auto-incrementing job identifier'; -COMMENT ON COLUMN app_jobs.jobs.database_id IS 'Database this job belongs to, for multi-tenant job isolation'; +COMMENT ON COLUMN app_jobs.jobs.database_id IS 'Database this job belongs to (nullable for system-level jobs without tenant context)'; +COMMENT ON COLUMN app_jobs.jobs.actor_id IS 'User who triggered this job, read from JWT claims at enqueue time'; COMMENT ON COLUMN app_jobs.jobs.queue_name IS 'Name of the queue this job belongs to; used for worker routing and concurrency control'; COMMENT ON COLUMN app_jobs.jobs.task_identifier IS 'Identifier for the task type (maps to a worker handler function)'; COMMENT ON COLUMN app_jobs.jobs.payload IS 'JSON payload of arguments passed to the task handler'; @@ -38,6 +41,7 @@ COMMENT ON COLUMN app_jobs.jobs.key IS 'Optional unique deduplication key; preve COMMENT ON COLUMN app_jobs.jobs.last_error IS 'Error message from the most recent failed attempt'; COMMENT ON COLUMN app_jobs.jobs.locked_at IS 'Timestamp when a worker locked this job for processing'; COMMENT ON COLUMN app_jobs.jobs.locked_by IS 'Identifier of the worker that currently holds the lock'; +COMMENT ON COLUMN app_jobs.jobs.is_available IS 'Generated column: true when job is unlocked and has remaining attempts'; COMMIT; diff --git a/packages/database-jobs/deploy/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql b/packages/database-jobs/deploy/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql index 9e6cec54c..e847ebbf9 100644 --- a/packages/database-jobs/deploy/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql +++ b/packages/database-jobs/deploy/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql @@ -5,9 +5,19 @@ -- requires: schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count BEGIN; -CREATE TRIGGER _900_notify_worker - AFTER INSERT ON app_jobs.jobs - FOR EACH ROW - EXECUTE PROCEDURE app_jobs.do_notify ('jobs:insert'); -COMMIT; +CREATE FUNCTION app_jobs.tg_jobs__after_insert () + RETURNS TRIGGER + AS $$ +BEGIN + PERFORM + pg_notify('jobs:insert', ''); + RETURN NULL; +END; +$$ +LANGUAGE plpgsql; +CREATE TRIGGER _900_after_insert + AFTER INSERT ON app_jobs.jobs + FOR EACH STATEMENT + EXECUTE PROCEDURE app_jobs.tg_jobs__after_insert (); +COMMIT; diff --git a/packages/database-jobs/deploy/schemas/app_jobs/tables/scheduled_jobs/table.sql b/packages/database-jobs/deploy/schemas/app_jobs/tables/scheduled_jobs/table.sql index 75d81c9e3..74ce55841 100644 --- a/packages/database-jobs/deploy/schemas/app_jobs/tables/scheduled_jobs/table.sql +++ b/packages/database-jobs/deploy/schemas/app_jobs/tables/scheduled_jobs/table.sql @@ -4,8 +4,9 @@ BEGIN; CREATE TABLE app_jobs.scheduled_jobs ( id bigserial PRIMARY KEY, - database_id uuid NOT NULL, - queue_name text DEFAULT (public.gen_random_uuid ()) ::text, + database_id uuid, + actor_id uuid, + queue_name text DEFAULT NULL, task_identifier text NOT NULL, payload json DEFAULT '{}' ::json NOT NULL, priority integer DEFAULT 0 NOT NULL, @@ -18,15 +19,16 @@ CREATE TABLE app_jobs.scheduled_jobs ( last_scheduled_id bigint, CHECK (length(key) < 513), CHECK (length(task_identifier) < 127), - CHECK (max_attempts > 0), + CHECK (max_attempts >= 1), CHECK (length(queue_name) < 127), CHECK (length(locked_by) > 3), UNIQUE (key) ); -COMMENT ON TABLE app_jobs.scheduled_jobs IS 'Recurring/cron-style job definitions with database scoping: each row spawns jobs on a schedule for a specific database'; +COMMENT ON TABLE app_jobs.scheduled_jobs IS 'Recurring/cron-style job definitions: each row spawns jobs on a schedule, optionally scoped to a database'; COMMENT ON COLUMN app_jobs.scheduled_jobs.id IS 'Auto-incrementing scheduled job identifier'; -COMMENT ON COLUMN app_jobs.scheduled_jobs.database_id IS 'Database this scheduled job belongs to, for multi-tenant isolation'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.database_id IS 'Database this scheduled job belongs to (nullable for system-level schedules without tenant context)'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.actor_id IS 'User who created this scheduled job, read from JWT claims at creation time'; COMMENT ON COLUMN app_jobs.scheduled_jobs.queue_name IS 'Name of the queue spawned jobs are placed into'; COMMENT ON COLUMN app_jobs.scheduled_jobs.task_identifier IS 'Task type identifier for spawned jobs'; COMMENT ON COLUMN app_jobs.scheduled_jobs.payload IS 'JSON payload passed to each spawned job'; diff --git a/packages/database-jobs/deploy/schemas/app_jobs/triggers/tg_add_job_with_fields.sql b/packages/database-jobs/deploy/schemas/app_jobs/triggers/tg_add_job_with_fields.sql index a83a8b833..314ad2ba0 100644 --- a/packages/database-jobs/deploy/schemas/app_jobs/triggers/tg_add_job_with_fields.sql +++ b/packages/database-jobs/deploy/schemas/app_jobs/triggers/tg_add_job_with_fields.sql @@ -34,7 +34,7 @@ BEGIN END IF; END LOOP; PERFORM - app_jobs.add_job (jwt_private.current_database_id(), fn, app_jobs.json_build_object_apply (args)); + app_jobs.add_job (fn, app_jobs.json_build_object_apply (args)); IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN RETURN NEW; END IF; diff --git a/packages/database-jobs/deploy/schemas/app_jobs/triggers/tg_add_job_with_row.sql b/packages/database-jobs/deploy/schemas/app_jobs/triggers/tg_add_job_with_row.sql index bb619156b..6dec82ffe 100644 --- a/packages/database-jobs/deploy/schemas/app_jobs/triggers/tg_add_job_with_row.sql +++ b/packages/database-jobs/deploy/schemas/app_jobs/triggers/tg_add_job_with_row.sql @@ -8,12 +8,12 @@ CREATE FUNCTION app_jobs.tg_add_job_with_row () BEGIN IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN PERFORM - app_jobs.add_job (jwt_private.current_database_id(), TG_ARGV[0], to_json(NEW)); + app_jobs.add_job (TG_ARGV[0], to_json(NEW)); RETURN NEW; END IF; IF (TG_OP = 'DELETE') THEN PERFORM - app_jobs.add_job (jwt_private.current_database_id(), TG_ARGV[0], to_json(OLD)); + app_jobs.add_job (TG_ARGV[0], to_json(OLD)); RETURN OLD; END IF; END; diff --git a/packages/database-jobs/deploy/schemas/app_jobs/triggers/tg_add_job_with_row_id.sql b/packages/database-jobs/deploy/schemas/app_jobs/triggers/tg_add_job_with_row_id.sql index faf7c78d3..64ad8c30e 100644 --- a/packages/database-jobs/deploy/schemas/app_jobs/triggers/tg_add_job_with_row_id.sql +++ b/packages/database-jobs/deploy/schemas/app_jobs/triggers/tg_add_job_with_row_id.sql @@ -9,12 +9,12 @@ CREATE FUNCTION app_jobs.tg_add_job_with_row_id () BEGIN IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN PERFORM - app_jobs.add_job (jwt_private.current_database_id(), tg_argv[0], json_build_object('id', NEW.id)); + app_jobs.add_job (tg_argv[0], json_build_object('id', NEW.id)); RETURN NEW; END IF; IF (TG_OP = 'DELETE') THEN PERFORM - app_jobs.add_job (jwt_private.current_database_id(), tg_argv[0], json_build_object('id', OLD.id)); + app_jobs.add_job (tg_argv[0], json_build_object('id', OLD.id)); RETURN OLD; END IF; END; diff --git a/packages/database-jobs/package.json b/packages/database-jobs/package.json index b4ccc3fe6..684623c4a 100644 --- a/packages/database-jobs/package.json +++ b/packages/database-jobs/package.json @@ -1,6 +1,6 @@ { "name": "@pgpm/database-jobs", - "version": "0.21.0", + "version": "0.22.0", "description": "Database-specific job handling and queue management", "author": "Dan Lynch ", "contributors": [ @@ -24,6 +24,7 @@ "pgpm": "^4.16.6" }, "dependencies": { + "@pgpm/jwt-claims": "workspace:*", "@pgpm/verify": "workspace:*" }, "repository": { diff --git a/packages/database-jobs/pgpm-database-jobs.control b/packages/database-jobs/pgpm-database-jobs.control index f9e37ea50..3ccad9d76 100644 --- a/packages/database-jobs/pgpm-database-jobs.control +++ b/packages/database-jobs/pgpm-database-jobs.control @@ -1,8 +1,8 @@ # pgpm-database-jobs extension comment = 'pgpm-database-jobs extension' -default_version = '0.15.5' +default_version = '0.22.0' module_pathname = '$libdir/pgpm-database-jobs' -requires = 'plpgsql,pgcrypto,pgpm-verify' +requires = 'plpgsql,pgcrypto,pgpm-verify,pgpm-jwt-claims' relocatable = false superuser = false diff --git a/packages/database-jobs/pgpm.plan b/packages/database-jobs/pgpm.plan index 838f3bd2f..567ada102 100644 --- a/packages/database-jobs/pgpm.plan +++ b/packages/database-jobs/pgpm.plan @@ -34,5 +34,7 @@ schemas/app_jobs/procedures/get_job [schemas/app_jobs/schema schemas/app_jobs/ta schemas/app_jobs/procedures/fail_job [schemas/app_jobs/schema schemas/app_jobs/tables/jobs/table schemas/app_jobs/tables/job_queues/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/procedures/fail_job schemas/app_jobs/procedures/complete_jobs [schemas/app_jobs/schema schemas/app_jobs/tables/job_queues/table schemas/app_jobs/tables/jobs/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/procedures/complete_jobs schemas/app_jobs/procedures/complete_job [schemas/app_jobs/schema schemas/app_jobs/tables/jobs/table schemas/app_jobs/tables/job_queues/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/procedures/complete_job -schemas/app_jobs/procedures/add_scheduled_job [schemas/app_jobs/schema schemas/app_jobs/tables/scheduled_jobs/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/procedures/add_scheduled_job -schemas/app_jobs/procedures/add_job [schemas/app_jobs/schema schemas/app_jobs/tables/jobs/table schemas/app_jobs/tables/job_queues/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/procedures/add_job +schemas/app_jobs/procedures/add_scheduled_job [schemas/app_jobs/schema schemas/app_jobs/tables/scheduled_jobs/table pgpm-jwt-claims:schemas/jwt_private/procedures/current_database_id] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/procedures/add_scheduled_job +schemas/app_jobs/procedures/add_job [schemas/app_jobs/schema schemas/app_jobs/tables/jobs/table schemas/app_jobs/tables/job_queues/table pgpm-jwt-claims:schemas/jwt_private/procedures/current_database_id pgpm-jwt-claims:schemas/jwt_public/procedures/current_user_id] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/procedures/add_job +schemas/app_jobs/procedures/remove_job [schemas/app_jobs/schema schemas/app_jobs/tables/jobs/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/procedures/remove_job +schemas/app_jobs/procedures/force_unlock_workers [schemas/app_jobs/schema schemas/app_jobs/tables/jobs/table schemas/app_jobs/tables/job_queues/table] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/procedures/force_unlock_workers diff --git a/packages/database-jobs/revert/schemas/app_jobs/procedures/force_unlock_workers.sql b/packages/database-jobs/revert/schemas/app_jobs/procedures/force_unlock_workers.sql new file mode 100644 index 000000000..aac5d270a --- /dev/null +++ b/packages/database-jobs/revert/schemas/app_jobs/procedures/force_unlock_workers.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/procedures/force_unlock_workers from pg + +BEGIN; + +DROP FUNCTION app_jobs.force_unlock_workers; + +COMMIT; diff --git a/packages/database-jobs/revert/schemas/app_jobs/procedures/remove_job.sql b/packages/database-jobs/revert/schemas/app_jobs/procedures/remove_job.sql new file mode 100644 index 000000000..e673bdee4 --- /dev/null +++ b/packages/database-jobs/revert/schemas/app_jobs/procedures/remove_job.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_jobs/procedures/remove_job from pg + +BEGIN; + +DROP FUNCTION app_jobs.remove_job; + +COMMIT; diff --git a/packages/database-jobs/revert/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql b/packages/database-jobs/revert/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql index 24fa09ac6..2268c2a07 100644 --- a/packages/database-jobs/revert/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql +++ b/packages/database-jobs/revert/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql @@ -2,6 +2,8 @@ BEGIN; -DROP INDEX app_jobs.priority_run_at_id_idx; +DROP INDEX IF EXISTS app_jobs.priority_run_at_id_idx; +DROP INDEX IF EXISTS app_jobs.jobs_main_index; +DROP INDEX IF EXISTS app_jobs.jobs_no_queue_index; COMMIT; diff --git a/packages/database-jobs/revert/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql b/packages/database-jobs/revert/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql index 612a02874..37a5f531e 100644 --- a/packages/database-jobs/revert/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql +++ b/packages/database-jobs/revert/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql @@ -1,5 +1,6 @@ -- Revert schemas/app_jobs/tables/jobs/triggers/notify_worker from pg BEGIN; -DROP TRIGGER _900_notify_worker ON app_jobs.jobs; +DROP TRIGGER IF EXISTS _900_notify_worker ON app_jobs.jobs; +DROP TRIGGER IF EXISTS _900_after_insert ON app_jobs.jobs; +DROP FUNCTION IF EXISTS app_jobs.tg_jobs__after_insert; COMMIT; - diff --git a/packages/database-jobs/verify/schemas/app_jobs/procedures/force_unlock_workers.sql b/packages/database-jobs/verify/schemas/app_jobs/procedures/force_unlock_workers.sql new file mode 100644 index 000000000..a71b0bbd6 --- /dev/null +++ b/packages/database-jobs/verify/schemas/app_jobs/procedures/force_unlock_workers.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/procedures/force_unlock_workers on pg + +BEGIN; + +SELECT verify_function ('app_jobs.force_unlock_workers'); + +ROLLBACK; diff --git a/packages/database-jobs/verify/schemas/app_jobs/procedures/remove_job.sql b/packages/database-jobs/verify/schemas/app_jobs/procedures/remove_job.sql new file mode 100644 index 000000000..b855f4090 --- /dev/null +++ b/packages/database-jobs/verify/schemas/app_jobs/procedures/remove_job.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_jobs/procedures/remove_job on pg + +BEGIN; + +SELECT verify_function ('app_jobs.remove_job'); + +ROLLBACK; diff --git a/packages/database-jobs/verify/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql b/packages/database-jobs/verify/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql index 2fc8b4ccd..eeec4f53d 100644 --- a/packages/database-jobs/verify/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql +++ b/packages/database-jobs/verify/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql @@ -2,6 +2,7 @@ BEGIN; -SELECT verify_index ('app_jobs.jobs', 'priority_run_at_id_idx'); +SELECT verify_index ('app_jobs.jobs', 'jobs_main_index'); +SELECT verify_index ('app_jobs.jobs', 'jobs_no_queue_index'); ROLLBACK; diff --git a/packages/database-jobs/verify/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql b/packages/database-jobs/verify/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql index dc0436efa..96a054a02 100644 --- a/packages/database-jobs/verify/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql +++ b/packages/database-jobs/verify/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql @@ -1,6 +1,5 @@ -- Verify schemas/app_jobs/tables/jobs/triggers/notify_worker on pg BEGIN; SELECT - verify_trigger ('app_jobs._900_notify_worker'); + verify_trigger ('app_jobs._900_after_insert'); ROLLBACK; - diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 182853db8..de16237ad 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -87,6 +87,19 @@ importers: version: 4.16.6(@dataplan/json@1.0.0(grafast@1.0.0(graphql@16.13.0)))(@dataplan/pg@1.0.0(@dataplan/json@1.0.0(grafast@1.0.0(graphql@16.13.0)))(grafast@1.0.0(graphql@16.13.0))(graphile-config@1.0.0)(graphql@16.13.0)(pg-sql2@5.0.0)(pg@8.20.0))(@types/node@22.19.17)(grafserv@1.0.0(@types/node@22.19.17)(grafast@1.0.0(graphql@16.13.0))(graphile-config@1.0.0)(graphql@16.13.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5))(ws@8.20.0))(graphile-build@5.0.0(grafast@1.0.0(graphql@16.13.0))(graphile-config@1.0.0)(graphql@16.13.0))(pg-sql2@5.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tamedevil@0.1.0)(use-sync-external-store@1.6.0(react@19.2.5))(ws@8.20.0) packages/database-jobs: + dependencies: + '@pgpm/jwt-claims': + specifier: workspace:* + version: link:../jwt-claims + '@pgpm/verify': + specifier: workspace:* + version: link:../verify + devDependencies: + pgpm: + specifier: ^4.16.6 + version: 4.16.6(@dataplan/json@1.0.0(grafast@1.0.0(graphql@16.13.0)))(@dataplan/pg@1.0.0(@dataplan/json@1.0.0(grafast@1.0.0(graphql@16.13.0)))(grafast@1.0.0(graphql@16.13.0))(graphile-config@1.0.0)(graphql@16.13.0)(pg-sql2@5.0.0)(pg@8.20.0))(@types/node@22.19.17)(grafserv@1.0.0(@types/node@22.19.17)(grafast@1.0.0(graphql@16.13.0))(graphile-config@1.0.0)(graphql@16.13.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5))(ws@8.20.0))(graphile-build@5.0.0(grafast@1.0.0(graphql@16.13.0))(graphile-config@1.0.0)(graphql@16.13.0))(pg-sql2@5.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tamedevil@0.1.0)(use-sync-external-store@1.6.0(react@19.2.5))(ws@8.20.0) + + packages/database-jobs-v1: dependencies: '@pgpm/verify': specifier: workspace:* @@ -1106,28 +1119,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@nx/nx-linux-arm64-musl@20.8.3': resolution: {integrity: sha512-LTTGzI8YVPlF1v0YlVf+exM+1q7rpsiUbjTTHJcfHFRU5t4BsiZD54K19Y1UBg1XFx5cwhEaIomSmJ88RwPPVQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@nx/nx-linux-x64-gnu@20.8.3': resolution: {integrity: sha512-SlA4GtXvQbSzSIWLgiIiLBOjdINPOUR/im+TUbaEMZ8wiGrOY8cnk0PVt95TIQJVBeXBCeb5HnoY0lHJpMOODg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@nx/nx-linux-x64-musl@20.8.3': resolution: {integrity: sha512-MNzkEwPktp5SQH9dJDH2wP9hgG9LsBDhKJXJfKw6sUI/6qz5+/aAjFziKy+zBnhU4AO1yXt5qEWzR8lDcIriVQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@nx/nx-win32-arm64-msvc@20.8.3': resolution: {integrity: sha512-qUV7CyXKwRCM/lkvyS6Xa1MqgAuK5da6w27RAehh7LATBUKn1I4/M7DGn6L7ERCxpZuh1TrDz9pUzEy0R+Ekkg==} @@ -2047,49 +2056,41 @@ packages: resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} cpu: [arm64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.11.1': resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} cpu: [arm64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} cpu: [riscv64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} cpu: [riscv64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} cpu: [s390x] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.11.1': resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} cpu: [x64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.11.1': resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} cpu: [x64] os: [linux] - libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.11.1': resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} @@ -6888,7 +6889,7 @@ snapshots: read-cmd-shim: 4.0.0 resolve-from: 5.0.0 rimraf: 4.4.1 - semver: 7.7.3 + semver: 7.7.4 set-blocking: 2.0.0 signal-exit: 3.0.7 slash: 3.0.0 @@ -6990,7 +6991,7 @@ snapshots: promise-all-reject-late: 1.0.1 promise-call-limit: 3.0.2 read-package-json-fast: 3.0.2 - semver: 7.7.3 + semver: 7.7.4 ssri: 10.0.6 treeverse: 3.0.0 walk-up-path: 3.0.1 @@ -7000,7 +7001,7 @@ snapshots: '@npmcli/fs@3.1.1': dependencies: - semver: 7.7.3 + semver: 7.7.4 '@npmcli/git@5.0.8': dependencies: @@ -7011,7 +7012,7 @@ snapshots: proc-log: 4.2.0 promise-inflight: 1.0.1 promise-retry: 2.0.1 - semver: 7.7.3 + semver: 7.7.4 which: 4.0.0 transitivePeerDependencies: - bluebird @@ -7025,7 +7026,7 @@ snapshots: dependencies: '@npmcli/name-from-folder': 2.0.0 glob: 10.5.0 - minimatch: 9.0.5 + minimatch: 9.0.9 read-package-json-fast: 3.0.2 '@npmcli/metavuln-calculator@7.1.1': @@ -7034,7 +7035,7 @@ snapshots: json-parse-even-better-errors: 3.0.2 pacote: 18.0.6 proc-log: 4.2.0 - semver: 7.7.3 + semver: 7.7.4 transitivePeerDependencies: - bluebird - supports-color @@ -7051,7 +7052,7 @@ snapshots: json-parse-even-better-errors: 3.0.2 normalize-package-data: 6.0.2 proc-log: 4.2.0 - semver: 7.7.3 + semver: 7.7.4 transitivePeerDependencies: - bluebird @@ -7084,7 +7085,7 @@ snapshots: ignore: 5.3.2 minimatch: 9.0.3 nx: 20.8.3 - semver: 7.7.3 + semver: 7.7.4 tmp: 0.2.5 tslib: 2.8.1 yargs-parser: 21.1.1 @@ -8137,7 +8138,7 @@ snapshots: '@typescript-eslint/visitor-keys': 8.50.1 debug: 4.4.3 minimatch: 9.0.5 - semver: 7.7.3 + semver: 7.7.4 tinyglobby: 0.2.15 ts-api-utils: 2.1.0(typescript@5.9.3) typescript: 5.9.3 @@ -8658,7 +8659,7 @@ snapshots: handlebars: 4.7.8 json-stringify-safe: 5.0.1 meow: 8.1.2 - semver: 7.7.3 + semver: 7.7.4 split: 1.0.1 conventional-commits-filter@3.0.0: @@ -8700,7 +8701,7 @@ snapshots: dependencies: env-paths: 2.2.1 import-fresh: 3.3.1 - js-yaml: 4.1.0 + js-yaml: 4.1.1 parse-json: 5.2.0 optionalDependencies: typescript: 5.9.3 @@ -9167,7 +9168,7 @@ snapshots: fs-minipass@3.0.3: dependencies: - minipass: 7.1.2 + minipass: 7.1.3 fs.realpath@1.0.0: {} @@ -9238,7 +9239,7 @@ snapshots: git-semver-tags@5.0.1: dependencies: meow: 8.1.2 - semver: 7.7.3 + semver: 7.7.4 git-up@7.0.0: dependencies: @@ -9737,7 +9738,7 @@ snapshots: npm-package-arg: 11.0.2 promzard: 1.0.2 read: 3.0.1 - semver: 7.7.3 + semver: 7.7.4 validate-npm-package-license: 3.0.4 validate-npm-package-name: 5.0.1 transitivePeerDependencies: @@ -10407,7 +10408,7 @@ snapshots: npm-package-arg: 11.0.2 npm-registry-fetch: 17.1.0 proc-log: 4.2.0 - semver: 7.7.3 + semver: 7.7.4 sigstore: 2.3.1 ssri: 10.0.6 transitivePeerDependencies: @@ -10500,7 +10501,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.7.3 + semver: 7.7.4 make-error@1.3.6: {} @@ -10510,7 +10511,7 @@ snapshots: cacache: 18.0.4 http-cache-semantics: 4.2.0 is-lambda: 1.0.1 - minipass: 7.1.2 + minipass: 7.1.3 minipass-fetch: 3.0.5 minipass-flush: 1.0.5 minipass-pipeline: 1.2.4 @@ -10637,11 +10638,11 @@ snapshots: minipass-collect@2.0.1: dependencies: - minipass: 7.1.2 + minipass: 7.1.3 minipass-fetch@3.0.5: dependencies: - minipass: 7.1.2 + minipass: 7.1.3 minipass-sized: 1.0.3 minizlib: 2.1.2 optionalDependencies: @@ -10737,7 +10738,7 @@ snapshots: make-fetch-happen: 13.0.1 nopt: 7.2.1 proc-log: 4.2.0 - semver: 7.7.3 + semver: 7.7.4 tar: 6.2.1 which: 4.0.0 transitivePeerDependencies: @@ -10764,13 +10765,13 @@ snapshots: dependencies: hosted-git-info: 4.1.0 is-core-module: 2.16.1 - semver: 7.7.3 + semver: 7.7.4 validate-npm-package-license: 3.0.4 normalize-package-data@6.0.2: dependencies: hosted-git-info: 7.0.2 - semver: 7.7.3 + semver: 7.7.4 validate-npm-package-license: 3.0.4 normalize-path@3.0.0: {} @@ -10781,7 +10782,7 @@ snapshots: npm-install-checks@6.3.0: dependencies: - semver: 7.7.3 + semver: 7.7.4 npm-normalize-package-bin@3.0.1: {} @@ -10789,7 +10790,7 @@ snapshots: dependencies: hosted-git-info: 7.0.2 proc-log: 4.2.0 - semver: 7.7.3 + semver: 7.7.4 validate-npm-package-name: 5.0.1 npm-packlist@8.0.2: @@ -10801,7 +10802,7 @@ snapshots: npm-install-checks: 6.3.0 npm-normalize-package-bin: 3.0.1 npm-package-arg: 11.0.2 - semver: 7.7.3 + semver: 7.7.4 npm-registry-fetch@17.1.0: dependencies: @@ -10849,7 +10850,7 @@ snapshots: open: 8.4.2 ora: 5.3.0 resolve.exports: 2.0.3 - semver: 7.7.3 + semver: 7.7.4 string-width: 4.2.3 tar-stream: 2.2.0 tmp: 0.2.5 @@ -10906,7 +10907,7 @@ snapshots: ora@5.3.0: dependencies: bl: 4.1.0 - chalk: 4.1.0 + chalk: 4.1.2 cli-cursor: 3.1.0 cli-spinners: 2.6.1 is-interactive: 1.0.0