mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat(tools): 2.0 phase 5 wave 2 - pdf depth (21 tools) (#220)
This commit is contained in:
@@ -1,54 +1,67 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "c7909605-aabf-4832-8ef8-9390c0f7c99a",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"id": "b83e1f2a-9c4d-4e7b-a1f3-8d2c6b5a4e90",
|
||||
"prevId": "47a9d637-64e3-4cca-a916-cf42ea63b335",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"api_keys": {
|
||||
"public.api_keys": {
|
||||
"name": "api_keys",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
"notNull": true
|
||||
},
|
||||
"key_hash": {
|
||||
"name": "key_hash",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
"notNull": true
|
||||
},
|
||||
"key_prefix": {
|
||||
"name": "key_prefix",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'Default API Key'"
|
||||
},
|
||||
"permissions": {
|
||||
"name": "permissions",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
"notNull": true
|
||||
},
|
||||
"last_used_at": {
|
||||
"name": "last_used_at",
|
||||
"type": "integer",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
"notNull": false
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
@@ -65,120 +78,423 @@
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"jobs": {
|
||||
"name": "jobs",
|
||||
"public.audit_log": {
|
||||
"name": "audit_log",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
"notNull": true
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"actor_id": {
|
||||
"name": "actor_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
"notNull": false
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"actor_username": {
|
||||
"name": "actor_username",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'queued'"
|
||||
"notNull": true
|
||||
},
|
||||
"progress": {
|
||||
"name": "progress",
|
||||
"type": "real",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
},
|
||||
"input_files": {
|
||||
"name": "input_files",
|
||||
"action": {
|
||||
"name": "action",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
"notNull": true
|
||||
},
|
||||
"output_path": {
|
||||
"name": "output_path",
|
||||
"target_type": {
|
||||
"name": "target_type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
"notNull": false
|
||||
},
|
||||
"settings": {
|
||||
"name": "settings",
|
||||
"target_id": {
|
||||
"name": "target_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
"notNull": false
|
||||
},
|
||||
"error": {
|
||||
"name": "error",
|
||||
"details": {
|
||||
"name": "details",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"ip_address": {
|
||||
"name": "ip_address",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"completed_at": {
|
||||
"name": "completed_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"foreignKeys": {
|
||||
"audit_log_actor_id_users_id_fk": {
|
||||
"name": "audit_log_actor_id_users_id_fk",
|
||||
"tableFrom": "audit_log",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": ["actor_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "set null",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"sessions": {
|
||||
"name": "sessions",
|
||||
"public.jobs": {
|
||||
"name": "jobs",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
"notNull": false
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"tool_id": {
|
||||
"name": "tool_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"pool": {
|
||||
"name": "pool",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "job_status",
|
||||
"typeSchema": "public",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'queued'"
|
||||
},
|
||||
"attempts": {
|
||||
"name": "attempts",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
"default": 0
|
||||
},
|
||||
"progress": {
|
||||
"name": "progress",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"input_refs": {
|
||||
"name": "input_refs",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"output_refs": {
|
||||
"name": "output_refs",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"settings": {
|
||||
"name": "settings",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"error": {
|
||||
"name": "error",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"bytes_in": {
|
||||
"name": "bytes_in",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"bytes_out": {
|
||||
"name": "bytes_out",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"duration_ms": {
|
||||
"name": "duration_ms",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"started_at": {
|
||||
"name": "started_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"completed_at": {
|
||||
"name": "completed_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"jobs_created_at_idx": {
|
||||
"name": "jobs_created_at_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "created_at",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"jobs_status_idx": {
|
||||
"name": "jobs_status_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "status",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"jobs_user_id_users_id_fk": {
|
||||
"name": "jobs_user_id_users_id_fk",
|
||||
"tableFrom": "jobs",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "set null",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.pipelines": {
|
||||
"name": "pipelines",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"steps": {
|
||||
"name": "steps",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"pipelines_user_id_users_id_fk": {
|
||||
"name": "pipelines_user_id_users_id_fk",
|
||||
"tableFrom": "pipelines",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.roles": {
|
||||
"name": "roles",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
"default": "''"
|
||||
},
|
||||
"permissions": {
|
||||
"name": "permissions",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"is_builtin": {
|
||||
"name": "is_builtin",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"created_by": {
|
||||
"name": "created_by",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"roles_created_by_users_id_fk": {
|
||||
"name": "roles_created_by_users_id_fk",
|
||||
"tableFrom": "roles",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": ["created_by"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "set null",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"roles_name_unique": {
|
||||
"name": "roles_name_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": ["name"]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.sessions": {
|
||||
"name": "sessions",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"id_token": {
|
||||
"name": "id_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
@@ -195,115 +511,297 @@
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"settings": {
|
||||
"public.settings": {
|
||||
"name": "settings",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"key": {
|
||||
"name": "key",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
"notNull": true
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"users": {
|
||||
"name": "users",
|
||||
"public.teams": {
|
||||
"name": "teams",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"teams_name_unique": {
|
||||
"name": "teams_name_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": ["name"]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.user_files": {
|
||||
"name": "user_files",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"original_name": {
|
||||
"name": "original_name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"stored_name": {
|
||||
"name": "stored_name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"mime_type": {
|
||||
"name": "mime_type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"size": {
|
||||
"name": "size",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"width": {
|
||||
"name": "width",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"height": {
|
||||
"name": "height",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"version": {
|
||||
"name": "version",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
"default": 1
|
||||
},
|
||||
"parent_id": {
|
||||
"name": "parent_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"tool_chain": {
|
||||
"name": "tool_chain",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"user_files_user_id_users_id_fk": {
|
||||
"name": "user_files_user_id_users_id_fk",
|
||||
"tableFrom": "user_files",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.users": {
|
||||
"name": "users",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"username": {
|
||||
"name": "username",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
"notNull": true
|
||||
},
|
||||
"password_hash": {
|
||||
"name": "password_hash",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
"notNull": false
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'user'"
|
||||
},
|
||||
"team": {
|
||||
"name": "team",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'Default'"
|
||||
},
|
||||
"must_change_password": {
|
||||
"name": "must_change_password",
|
||||
"type": "integer",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"auth_provider": {
|
||||
"name": "auth_provider",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'local'"
|
||||
},
|
||||
"external_id": {
|
||||
"name": "external_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"users_username_unique": {
|
||||
"name": "users_username_unique",
|
||||
"columns": ["username"],
|
||||
"isUnique": true
|
||||
"notNull": true
|
||||
},
|
||||
"analytics_enabled": {
|
||||
"name": "analytics_enabled",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"analytics_consent_shown_at": {
|
||||
"name": "analytics_consent_shown_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"analytics_consent_remind_at": {
|
||||
"name": "analytics_consent_remind_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
"uniqueConstraints": {
|
||||
"users_username_unique": {
|
||||
"name": "users_username_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": ["username"]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
"enums": {
|
||||
"public.job_status": {
|
||||
"name": "job_status",
|
||||
"schema": "public",
|
||||
"values": ["queued", "processing", "completed", "failed", "canceled"]
|
||||
}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,67 +1,54 @@
|
||||
{
|
||||
"id": "b83e1f2a-9c4d-4e7b-a1f3-8d2c6b5a4e90",
|
||||
"prevId": "47a9d637-64e3-4cca-a916-cf42ea63b335",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "91a14a95-bbcb-46ef-abe3-6d2f6fbc8458",
|
||||
"prevId": "c7909605-aabf-4832-8ef8-9390c0f7c99a",
|
||||
"tables": {
|
||||
"public.api_keys": {
|
||||
"api_keys": {
|
||||
"name": "api_keys",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"key_hash": {
|
||||
"name": "key_hash",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"key_prefix": {
|
||||
"name": "key_prefix",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'Default API Key'"
|
||||
},
|
||||
"permissions": {
|
||||
"name": "permissions",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_used_at": {
|
||||
"name": "last_used_at",
|
||||
"type": "timestamp with time zone",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
@@ -78,423 +65,165 @@
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"public.audit_log": {
|
||||
"name": "audit_log",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"actor_id": {
|
||||
"name": "actor_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"actor_username": {
|
||||
"name": "actor_username",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"action": {
|
||||
"name": "action",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"target_type": {
|
||||
"name": "target_type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"target_id": {
|
||||
"name": "target_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"details": {
|
||||
"name": "details",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"ip_address": {
|
||||
"name": "ip_address",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"audit_log_actor_id_users_id_fk": {
|
||||
"name": "audit_log_actor_id_users_id_fk",
|
||||
"tableFrom": "audit_log",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": ["actor_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "set null",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.jobs": {
|
||||
"jobs": {
|
||||
"name": "jobs",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"tool_id": {
|
||||
"name": "tool_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"pool": {
|
||||
"name": "pool",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "job_status",
|
||||
"typeSchema": "public",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'queued'"
|
||||
},
|
||||
"attempts": {
|
||||
"name": "attempts",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 0
|
||||
},
|
||||
"progress": {
|
||||
"name": "progress",
|
||||
"type": "jsonb",
|
||||
"type": "real",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
},
|
||||
"input_refs": {
|
||||
"name": "input_refs",
|
||||
"type": "jsonb",
|
||||
"input_files": {
|
||||
"name": "input_files",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"output_refs": {
|
||||
"name": "output_refs",
|
||||
"type": "jsonb",
|
||||
"output_path": {
|
||||
"name": "output_path",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"settings": {
|
||||
"name": "settings",
|
||||
"type": "jsonb",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"error": {
|
||||
"name": "error",
|
||||
"type": "jsonb",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"bytes_in": {
|
||||
"name": "bytes_in",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"bytes_out": {
|
||||
"name": "bytes_out",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"duration_ms": {
|
||||
"name": "duration_ms",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"started_at": {
|
||||
"name": "started_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"completed_at": {
|
||||
"name": "completed_at",
|
||||
"type": "timestamp with time zone",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"jobs_created_at_idx": {
|
||||
"name": "jobs_created_at_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "created_at",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"jobs_status_idx": {
|
||||
"name": "jobs_status_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "status",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"jobs_user_id_users_id_fk": {
|
||||
"name": "jobs_user_id_users_id_fk",
|
||||
"tableFrom": "jobs",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "set null",
|
||||
"onUpdate": "no action"
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"public.pipelines": {
|
||||
"pipelines": {
|
||||
"name": "pipelines",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"steps": {
|
||||
"name": "steps",
|
||||
"type": "jsonb",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"pipelines_user_id_users_id_fk": {
|
||||
"name": "pipelines_user_id_users_id_fk",
|
||||
"tableFrom": "pipelines",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"public.roles": {
|
||||
"name": "roles",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "''"
|
||||
},
|
||||
"permissions": {
|
||||
"name": "permissions",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"is_builtin": {
|
||||
"name": "is_builtin",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"created_by": {
|
||||
"name": "created_by",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"roles_created_by_users_id_fk": {
|
||||
"name": "roles_created_by_users_id_fk",
|
||||
"tableFrom": "roles",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": ["created_by"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "set null",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"roles_name_unique": {
|
||||
"name": "roles_name_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": ["name"]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.sessions": {
|
||||
"sessions": {
|
||||
"name": "sessions",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp with time zone",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"id_token": {
|
||||
"name": "id_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
@@ -511,297 +240,115 @@
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"public.settings": {
|
||||
"settings": {
|
||||
"name": "settings",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"key": {
|
||||
"name": "key",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.teams": {
|
||||
"name": "teams",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"teams_name_unique": {
|
||||
"name": "teams_name_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": ["name"]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.user_files": {
|
||||
"name": "user_files",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"original_name": {
|
||||
"name": "original_name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"stored_name": {
|
||||
"name": "stored_name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"mime_type": {
|
||||
"name": "mime_type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"size": {
|
||||
"name": "size",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"width": {
|
||||
"name": "width",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"height": {
|
||||
"name": "height",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"version": {
|
||||
"name": "version",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 1
|
||||
},
|
||||
"parent_id": {
|
||||
"name": "parent_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"tool_chain": {
|
||||
"name": "tool_chain",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"user_files_user_id_users_id_fk": {
|
||||
"name": "user_files_user_id_users_id_fk",
|
||||
"tableFrom": "user_files",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"public.users": {
|
||||
"users": {
|
||||
"name": "users",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"username": {
|
||||
"name": "username",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"password_hash": {
|
||||
"name": "password_hash",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'user'"
|
||||
},
|
||||
"team": {
|
||||
"name": "team",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'Default'"
|
||||
},
|
||||
"must_change_password": {
|
||||
"name": "must_change_password",
|
||||
"type": "boolean",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"auth_provider": {
|
||||
"name": "auth_provider",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'local'"
|
||||
},
|
||||
"external_id": {
|
||||
"name": "external_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"analytics_enabled": {
|
||||
"name": "analytics_enabled",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"analytics_consent_shown_at": {
|
||||
"name": "analytics_consent_shown_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"analytics_consent_remind_at": {
|
||||
"name": "analytics_consent_remind_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"indexes": {
|
||||
"users_username_unique": {
|
||||
"name": "users_username_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": ["username"]
|
||||
"columns": ["username"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"enums": {
|
||||
"public.job_status": {
|
||||
"name": "job_status",
|
||||
"schema": "public",
|
||||
"values": ["queued", "processing", "completed", "failed", "canceled"]
|
||||
}
|
||||
},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,9 @@ export async function closeFlowProducer(): Promise<void> {
|
||||
export async function enqueueToolJob(data: ToolJobData): Promise<Job<ToolJobData, ToolJobResult>> {
|
||||
// Insert the durable DB row first (crash-safe: row exists even if
|
||||
// Redis add fails and the job is retried on next boot).
|
||||
// When dbSettings is provided, persist the redacted version instead of
|
||||
// the real settings (which may contain secrets like passwords). The
|
||||
// worker reads settings from BullMQ job data, never the DB row.
|
||||
await db.insert(schema.jobs).values({
|
||||
id: data.jobId,
|
||||
userId: data.userId,
|
||||
@@ -70,7 +73,7 @@ export async function enqueueToolJob(data: ToolJobData): Promise<Job<ToolJobData
|
||||
type: data.kind,
|
||||
status: "queued",
|
||||
inputRefs: data.inputRefs,
|
||||
settings: data.settings as Record<string, unknown>,
|
||||
settings: (data.dbSettings ?? data.settings) as Record<string, unknown>,
|
||||
});
|
||||
|
||||
const queue = getQueue(data.pool);
|
||||
|
||||
@@ -25,6 +25,12 @@ export interface ToolJobData {
|
||||
inputRefs: string[];
|
||||
filename: string;
|
||||
settings: unknown;
|
||||
/**
|
||||
* When set, persisted to the DB row instead of settings. Use to strip
|
||||
* secrets the worker needs but should not persist (the worker reads
|
||||
* settings from BullMQ job data, never the DB row).
|
||||
*/
|
||||
dbSettings?: Record<string, unknown>;
|
||||
fileId?: string;
|
||||
clientJobId?: string;
|
||||
kind:
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Pure helper: expand a human-friendly page specification into a Set of
|
||||
* 1-based page numbers. Handles single pages (N), ranges (N-M), reverse
|
||||
* indices (rN = total - N + 1), the "z" alias for the last page, and
|
||||
* comma-separated combinations.
|
||||
*
|
||||
* Throws on out-of-range references or empty specs.
|
||||
*/
|
||||
export function parsePageSpec(spec: string, total: number): Set<number> {
|
||||
if (total < 1) throw new Error("Total page count must be at least 1");
|
||||
|
||||
const result = new Set<number>();
|
||||
const parts = spec
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
if (parts.length === 0) {
|
||||
throw new Error("Empty page specification");
|
||||
}
|
||||
|
||||
for (const part of parts) {
|
||||
if (part.includes("-")) {
|
||||
const [startRaw, endRaw] = part.split("-", 2);
|
||||
const start = resolveToken(startRaw, total);
|
||||
const end = resolveToken(endRaw, total);
|
||||
const lo = Math.min(start, end);
|
||||
const hi = Math.max(start, end);
|
||||
for (let i = lo; i <= hi; i++) {
|
||||
result.add(i);
|
||||
}
|
||||
} else {
|
||||
result.add(resolveToken(part, total));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapse a sorted-ascending array of page numbers into the most compact
|
||||
* qpdf range string by merging consecutive runs into N-M parts.
|
||||
*
|
||||
* Examples:
|
||||
* [2,3,4,...,80] -> "2-80"
|
||||
* [1,3,5,7] -> "1,3,5,7"
|
||||
* [1,2,4,5,6,9] -> "1-2,4-6,9"
|
||||
* [] -> ""
|
||||
* [3] -> "3"
|
||||
*
|
||||
* The input MUST be sorted ascending with no duplicates (as produced by
|
||||
* iterating 1..total and filtering).
|
||||
*/
|
||||
export function compressPageRuns(pages: number[]): string {
|
||||
if (pages.length === 0) return "";
|
||||
|
||||
const parts: string[] = [];
|
||||
let runStart = pages[0];
|
||||
let runEnd = pages[0];
|
||||
|
||||
for (let i = 1; i < pages.length; i++) {
|
||||
if (pages[i] === runEnd + 1) {
|
||||
runEnd = pages[i];
|
||||
} else {
|
||||
parts.push(runStart === runEnd ? `${runStart}` : `${runStart}-${runEnd}`);
|
||||
runStart = pages[i];
|
||||
runEnd = pages[i];
|
||||
}
|
||||
}
|
||||
|
||||
parts.push(runStart === runEnd ? `${runStart}` : `${runStart}-${runEnd}`);
|
||||
return parts.join(",");
|
||||
}
|
||||
|
||||
function resolveToken(token: string, total: number): number {
|
||||
const t = token.trim().toLowerCase();
|
||||
let page: number;
|
||||
|
||||
if (t === "z") {
|
||||
page = total;
|
||||
} else if (t.startsWith("r")) {
|
||||
const n = Number.parseInt(t.slice(1), 10);
|
||||
if (!Number.isFinite(n) || n < 1) {
|
||||
throw new Error(`Invalid reverse page index: ${token}`);
|
||||
}
|
||||
page = total - n + 1;
|
||||
if (page < 1) {
|
||||
throw new Error(
|
||||
`Reverse index ${token} resolves to page ${page}, out of range for ${total} pages`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
page = Number.parseInt(t, 10);
|
||||
if (!Number.isFinite(page) || page < 1) {
|
||||
throw new Error(`Invalid page number: ${token}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (page < 1 || page > total) {
|
||||
throw new Error(`Page ${page} out of range for a ${total}-page document`);
|
||||
}
|
||||
|
||||
return page;
|
||||
}
|
||||
@@ -26,5 +26,17 @@ export interface PreparedInput {
|
||||
* enqueueing; handlers own format logic only.
|
||||
*/
|
||||
export interface InputHandler {
|
||||
prepare(raw: Buffer, filename: string, opts: { scratchDir: string }): Promise<PreparedInput>;
|
||||
prepare(
|
||||
raw: Buffer,
|
||||
filename: string,
|
||||
opts: {
|
||||
scratchDir: string;
|
||||
/**
|
||||
* When true, skip structural validation (qpdfCheck, page caps) but
|
||||
* keep header-magic checks. Used by tools that intentionally accept
|
||||
* damaged inputs (e.g. repair-pdf).
|
||||
*/
|
||||
lenient?: boolean;
|
||||
},
|
||||
): Promise<PreparedInput>;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { mkdir, rm, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { qpdfAvailable, qpdfCheck, qpdfPageCount } from "@snapotter/doc-engine";
|
||||
import {
|
||||
qpdfAvailable,
|
||||
qpdfCheck,
|
||||
qpdfPageCount,
|
||||
qpdfRequiresPassword,
|
||||
} from "@snapotter/doc-engine";
|
||||
import { env } from "../config.js";
|
||||
import { type InputHandler, InputValidationError, type PreparedInput } from "./contract.js";
|
||||
|
||||
@@ -17,7 +22,7 @@ export class DocumentInputHandler implements InputHandler {
|
||||
async prepare(
|
||||
raw: Buffer,
|
||||
filename: string,
|
||||
opts: { scratchDir: string },
|
||||
opts: { scratchDir: string; lenient?: boolean },
|
||||
): Promise<PreparedInput> {
|
||||
if (raw.length === 0) throw new InputValidationError("Empty file");
|
||||
const lower = filename.toLowerCase();
|
||||
@@ -25,26 +30,34 @@ export class DocumentInputHandler implements InputHandler {
|
||||
if (raw.subarray(0, 5).toString() !== "%PDF-") {
|
||||
throw new InputValidationError("File does not start with a PDF header");
|
||||
}
|
||||
if (qpdfAvailable()) {
|
||||
// When lenient, skip qpdfCheck + page-cap (repair-pdf's input is
|
||||
// intentionally damaged). The %PDF- header check above still runs.
|
||||
if (!opts.lenient && qpdfAvailable()) {
|
||||
const dir = join(opts.scratchDir, `qpdf-${randomUUID()}`);
|
||||
await mkdir(dir, { recursive: true });
|
||||
const p = join(dir, "input.pdf");
|
||||
try {
|
||||
await writeFile(p, raw);
|
||||
try {
|
||||
await qpdfCheck(p);
|
||||
} catch (err) {
|
||||
throw new InputValidationError(
|
||||
`Damaged PDF: ${err instanceof Error ? err.message.slice(0, 300) : "structural check failed"}`,
|
||||
);
|
||||
}
|
||||
if (env.MAX_PDF_PAGES > 0) {
|
||||
const pages = await qpdfPageCount(p);
|
||||
if (pages > env.MAX_PDF_PAGES) {
|
||||
if (await qpdfRequiresPassword(p)) {
|
||||
// Password-protected PDFs are structurally unverifiable without the
|
||||
// password and are legitimate inputs (unlock-pdf). Page caps cannot be
|
||||
// read either; the consuming tool enforces its own limits.
|
||||
} else {
|
||||
try {
|
||||
await qpdfCheck(p);
|
||||
} catch (err) {
|
||||
throw new InputValidationError(
|
||||
`PDF has ${pages} pages, exceeding the maximum of ${env.MAX_PDF_PAGES}`,
|
||||
`Damaged PDF: ${err instanceof Error ? err.message.slice(0, 300) : "structural check failed"}`,
|
||||
);
|
||||
}
|
||||
if (env.MAX_PDF_PAGES > 0) {
|
||||
const pages = await qpdfPageCount(p);
|
||||
if (pages > env.MAX_PDF_PAGES) {
|
||||
throw new InputValidationError(
|
||||
`PDF has ${pages} pages, exceeding the maximum of ${env.MAX_PDF_PAGES}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true }).catch(() => {});
|
||||
|
||||
@@ -71,6 +71,13 @@ interface ParsedStep {
|
||||
pool: Pool;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tools whose settings carry passwords. They are blocked from ALL pipeline
|
||||
* paths so secrets never persist in step rows (the single-tool route redacts
|
||||
* via dbSettings).
|
||||
*/
|
||||
const PASSWORD_TOOLS = new Set(["protect-pdf", "unlock-pdf"]);
|
||||
|
||||
/**
|
||||
* Build a FlowJob tree for a single-file pipeline.
|
||||
*
|
||||
@@ -313,6 +320,12 @@ export async function registerPipelineRoutes(app: FastifyInstance): Promise<void
|
||||
});
|
||||
}
|
||||
|
||||
if (PASSWORD_TOOLS.has(step.toolId)) {
|
||||
return reply.status(400).send({
|
||||
error: `Step ${i + 1}: This tool cannot be used in pipelines because it requires a password`,
|
||||
});
|
||||
}
|
||||
|
||||
const settingsResult = toolConfig.settingsSchema.safeParse(step.settings);
|
||||
if (!settingsResult.success) {
|
||||
return reply.status(400).send({
|
||||
@@ -483,6 +496,11 @@ export async function registerPipelineRoutes(app: FastifyInstance): Promise<void
|
||||
|
||||
// Validate all tool IDs exist
|
||||
for (let i = 0; i < steps.length; i++) {
|
||||
if (PASSWORD_TOOLS.has(steps[i].toolId)) {
|
||||
return reply.status(400).send({
|
||||
error: `This tool cannot be used in saved pipelines because it requires a password`,
|
||||
});
|
||||
}
|
||||
const toolConfig = getToolConfig(steps[i].toolId);
|
||||
if (!toolConfig) {
|
||||
return reply.status(400).send({
|
||||
@@ -696,6 +714,12 @@ export async function registerPipelineRoutes(app: FastifyInstance): Promise<void
|
||||
});
|
||||
}
|
||||
|
||||
if (PASSWORD_TOOLS.has(step.toolId)) {
|
||||
return reply.status(400).send({
|
||||
error: `Step ${i + 1}: This tool cannot be used in pipelines because it requires a password`,
|
||||
});
|
||||
}
|
||||
|
||||
const settingsResult = toolConfig.settingsSchema.safeParse(step.settings);
|
||||
if (!settingsResult.success) {
|
||||
return reply.status(400).send({
|
||||
|
||||
@@ -80,6 +80,20 @@ export interface ToolRouteConfig<T> {
|
||||
) => Promise<{ buffer: Buffer; filename: string; contentType: string }>;
|
||||
/** Optional v2 process function. When set, the worker calls this instead of the legacy process. */
|
||||
processV2?: ToolProcessV2;
|
||||
/**
|
||||
* When set, the factory passes `{ scratchDir, lenient: true }` to the
|
||||
* input handler's prepare(). DocumentInputHandler skips qpdfCheck and
|
||||
* page-cap when lenient, keeping only the %PDF- header check. Set on
|
||||
* tools that intentionally accept damaged inputs (e.g. repair-pdf).
|
||||
*/
|
||||
skipStructuralValidation?: boolean;
|
||||
/**
|
||||
* When set, produces a redacted copy of settings for the durable DB
|
||||
* row. Passwords and other secrets are replaced so they do not persist
|
||||
* in the jobs table (retention keeps rows for days). The BullMQ job
|
||||
* data keeps the real settings; the worker reads from job data.
|
||||
*/
|
||||
redactSettingsForAudit?: (settings: unknown) => Record<string, unknown>;
|
||||
}
|
||||
|
||||
/** Type-erased config stored in the registry (settings type is widened to avoid variance issues). */
|
||||
@@ -94,6 +108,8 @@ export interface AnyToolRouteConfig {
|
||||
ctx?: ToolProcessCtx,
|
||||
) => Promise<{ buffer: Buffer; filename: string; contentType: string }>;
|
||||
processV2?: ToolProcessV2;
|
||||
skipStructuralValidation?: boolean;
|
||||
redactSettingsForAudit?: (settings: unknown) => Record<string, unknown>;
|
||||
}
|
||||
|
||||
// ── Legacy adapter ────────────────────────────────────────────
|
||||
@@ -289,6 +305,7 @@ export function createToolRoute<T>(app: FastifyInstance, config: ToolRouteConfig
|
||||
try {
|
||||
const prepared = await inputHandlerFor(modality).prepare(fileBuffer, fname, {
|
||||
scratchDir,
|
||||
lenient: config.skipStructuralValidation,
|
||||
});
|
||||
fileBuffer = prepared.buffer;
|
||||
fname = prepared.filename;
|
||||
@@ -366,6 +383,9 @@ export function createToolRoute<T>(app: FastifyInstance, config: ToolRouteConfig
|
||||
const pool = resolveToolPool(config.toolId);
|
||||
|
||||
// Enqueue for the BullMQ worker
|
||||
const dbSettings = config.redactSettingsForAudit
|
||||
? config.redactSettingsForAudit(settings)
|
||||
: undefined;
|
||||
await enqueueToolJob({
|
||||
jobId,
|
||||
toolId: config.toolId,
|
||||
@@ -374,6 +394,7 @@ export function createToolRoute<T>(app: FastifyInstance, config: ToolRouteConfig
|
||||
inputRefs,
|
||||
filename,
|
||||
settings,
|
||||
dbSettings,
|
||||
fileId: fileId ?? undefined,
|
||||
clientJobId: clientJobId ?? undefined,
|
||||
kind: "tool",
|
||||
@@ -404,6 +425,7 @@ export function createToolRoute<T>(app: FastifyInstance, config: ToolRouteConfig
|
||||
originalSize: result.originalSize,
|
||||
processedSize: result.processedSize,
|
||||
savedFileId: result.savedFileId,
|
||||
...result.resultPayload,
|
||||
});
|
||||
}
|
||||
return reply.status(202).send({ jobId: clientJobId || jobId, async: true });
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import type { BookletValue } from "@snapotter/doc-engine";
|
||||
import { pdfcpuBooklet } from "@snapotter/doc-engine";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
|
||||
const settingsSchema = z.object({
|
||||
perSheet: z.union([z.literal(2), z.literal(4), z.literal(6), z.literal(8)]).default(2),
|
||||
});
|
||||
|
||||
export function registerBookletPdf(app: FastifyInstance) {
|
||||
createToolRoute(app, {
|
||||
toolId: "booklet-pdf",
|
||||
settingsSchema,
|
||||
process: async () => {
|
||||
throw new Error("booklet-pdf is v2-only");
|
||||
},
|
||||
processV2: async (ctx) => {
|
||||
const settings = settingsSchema.parse(ctx.settings);
|
||||
const input = ctx.inputs[0];
|
||||
const base = input.filename.replace(/\.[^.]+$/, "");
|
||||
const inPath = join(ctx.scratchDir, `in-${input.filename.replace(/[^A-Za-z0-9._-]/g, "_")}`);
|
||||
await writeFile(inPath, input.buffer);
|
||||
|
||||
const outPath = join(ctx.scratchDir, `${base}_booklet.pdf`);
|
||||
ctx.report(10, "Creating booklet");
|
||||
await pdfcpuBooklet(inPath, settings.perSheet as BookletValue, outPath);
|
||||
ctx.report(90, "Done");
|
||||
|
||||
return {
|
||||
scratchPath: outPath,
|
||||
filename: `${base}_booklet.pdf`,
|
||||
contentType: "application/pdf",
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { pdfcpuCropMargin } from "@snapotter/doc-engine";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
|
||||
const settingsSchema = z.object({
|
||||
margin: z.number().min(0).max(2000).default(20),
|
||||
});
|
||||
|
||||
export function registerCropPdf(app: FastifyInstance) {
|
||||
createToolRoute(app, {
|
||||
toolId: "crop-pdf",
|
||||
settingsSchema,
|
||||
process: async () => {
|
||||
throw new Error("crop-pdf is v2-only");
|
||||
},
|
||||
processV2: async (ctx) => {
|
||||
const settings = settingsSchema.parse(ctx.settings);
|
||||
const input = ctx.inputs[0];
|
||||
const base = input.filename.replace(/\.[^.]+$/, "");
|
||||
const inPath = join(ctx.scratchDir, `in-${input.filename.replace(/[^A-Za-z0-9._-]/g, "_")}`);
|
||||
await writeFile(inPath, input.buffer);
|
||||
|
||||
const outPath = join(ctx.scratchDir, `${base}_cropped.pdf`);
|
||||
ctx.report(10, "Cropping");
|
||||
await pdfcpuCropMargin(inPath, settings.margin, outPath);
|
||||
ctx.report(90, "Done");
|
||||
|
||||
return {
|
||||
scratchPath: outPath,
|
||||
filename: `${base}_cropped.pdf`,
|
||||
contentType: "application/pdf",
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { qpdfPagesSpec } from "@snapotter/doc-engine";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
|
||||
const rangeField = z
|
||||
.string()
|
||||
.max(200)
|
||||
.regex(/^[0-9rz][0-9rz,-]*$/i, "Invalid page range");
|
||||
|
||||
const settingsSchema = z.object({
|
||||
range: rangeField,
|
||||
});
|
||||
|
||||
export function registerExtractPages(app: FastifyInstance) {
|
||||
createToolRoute(app, {
|
||||
toolId: "extract-pages",
|
||||
settingsSchema,
|
||||
process: async () => {
|
||||
throw new Error("extract-pages is v2-only");
|
||||
},
|
||||
processV2: async (ctx) => {
|
||||
const settings = settingsSchema.parse(ctx.settings);
|
||||
const input = ctx.inputs[0];
|
||||
const base = input.filename.replace(/\.[^.]+$/, "");
|
||||
const inPath = join(ctx.scratchDir, `in-${input.filename.replace(/[^A-Za-z0-9._-]/g, "_")}`);
|
||||
await writeFile(inPath, input.buffer);
|
||||
|
||||
const outPath = join(ctx.scratchDir, `${base}_extracted.pdf`);
|
||||
ctx.report(10, "Extracting pages");
|
||||
await qpdfPagesSpec(inPath, settings.range, outPath);
|
||||
ctx.report(90, "Done");
|
||||
|
||||
return {
|
||||
scratchPath: outPath,
|
||||
filename: `${base}_extracted.pdf`,
|
||||
contentType: "application/pdf",
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { pdfFlattenPy } from "@snapotter/doc-engine";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
|
||||
const settingsSchema = z.object({});
|
||||
|
||||
export function registerFlattenPdf(app: FastifyInstance) {
|
||||
createToolRoute(app, {
|
||||
toolId: "flatten-pdf",
|
||||
settingsSchema,
|
||||
process: async () => {
|
||||
throw new Error("flatten-pdf is v2-only");
|
||||
},
|
||||
processV2: async (ctx) => {
|
||||
const input = ctx.inputs[0];
|
||||
const base = input.filename.replace(/\.[^.]+$/, "");
|
||||
const inPath = join(ctx.scratchDir, `in-${input.filename.replace(/[^A-Za-z0-9._-]/g, "_")}`);
|
||||
await writeFile(inPath, input.buffer);
|
||||
|
||||
const outPath = join(ctx.scratchDir, `${base}_flattened.pdf`);
|
||||
ctx.report(10, "Flattening");
|
||||
await pdfFlattenPy(inPath, outPath);
|
||||
ctx.report(90, "Done");
|
||||
|
||||
return {
|
||||
scratchPath: outPath,
|
||||
filename: `${base}_flattened.pdf`,
|
||||
contentType: "application/pdf",
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { gsGrayscalePdf } from "@snapotter/doc-engine";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
|
||||
const settingsSchema = z.object({});
|
||||
|
||||
export function registerGrayscalePdf(app: FastifyInstance) {
|
||||
createToolRoute(app, {
|
||||
toolId: "grayscale-pdf",
|
||||
settingsSchema,
|
||||
process: async () => {
|
||||
throw new Error("grayscale-pdf is v2-only");
|
||||
},
|
||||
processV2: async (ctx) => {
|
||||
const input = ctx.inputs[0];
|
||||
const base = input.filename.replace(/\.[^.]+$/, "");
|
||||
const inPath = join(ctx.scratchDir, `in-${input.filename.replace(/[^A-Za-z0-9._-]/g, "_")}`);
|
||||
await writeFile(inPath, input.buffer);
|
||||
|
||||
const outPath = join(ctx.scratchDir, `${base}_grayscale.pdf`);
|
||||
ctx.report(10, "Converting to grayscale");
|
||||
await gsGrayscalePdf(inPath, outPath);
|
||||
ctx.report(90, "Done");
|
||||
|
||||
return {
|
||||
scratchPath: outPath,
|
||||
filename: `${base}_grayscale.pdf`,
|
||||
contentType: "application/pdf",
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { htmlToPdfPy } from "@snapotter/doc-engine";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
|
||||
const settingsSchema = z.object({});
|
||||
|
||||
export function registerHtmlToPdf(app: FastifyInstance) {
|
||||
createToolRoute(app, {
|
||||
toolId: "html-to-pdf",
|
||||
settingsSchema,
|
||||
process: async () => {
|
||||
throw new Error("html-to-pdf is v2-only");
|
||||
},
|
||||
processV2: async (ctx) => {
|
||||
const input = ctx.inputs[0];
|
||||
const base = input.filename.replace(/\.[^.]+$/, "");
|
||||
const inPath = join(ctx.scratchDir, `in-${input.filename.replace(/[^A-Za-z0-9._-]/g, "_")}`);
|
||||
await writeFile(inPath, input.buffer);
|
||||
|
||||
const outPath = join(ctx.scratchDir, `${base}.pdf`);
|
||||
ctx.report(10, "Converting");
|
||||
await htmlToPdfPy(inPath, outPath, "html");
|
||||
ctx.report(90, "Done");
|
||||
|
||||
return {
|
||||
scratchPath: outPath,
|
||||
filename: `${base}.pdf`,
|
||||
contentType: "application/pdf",
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { registerAiCanvasExpand } from "./ai-canvas-expand.js";
|
||||
import { registerBarcodeRead } from "./barcode-read.js";
|
||||
import { registerBeautify } from "./beautify.js";
|
||||
import { registerBlurFaces } from "./blur-faces.js";
|
||||
import { registerBookletPdf } from "./booklet-pdf.js";
|
||||
import { registerBorder } from "./border.js";
|
||||
import { registerBulkRename } from "./bulk-rename.js";
|
||||
import { registerCollage } from "./collage.js";
|
||||
@@ -23,32 +24,50 @@ import { registerConvert } from "./convert.js";
|
||||
import { registerConvertAudio } from "./convert-audio.js";
|
||||
import { registerConvertVideo } from "./convert-video.js";
|
||||
import { registerCrop } from "./crop.js";
|
||||
import { registerCropPdf } from "./crop-pdf.js";
|
||||
import { registerCsvExcel } from "./csv-excel.js";
|
||||
import { registerCsvJson } from "./csv-json.js";
|
||||
import { registerEditMetadata } from "./edit-metadata.js";
|
||||
import { registerEnhanceFaces } from "./enhance-faces.js";
|
||||
import { registerEraseObject } from "./erase-object.js";
|
||||
import { registerExtractAudio } from "./extract-audio.js";
|
||||
import { registerExtractPages } from "./extract-pages.js";
|
||||
import { registerFavicon } from "./favicon.js";
|
||||
import { registerFindDuplicates } from "./find-duplicates.js";
|
||||
import { registerFlattenPdf } from "./flatten-pdf.js";
|
||||
import { registerGifTools } from "./gif-tools.js";
|
||||
import { registerGrayscalePdf } from "./grayscale-pdf.js";
|
||||
import { registerHtmlToImage } from "./html-to-image.js";
|
||||
import { registerHtmlToPdf } from "./html-to-pdf.js";
|
||||
import { registerImageEnhancement } from "./image-enhancement.js";
|
||||
import { registerImageToBase64 } from "./image-to-base64.js";
|
||||
import { registerImageToPdf } from "./image-to-pdf.js";
|
||||
import { registerInfo } from "./info.js";
|
||||
import { registerJsonXml } from "./json-xml.js";
|
||||
import { registerLinearizePdf } from "./linearize-pdf.js";
|
||||
import { registerMarkdownToPdf } from "./markdown-to-pdf.js";
|
||||
import { registerMemeGenerator } from "./meme-generator.js";
|
||||
import { registerMergePdf } from "./merge-pdf.js";
|
||||
import { registerMuteVideo } from "./mute-video.js";
|
||||
import { registerNoiseRemoval } from "./noise-removal.js";
|
||||
import { registerNupPdf } from "./nup-pdf.js";
|
||||
import { registerOcr } from "./ocr.js";
|
||||
import { registerOptimizeForWeb } from "./optimize-for-web.js";
|
||||
import { registerOrganizePdf } from "./organize-pdf.js";
|
||||
import { registerPassportPhoto } from "./passport-photo.js";
|
||||
import { registerPdfMetadata } from "./pdf-metadata.js";
|
||||
import { registerPdfPageNumbers } from "./pdf-page-numbers.js";
|
||||
import { registerPdfToImage } from "./pdf-to-image.js";
|
||||
import { registerPdfToText } from "./pdf-to-text.js";
|
||||
import { registerPdfToWord } from "./pdf-to-word.js";
|
||||
import { registerPdfaConvert } from "./pdfa-convert.js";
|
||||
import { registerProtectPdf } from "./protect-pdf.js";
|
||||
import { registerQrGenerate } from "./qr-generate.js";
|
||||
import { registerRedEyeRemoval } from "./red-eye-removal.js";
|
||||
import { registerRedactPdf } from "./redact-pdf.js";
|
||||
import { registerRemoveBackground } from "./remove-background.js";
|
||||
import { registerRemovePages } from "./remove-pages.js";
|
||||
import { registerRepairPdf } from "./repair-pdf.js";
|
||||
import { registerReplaceColor } from "./replace-color.js";
|
||||
import { registerResize } from "./resize.js";
|
||||
import { registerRestorePhoto } from "./restore-photo.js";
|
||||
@@ -66,10 +85,12 @@ import { registerTextOverlay } from "./text-overlay.js";
|
||||
import { registerTransparencyFixer } from "./transparency-fixer.js";
|
||||
import { registerTrimAudio } from "./trim-audio.js";
|
||||
import { registerTrimVideo } from "./trim-video.js";
|
||||
import { registerUnlockPdf } from "./unlock-pdf.js";
|
||||
import { registerUpscale } from "./upscale.js";
|
||||
import { registerVectorize } from "./vectorize.js";
|
||||
import { registerVideoToGif } from "./video-to-gif.js";
|
||||
import { registerWatermarkImage } from "./watermark-image.js";
|
||||
import { registerWatermarkPdf } from "./watermark-pdf.js";
|
||||
import { registerWatermarkText } from "./watermark-text.js";
|
||||
import { registerWordToPdf } from "./word-to-pdf.js";
|
||||
|
||||
@@ -174,6 +195,27 @@ export async function registerToolRoutes(app: FastifyInstance): Promise<void> {
|
||||
{ id: "compress-pdf", register: registerCompressPdf },
|
||||
{ id: "rotate-pdf", register: registerRotatePdf },
|
||||
{ id: "word-to-pdf", register: registerWordToPdf },
|
||||
{ id: "extract-pages", register: registerExtractPages },
|
||||
{ id: "remove-pages", register: registerRemovePages },
|
||||
{ id: "organize-pdf", register: registerOrganizePdf },
|
||||
{ id: "protect-pdf", register: registerProtectPdf },
|
||||
{ id: "unlock-pdf", register: registerUnlockPdf },
|
||||
{ id: "repair-pdf", register: registerRepairPdf },
|
||||
{ id: "linearize-pdf", register: registerLinearizePdf },
|
||||
{ id: "grayscale-pdf", register: registerGrayscalePdf },
|
||||
{ id: "pdfa-convert", register: registerPdfaConvert },
|
||||
{ id: "crop-pdf", register: registerCropPdf },
|
||||
{ id: "nup-pdf", register: registerNupPdf },
|
||||
{ id: "booklet-pdf", register: registerBookletPdf },
|
||||
{ id: "watermark-pdf", register: registerWatermarkPdf },
|
||||
{ id: "pdf-page-numbers", register: registerPdfPageNumbers },
|
||||
{ id: "flatten-pdf", register: registerFlattenPdf },
|
||||
{ id: "redact-pdf", register: registerRedactPdf },
|
||||
{ id: "pdf-to-text", register: registerPdfToText },
|
||||
{ id: "pdf-to-word", register: registerPdfToWord },
|
||||
{ id: "pdf-metadata", register: registerPdfMetadata },
|
||||
{ id: "html-to-pdf", register: registerHtmlToPdf },
|
||||
{ id: "markdown-to-pdf", register: registerMarkdownToPdf },
|
||||
|
||||
// Data Files
|
||||
{ id: "csv-excel", register: registerCsvExcel },
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { qpdfLinearize } from "@snapotter/doc-engine";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
|
||||
const settingsSchema = z.object({});
|
||||
|
||||
export function registerLinearizePdf(app: FastifyInstance) {
|
||||
createToolRoute(app, {
|
||||
toolId: "linearize-pdf",
|
||||
settingsSchema,
|
||||
process: async () => {
|
||||
throw new Error("linearize-pdf is v2-only");
|
||||
},
|
||||
processV2: async (ctx) => {
|
||||
const input = ctx.inputs[0];
|
||||
const base = input.filename.replace(/\.[^.]+$/, "");
|
||||
const inPath = join(ctx.scratchDir, `in-${input.filename.replace(/[^A-Za-z0-9._-]/g, "_")}`);
|
||||
await writeFile(inPath, input.buffer);
|
||||
|
||||
const outPath = join(ctx.scratchDir, `${base}_linearized.pdf`);
|
||||
ctx.report(10, "Linearizing");
|
||||
await qpdfLinearize(inPath, outPath);
|
||||
ctx.report(90, "Done");
|
||||
|
||||
return {
|
||||
scratchPath: outPath,
|
||||
filename: `${base}_linearized.pdf`,
|
||||
contentType: "application/pdf",
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { htmlToPdfPy } from "@snapotter/doc-engine";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
|
||||
const settingsSchema = z.object({});
|
||||
|
||||
export function registerMarkdownToPdf(app: FastifyInstance) {
|
||||
createToolRoute(app, {
|
||||
toolId: "markdown-to-pdf",
|
||||
settingsSchema,
|
||||
process: async () => {
|
||||
throw new Error("markdown-to-pdf is v2-only");
|
||||
},
|
||||
processV2: async (ctx) => {
|
||||
const input = ctx.inputs[0];
|
||||
const base = input.filename.replace(/\.[^.]+$/, "");
|
||||
const inPath = join(ctx.scratchDir, `in-${input.filename.replace(/[^A-Za-z0-9._-]/g, "_")}`);
|
||||
await writeFile(inPath, input.buffer);
|
||||
|
||||
const outPath = join(ctx.scratchDir, `${base}.pdf`);
|
||||
ctx.report(10, "Converting");
|
||||
await htmlToPdfPy(inPath, outPath, "markdown");
|
||||
ctx.report(90, "Done");
|
||||
|
||||
return {
|
||||
scratchPath: outPath,
|
||||
filename: `${base}.pdf`,
|
||||
contentType: "application/pdf",
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import type { NupValue } from "@snapotter/doc-engine";
|
||||
import { pdfcpuNup } from "@snapotter/doc-engine";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
|
||||
const settingsSchema = z.object({
|
||||
perSheet: z
|
||||
.union([
|
||||
z.literal(2),
|
||||
z.literal(3),
|
||||
z.literal(4),
|
||||
z.literal(8),
|
||||
z.literal(9),
|
||||
z.literal(12),
|
||||
z.literal(16),
|
||||
])
|
||||
.default(2),
|
||||
});
|
||||
|
||||
export function registerNupPdf(app: FastifyInstance) {
|
||||
createToolRoute(app, {
|
||||
toolId: "nup-pdf",
|
||||
settingsSchema,
|
||||
process: async () => {
|
||||
throw new Error("nup-pdf is v2-only");
|
||||
},
|
||||
processV2: async (ctx) => {
|
||||
const settings = settingsSchema.parse(ctx.settings);
|
||||
const input = ctx.inputs[0];
|
||||
const base = input.filename.replace(/\.[^.]+$/, "");
|
||||
const inPath = join(ctx.scratchDir, `in-${input.filename.replace(/[^A-Za-z0-9._-]/g, "_")}`);
|
||||
await writeFile(inPath, input.buffer);
|
||||
|
||||
const outPath = join(ctx.scratchDir, `${base}_nup.pdf`);
|
||||
ctx.report(10, "Arranging pages");
|
||||
await pdfcpuNup(inPath, settings.perSheet as NupValue, outPath);
|
||||
ctx.report(90, "Done");
|
||||
|
||||
return {
|
||||
scratchPath: outPath,
|
||||
filename: `${base}_nup.pdf`,
|
||||
contentType: "application/pdf",
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { qpdfPagesSpec } from "@snapotter/doc-engine";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
|
||||
const rangeField = z
|
||||
.string()
|
||||
.max(200)
|
||||
.regex(/^[0-9rz][0-9rz,-]*$/i, "Invalid page range");
|
||||
|
||||
const settingsSchema = z.object({
|
||||
order: rangeField,
|
||||
});
|
||||
|
||||
export function registerOrganizePdf(app: FastifyInstance) {
|
||||
createToolRoute(app, {
|
||||
toolId: "organize-pdf",
|
||||
settingsSchema,
|
||||
process: async () => {
|
||||
throw new Error("organize-pdf is v2-only");
|
||||
},
|
||||
processV2: async (ctx) => {
|
||||
const settings = settingsSchema.parse(ctx.settings);
|
||||
const input = ctx.inputs[0];
|
||||
const base = input.filename.replace(/\.[^.]+$/, "");
|
||||
const inPath = join(ctx.scratchDir, `in-${input.filename.replace(/[^A-Za-z0-9._-]/g, "_")}`);
|
||||
await writeFile(inPath, input.buffer);
|
||||
|
||||
const outPath = join(ctx.scratchDir, `${base}_organized.pdf`);
|
||||
ctx.report(10, "Reordering pages");
|
||||
await qpdfPagesSpec(inPath, settings.order, outPath);
|
||||
ctx.report(90, "Done");
|
||||
|
||||
return {
|
||||
scratchPath: outPath,
|
||||
filename: `${base}_organized.pdf`,
|
||||
contentType: "application/pdf",
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { pdfMetadataGetPy, pdfMetadataSetPy } from "@snapotter/doc-engine";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
|
||||
const settingsSchema = z.object({
|
||||
title: z.string().max(500).optional(),
|
||||
author: z.string().max(500).optional(),
|
||||
subject: z.string().max(500).optional(),
|
||||
keywords: z.string().max(500).optional(),
|
||||
});
|
||||
|
||||
export function registerPdfMetadata(app: FastifyInstance) {
|
||||
createToolRoute(app, {
|
||||
toolId: "pdf-metadata",
|
||||
settingsSchema,
|
||||
process: async () => {
|
||||
throw new Error("pdf-metadata is v2-only");
|
||||
},
|
||||
processV2: async (ctx) => {
|
||||
const settings = settingsSchema.parse(ctx.settings);
|
||||
const input = ctx.inputs[0];
|
||||
const base = input.filename.replace(/\.[^.]+$/, "");
|
||||
const inPath = join(ctx.scratchDir, `in-${input.filename.replace(/[^A-Za-z0-9._-]/g, "_")}`);
|
||||
await writeFile(inPath, input.buffer);
|
||||
|
||||
const outPath = join(ctx.scratchDir, `${base}_metadata.pdf`);
|
||||
ctx.report(10, "Setting metadata");
|
||||
|
||||
// Build metadata record from DEFINED keys only (empty string = clear).
|
||||
const meta: Record<string, string> = {};
|
||||
if (settings.title !== undefined) meta.Title = settings.title;
|
||||
if (settings.author !== undefined) meta.Author = settings.author;
|
||||
if (settings.subject !== undefined) meta.Subject = settings.subject;
|
||||
if (settings.keywords !== undefined) meta.Keywords = settings.keywords;
|
||||
|
||||
await pdfMetadataSetPy(inPath, outPath, meta);
|
||||
const metadata = await pdfMetadataGetPy(outPath);
|
||||
ctx.report(90, "Done");
|
||||
|
||||
return {
|
||||
scratchPath: outPath,
|
||||
filename: `${base}_metadata.pdf`,
|
||||
contentType: "application/pdf",
|
||||
resultPayload: { metadata },
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { pdfcpuTextStamp } from "@snapotter/doc-engine";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
|
||||
const settingsSchema = z.object({
|
||||
position: z.enum(["bl", "bc", "br", "tl", "tc", "tr"]).default("bc"),
|
||||
fontSize: z.number().int().min(6).max(24).default(10),
|
||||
});
|
||||
|
||||
export function registerPdfPageNumbers(app: FastifyInstance) {
|
||||
createToolRoute(app, {
|
||||
toolId: "pdf-page-numbers",
|
||||
settingsSchema,
|
||||
process: async () => {
|
||||
throw new Error("pdf-page-numbers is v2-only");
|
||||
},
|
||||
processV2: async (ctx) => {
|
||||
const settings = settingsSchema.parse(ctx.settings);
|
||||
const input = ctx.inputs[0];
|
||||
const base = input.filename.replace(/\.[^.]+$/, "");
|
||||
const inPath = join(ctx.scratchDir, `in-${input.filename.replace(/[^A-Za-z0-9._-]/g, "_")}`);
|
||||
await writeFile(inPath, input.buffer);
|
||||
|
||||
const outPath = join(ctx.scratchDir, `${base}_numbered.pdf`);
|
||||
ctx.report(10, "Adding page numbers");
|
||||
await pdfcpuTextStamp(
|
||||
inPath,
|
||||
{
|
||||
text: "Page %p of %P",
|
||||
position: settings.position,
|
||||
fontSize: settings.fontSize,
|
||||
opacity: 1,
|
||||
rotation: 0,
|
||||
},
|
||||
outPath,
|
||||
);
|
||||
ctx.report(90, "Done");
|
||||
|
||||
return {
|
||||
scratchPath: outPath,
|
||||
filename: `${base}_numbered.pdf`,
|
||||
contentType: "application/pdf",
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { pdfTextPy } from "@snapotter/doc-engine";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
|
||||
const settingsSchema = z.object({});
|
||||
|
||||
export function registerPdfToText(app: FastifyInstance) {
|
||||
createToolRoute(app, {
|
||||
toolId: "pdf-to-text",
|
||||
settingsSchema,
|
||||
process: async () => {
|
||||
throw new Error("pdf-to-text is v2-only");
|
||||
},
|
||||
processV2: async (ctx) => {
|
||||
const input = ctx.inputs[0];
|
||||
const base = input.filename.replace(/\.[^.]+$/, "");
|
||||
const inPath = join(ctx.scratchDir, `in-${input.filename.replace(/[^A-Za-z0-9._-]/g, "_")}`);
|
||||
await writeFile(inPath, input.buffer);
|
||||
|
||||
const outPath = join(ctx.scratchDir, `${base}.txt`);
|
||||
ctx.report(10, "Extracting text");
|
||||
await pdfTextPy(inPath, outPath);
|
||||
ctx.report(90, "Done");
|
||||
|
||||
return {
|
||||
scratchPath: outPath,
|
||||
filename: `${base}.txt`,
|
||||
contentType: "text/plain",
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { pdfToWordPy } from "@snapotter/doc-engine";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
|
||||
const settingsSchema = z.object({});
|
||||
|
||||
export function registerPdfToWord(app: FastifyInstance) {
|
||||
createToolRoute(app, {
|
||||
toolId: "pdf-to-word",
|
||||
settingsSchema,
|
||||
process: async () => {
|
||||
throw new Error("pdf-to-word is v2-only");
|
||||
},
|
||||
processV2: async (ctx) => {
|
||||
const input = ctx.inputs[0];
|
||||
const base = input.filename.replace(/\.[^.]+$/, "");
|
||||
const inPath = join(ctx.scratchDir, `in-${input.filename.replace(/[^A-Za-z0-9._-]/g, "_")}`);
|
||||
await writeFile(inPath, input.buffer);
|
||||
|
||||
const outPath = join(ctx.scratchDir, `${base}.docx`);
|
||||
ctx.report(10, "Converting");
|
||||
await pdfToWordPy(inPath, outPath);
|
||||
ctx.report(90, "Done");
|
||||
|
||||
return {
|
||||
scratchPath: outPath,
|
||||
filename: `${base}.docx`,
|
||||
contentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { gsPdfaConvert } from "@snapotter/doc-engine";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
|
||||
const settingsSchema = z.object({});
|
||||
|
||||
export function registerPdfaConvert(app: FastifyInstance) {
|
||||
createToolRoute(app, {
|
||||
toolId: "pdfa-convert",
|
||||
settingsSchema,
|
||||
process: async () => {
|
||||
throw new Error("pdfa-convert is v2-only");
|
||||
},
|
||||
processV2: async (ctx) => {
|
||||
const input = ctx.inputs[0];
|
||||
const base = input.filename.replace(/\.[^.]+$/, "");
|
||||
const inPath = join(ctx.scratchDir, `in-${input.filename.replace(/[^A-Za-z0-9._-]/g, "_")}`);
|
||||
await writeFile(inPath, input.buffer);
|
||||
|
||||
const outPath = join(ctx.scratchDir, `${base}_pdfa.pdf`);
|
||||
ctx.report(10, "Converting to PDF/A");
|
||||
await gsPdfaConvert(inPath, outPath);
|
||||
ctx.report(90, "Done");
|
||||
|
||||
return {
|
||||
scratchPath: outPath,
|
||||
filename: `${base}_pdfa.pdf`,
|
||||
contentType: "application/pdf",
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { qpdfEncrypt } from "@snapotter/doc-engine";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
|
||||
const settingsSchema = z.object({
|
||||
userPassword: z.string().min(1).max(256),
|
||||
ownerPassword: z.string().min(1).max(256).optional(),
|
||||
});
|
||||
|
||||
export function registerProtectPdf(app: FastifyInstance) {
|
||||
createToolRoute(app, {
|
||||
toolId: "protect-pdf",
|
||||
settingsSchema,
|
||||
redactSettingsForAudit: (settings) => {
|
||||
const s = settings as z.infer<typeof settingsSchema>;
|
||||
return {
|
||||
...s,
|
||||
userPassword: "<redacted>",
|
||||
ownerPassword: "<redacted>",
|
||||
};
|
||||
},
|
||||
process: async () => {
|
||||
throw new Error("protect-pdf is v2-only");
|
||||
},
|
||||
processV2: async (ctx) => {
|
||||
const settings = settingsSchema.parse(ctx.settings);
|
||||
const input = ctx.inputs[0];
|
||||
const base = input.filename.replace(/\.[^.]+$/, "");
|
||||
const inPath = join(ctx.scratchDir, `in-${input.filename.replace(/[^A-Za-z0-9._-]/g, "_")}`);
|
||||
await writeFile(inPath, input.buffer);
|
||||
|
||||
const outPath = join(ctx.scratchDir, `${base}_protected.pdf`);
|
||||
ctx.report(10, "Encrypting");
|
||||
await qpdfEncrypt(
|
||||
inPath,
|
||||
settings.userPassword,
|
||||
settings.ownerPassword ?? settings.userPassword,
|
||||
outPath,
|
||||
);
|
||||
ctx.report(90, "Done");
|
||||
|
||||
return {
|
||||
scratchPath: outPath,
|
||||
filename: `${base}_protected.pdf`,
|
||||
contentType: "application/pdf",
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { pdfRedactPy } from "@snapotter/doc-engine";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
|
||||
const settingsSchema = z.object({
|
||||
terms: z.array(z.string().min(1).max(200)).min(1).max(50),
|
||||
caseSensitive: z.boolean().default(false),
|
||||
});
|
||||
|
||||
export function registerRedactPdf(app: FastifyInstance) {
|
||||
createToolRoute(app, {
|
||||
toolId: "redact-pdf",
|
||||
settingsSchema,
|
||||
process: async () => {
|
||||
throw new Error("redact-pdf is v2-only");
|
||||
},
|
||||
processV2: async (ctx) => {
|
||||
const settings = settingsSchema.parse(ctx.settings);
|
||||
const input = ctx.inputs[0];
|
||||
const base = input.filename.replace(/\.[^.]+$/, "");
|
||||
const inPath = join(ctx.scratchDir, `in-${input.filename.replace(/[^A-Za-z0-9._-]/g, "_")}`);
|
||||
await writeFile(inPath, input.buffer);
|
||||
|
||||
const outPath = join(ctx.scratchDir, `${base}_redacted.pdf`);
|
||||
ctx.report(10, "Redacting");
|
||||
const { found } = await pdfRedactPy(inPath, outPath, settings.terms, settings.caseSensitive);
|
||||
ctx.report(90, "Done");
|
||||
|
||||
return {
|
||||
scratchPath: outPath,
|
||||
filename: `${base}_redacted.pdf`,
|
||||
contentType: "application/pdf",
|
||||
resultPayload: { found },
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { qpdfPageCount, qpdfPagesSpecUnchecked } from "@snapotter/doc-engine";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { compressPageRuns, parsePageSpec } from "../../lib/page-spec.js";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
|
||||
const rangeField = z
|
||||
.string()
|
||||
.max(200)
|
||||
.regex(/^[0-9rz][0-9rz,-]*$/i, "Invalid page range");
|
||||
|
||||
const settingsSchema = z.object({
|
||||
pages: rangeField,
|
||||
});
|
||||
|
||||
export function registerRemovePages(app: FastifyInstance) {
|
||||
createToolRoute(app, {
|
||||
toolId: "remove-pages",
|
||||
settingsSchema,
|
||||
process: async () => {
|
||||
throw new Error("remove-pages is v2-only");
|
||||
},
|
||||
processV2: async (ctx) => {
|
||||
const settings = settingsSchema.parse(ctx.settings);
|
||||
const input = ctx.inputs[0];
|
||||
const base = input.filename.replace(/\.[^.]+$/, "");
|
||||
const inPath = join(ctx.scratchDir, `in-${input.filename.replace(/[^A-Za-z0-9._-]/g, "_")}`);
|
||||
await writeFile(inPath, input.buffer);
|
||||
|
||||
ctx.report(10, "Analyzing pages");
|
||||
const total = await qpdfPageCount(inPath);
|
||||
const removeSet = parsePageSpec(settings.pages, total);
|
||||
|
||||
// Build the keep list: all pages NOT in the remove set
|
||||
const keepPages: number[] = [];
|
||||
for (let i = 1; i <= total; i++) {
|
||||
if (!removeSet.has(i)) {
|
||||
keepPages.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
if (keepPages.length === 0) {
|
||||
throw new Error("Cannot remove every page from the document");
|
||||
}
|
||||
|
||||
const keepSpec = compressPageRuns(keepPages);
|
||||
const outPath = join(ctx.scratchDir, `${base}_removed.pdf`);
|
||||
ctx.report(30, "Removing pages");
|
||||
await qpdfPagesSpecUnchecked(inPath, keepSpec, outPath);
|
||||
ctx.report(90, "Done");
|
||||
|
||||
return {
|
||||
scratchPath: outPath,
|
||||
filename: `${base}_removed.pdf`,
|
||||
contentType: "application/pdf",
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { qpdfRepair } from "@snapotter/doc-engine";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
|
||||
const settingsSchema = z.object({});
|
||||
|
||||
export function registerRepairPdf(app: FastifyInstance) {
|
||||
createToolRoute(app, {
|
||||
toolId: "repair-pdf",
|
||||
settingsSchema,
|
||||
skipStructuralValidation: true,
|
||||
process: async () => {
|
||||
throw new Error("repair-pdf is v2-only");
|
||||
},
|
||||
processV2: async (ctx) => {
|
||||
const input = ctx.inputs[0];
|
||||
const base = input.filename.replace(/\.[^.]+$/, "");
|
||||
const inPath = join(ctx.scratchDir, `in-${input.filename.replace(/[^A-Za-z0-9._-]/g, "_")}`);
|
||||
await writeFile(inPath, input.buffer);
|
||||
|
||||
const outPath = join(ctx.scratchDir, `${base}_repaired.pdf`);
|
||||
ctx.report(10, "Repairing");
|
||||
await qpdfRepair(inPath, outPath);
|
||||
ctx.report(90, "Done");
|
||||
|
||||
return {
|
||||
scratchPath: outPath,
|
||||
filename: `${base}_repaired.pdf`,
|
||||
contentType: "application/pdf",
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { qpdfDecrypt } from "@snapotter/doc-engine";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
|
||||
const settingsSchema = z.object({
|
||||
password: z.string().min(1).max(256),
|
||||
});
|
||||
|
||||
export function registerUnlockPdf(app: FastifyInstance) {
|
||||
createToolRoute(app, {
|
||||
toolId: "unlock-pdf",
|
||||
settingsSchema,
|
||||
redactSettingsForAudit: (settings) => {
|
||||
const s = settings as z.infer<typeof settingsSchema>;
|
||||
return {
|
||||
...s,
|
||||
password: "<redacted>",
|
||||
};
|
||||
},
|
||||
process: async () => {
|
||||
throw new Error("unlock-pdf is v2-only");
|
||||
},
|
||||
processV2: async (ctx) => {
|
||||
const settings = settingsSchema.parse(ctx.settings);
|
||||
const input = ctx.inputs[0];
|
||||
const base = input.filename.replace(/\.[^.]+$/, "");
|
||||
const inPath = join(ctx.scratchDir, `in-${input.filename.replace(/[^A-Za-z0-9._-]/g, "_")}`);
|
||||
await writeFile(inPath, input.buffer);
|
||||
|
||||
const outPath = join(ctx.scratchDir, `${base}_unlocked.pdf`);
|
||||
ctx.report(10, "Decrypting");
|
||||
await qpdfDecrypt(inPath, settings.password, outPath);
|
||||
ctx.report(90, "Done");
|
||||
|
||||
return {
|
||||
scratchPath: outPath,
|
||||
filename: `${base}_unlocked.pdf`,
|
||||
contentType: "application/pdf",
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { pdfcpuTextStamp } from "@snapotter/doc-engine";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
|
||||
const settingsSchema = z.object({
|
||||
text: z.string().min(1).max(200),
|
||||
position: z.enum(["tl", "tc", "tr", "l", "c", "r", "bl", "bc", "br"]).default("c"),
|
||||
fontSize: z.number().int().min(6).max(72).default(48),
|
||||
opacity: z.number().min(0.05).max(1).default(0.3),
|
||||
rotation: z.number().min(-180).max(180).default(45),
|
||||
});
|
||||
|
||||
export function registerWatermarkPdf(app: FastifyInstance) {
|
||||
createToolRoute(app, {
|
||||
toolId: "watermark-pdf",
|
||||
settingsSchema,
|
||||
process: async () => {
|
||||
throw new Error("watermark-pdf is v2-only");
|
||||
},
|
||||
processV2: async (ctx) => {
|
||||
const settings = settingsSchema.parse(ctx.settings);
|
||||
const input = ctx.inputs[0];
|
||||
const base = input.filename.replace(/\.[^.]+$/, "");
|
||||
const inPath = join(ctx.scratchDir, `in-${input.filename.replace(/[^A-Za-z0-9._-]/g, "_")}`);
|
||||
await writeFile(inPath, input.buffer);
|
||||
|
||||
const outPath = join(ctx.scratchDir, `${base}_watermarked.pdf`);
|
||||
ctx.report(10, "Stamping watermark");
|
||||
await pdfcpuTextStamp(
|
||||
inPath,
|
||||
{
|
||||
text: settings.text,
|
||||
position: settings.position,
|
||||
fontSize: settings.fontSize,
|
||||
opacity: settings.opacity,
|
||||
rotation: settings.rotation,
|
||||
},
|
||||
outPath,
|
||||
);
|
||||
ctx.report(90, "Done");
|
||||
|
||||
return {
|
||||
scratchPath: outPath,
|
||||
filename: `${base}_watermarked.pdf`,
|
||||
contentType: "application/pdf",
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user