nomkit 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.txt +21 -0
- package/dist/_virtual/_rolldown/runtime.js +27 -0
- package/dist/adapters/index.d.ts +15 -0
- package/dist/adapters/index.js +6 -0
- package/dist/cli/commands/push.d.ts +6 -0
- package/dist/cli/commands/push.js +143 -0
- package/dist/cli/index.d.ts +4 -0
- package/dist/cli/index.js +18 -0
- package/dist/cli/lib/collection_sync.d.ts +107 -0
- package/dist/cli/lib/collection_sync.js +158 -0
- package/dist/cli/lib/config_loader.d.ts +15 -0
- package/dist/cli/lib/config_loader.js +43 -0
- package/dist/cli/lib/hash.d.ts +22 -0
- package/dist/cli/lib/hash.js +63 -0
- package/dist/cli/lib/migrations.d.ts +6 -0
- package/dist/cli/lib/migrations.js +17 -0
- package/dist/client/index.d.ts +13 -0
- package/dist/client/index.js +34 -0
- package/dist/core/nomba_api/banks.d.ts +14 -0
- package/dist/core/nomba_api/banks.js +0 -0
- package/dist/core/nomba_api/charge-tokenized-card.d.ts +33 -0
- package/dist/core/nomba_api/charge-tokenized-card.js +0 -0
- package/dist/core/nomba_api/checkout.d.ts +44 -0
- package/dist/core/nomba_api/checkout.js +0 -0
- package/dist/core/nomba_api/get_checkout.d.ts +57 -0
- package/dist/core/nomba_api/get_checkout.js +0 -0
- package/dist/core/nomba_api/index.d.ts +313 -0
- package/dist/core/nomba_api/index.js +179 -0
- package/dist/core/nomba_api/lib/utils.d.ts +235 -0
- package/dist/core/nomba_api/lib/utils.js +313 -0
- package/dist/core/nomba_api/list-tokenized-cards.d.ts +24 -0
- package/dist/core/nomba_api/list-tokenized-cards.js +0 -0
- package/dist/core/nomba_api/token-manager/index.d.ts +51 -0
- package/dist/core/nomba_api/token-manager/index.js +109 -0
- package/dist/core/pg_db/index.d.ts +108 -0
- package/dist/core/pg_db/index.js +76 -0
- package/dist/core/pg_db/migrations/20260703085901_wealthy_blacklash/migration.sql +120 -0
- package/dist/core/pg_db/migrations/20260703085901_wealthy_blacklash/snapshot.json +1616 -0
- package/dist/core/pg_db/relations.d.ts +46 -0
- package/dist/core/pg_db/relations.js +83 -0
- package/dist/core/pg_db/schema.d.ts +1138 -0
- package/dist/core/pg_db/schema.js +124 -0
- package/dist/endpoints/customers/api.js +51 -0
- package/dist/endpoints/entitlements/api.js +42 -0
- package/dist/endpoints/routes.d.ts +15 -0
- package/dist/endpoints/routes.js +15 -0
- package/dist/endpoints/subscriptions/api.js +263 -0
- package/dist/endpoints/subscriptions/utils.js +105 -0
- package/dist/endpoints/webhooks/invoice/api.js +28 -0
- package/dist/endpoints/webhooks/nomba/api.js +76 -0
- package/dist/endpoints/webhooks/nomba/utils.js +36 -0
- package/dist/index.d.ts +204 -0
- package/dist/index.js +175 -0
- package/dist/lib/utils.d.ts +21 -0
- package/dist/lib/utils.js +41 -0
- package/dist/node_modules/.pnpm/@better-fetch_fetch@1.3.1/node_modules/@better-fetch/fetch/dist/index.js +475 -0
- package/dist/package.js +4 -0
- package/dist/queue/backends/pglite/backend.d.ts +43 -0
- package/dist/queue/backends/pglite/backend.js +33 -0
- package/dist/queue/backends/pglite/index.d.ts +4 -0
- package/dist/queue/backends/pglite/index.js +4 -0
- package/dist/queue/backends/pglite/migrations/schema.d.ts +4 -0
- package/dist/queue/backends/pglite/migrations/schema.js +37 -0
- package/dist/queue/backends/pglite/notification-channel.d.ts +17 -0
- package/dist/queue/backends/pglite/notification-channel.js +61 -0
- package/dist/queue/backends/pglite/repository.d.ts +38 -0
- package/dist/queue/backends/pglite/repository.js +299 -0
- package/dist/queue/backends/redis/index.d.ts +7 -0
- package/dist/queue/backends/redis/index.js +1 -0
- package/dist/queue/client/index.d.ts +12 -0
- package/dist/queue/client/index.js +31 -0
- package/dist/queue/endpoints/api.d.ts +53 -0
- package/dist/queue/endpoints/api.js +45 -0
- package/dist/queue/endpoints/routes.d.ts +32 -0
- package/dist/queue/endpoints/routes.js +5 -0
- package/dist/queue/init.d.ts +27 -0
- package/dist/queue/init.js +31 -0
- package/dist/queue/lib/billing.d.ts +25 -0
- package/dist/queue/lib/billing.js +87 -0
- package/dist/queue/lib/utils.d.ts +30 -0
- package/dist/queue/lib/utils.js +35 -0
- package/package.json +71 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { BaseNotificationChannel } from "agenda";
|
|
2
|
+
//#region queue/backends/pglite/notification-channel.ts
|
|
3
|
+
const CHANNEL = "agenda_jobs_channel";
|
|
4
|
+
/**
|
|
5
|
+
* Real-time notifications for the PGlite backend using NOTIFY/LISTEN.
|
|
6
|
+
*/
|
|
7
|
+
var PgliteNotificationChannel = class extends BaseNotificationChannel {
|
|
8
|
+
db;
|
|
9
|
+
unlisten = null;
|
|
10
|
+
constructor(db) {
|
|
11
|
+
super();
|
|
12
|
+
this.db = db;
|
|
13
|
+
}
|
|
14
|
+
async connect() {
|
|
15
|
+
if (this.state === "connected" || this.state === "connecting") return;
|
|
16
|
+
this.setState("connecting");
|
|
17
|
+
try {
|
|
18
|
+
this.unlisten = await this.db.listen(CHANNEL, (payload) => {
|
|
19
|
+
try {
|
|
20
|
+
const raw = JSON.parse(payload);
|
|
21
|
+
const notification = {
|
|
22
|
+
jobId: raw.jobId,
|
|
23
|
+
jobName: raw.jobName,
|
|
24
|
+
nextRunAt: raw.nextRunAt ? new Date(raw.nextRunAt) : null,
|
|
25
|
+
priority: raw.priority ?? 0,
|
|
26
|
+
timestamp: new Date(raw.timestamp ?? Date.now()),
|
|
27
|
+
source: raw.source
|
|
28
|
+
};
|
|
29
|
+
this.notifyHandlers(notification);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
this.emit("error", err);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
this.setState("connected");
|
|
35
|
+
} catch (err) {
|
|
36
|
+
this.setState("error");
|
|
37
|
+
this.emit("error", err);
|
|
38
|
+
throw err;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async disconnect() {
|
|
42
|
+
if (this.unlisten) {
|
|
43
|
+
await this.unlisten();
|
|
44
|
+
this.unlisten = null;
|
|
45
|
+
}
|
|
46
|
+
this.setState("disconnected");
|
|
47
|
+
}
|
|
48
|
+
async publish(notification) {
|
|
49
|
+
const payload = JSON.stringify({
|
|
50
|
+
jobId: notification.jobId,
|
|
51
|
+
jobName: notification.jobName,
|
|
52
|
+
nextRunAt: notification.nextRunAt,
|
|
53
|
+
priority: notification.priority,
|
|
54
|
+
timestamp: notification.timestamp,
|
|
55
|
+
source: notification.source
|
|
56
|
+
});
|
|
57
|
+
await this.db.query(`NOTIFY ${CHANNEL}, '${payload.replace(/'/g, "''")}'`);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
//#endregion
|
|
61
|
+
export { PgliteNotificationChannel };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { JobId, JobParameters, JobRepository, JobRepositoryOptions, JobsOverview, JobsQueryOptions, JobsResult, RemoveJobsOptions } from "agenda";
|
|
2
|
+
import { PGlite } from "@electric-sql/pglite";
|
|
3
|
+
|
|
4
|
+
//#region queue/backends/pglite/repository.d.ts
|
|
5
|
+
interface PgliteRepositoryOptions {
|
|
6
|
+
/** Table name to store jobs in.'.
|
|
7
|
+
* @default "agenda_jobs"
|
|
8
|
+
*/
|
|
9
|
+
table?: string;
|
|
10
|
+
}
|
|
11
|
+
declare class PgliteRepository implements JobRepository {
|
|
12
|
+
private readonly db;
|
|
13
|
+
private readonly table;
|
|
14
|
+
private ready;
|
|
15
|
+
constructor(db: PGlite, options?: PgliteRepositoryOptions);
|
|
16
|
+
connect(): Promise<void>;
|
|
17
|
+
queryJobs(options?: JobsQueryOptions): Promise<JobsResult>;
|
|
18
|
+
getJobsOverview(): Promise<JobsOverview[]>;
|
|
19
|
+
getDistinctJobNames(): Promise<string[]>;
|
|
20
|
+
getJobById(id: string): Promise<JobParameters | null>;
|
|
21
|
+
getQueueSize(): Promise<number>;
|
|
22
|
+
private buildRemoveWhere;
|
|
23
|
+
removeJobs(options: RemoveJobsOptions): Promise<number>;
|
|
24
|
+
disableJobs(options: RemoveJobsOptions): Promise<number>;
|
|
25
|
+
enableJobs(options: RemoveJobsOptions): Promise<number>;
|
|
26
|
+
purgeAllJobs(): Promise<number>;
|
|
27
|
+
saveJob<DATA = unknown>(job: JobParameters<DATA>, _options?: JobRepositoryOptions): Promise<JobParameters<DATA>>;
|
|
28
|
+
saveJobState(job: JobParameters, _options?: JobRepositoryOptions): Promise<void>;
|
|
29
|
+
lockJob(job: JobParameters, _options?: JobRepositoryOptions): Promise<JobParameters | undefined>;
|
|
30
|
+
unlockJob(job: JobParameters): Promise<void>;
|
|
31
|
+
unlockJobs(jobIds: (JobId | string)[]): Promise<void>;
|
|
32
|
+
getNextJobToRun(jobName: string, nextScanAt: Date, lockDeadline: Date, now?: Date, _options?: JobRepositoryOptions): Promise<JobParameters | undefined>;
|
|
33
|
+
private insert;
|
|
34
|
+
private upsertById;
|
|
35
|
+
private upsertByMatch;
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
export { PgliteRepository, PgliteRepositoryOptions };
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { migratePGlite } from "./migrations/schema.js";
|
|
2
|
+
import { computeJobState, toJobId } from "agenda";
|
|
3
|
+
//#region queue/backends/pglite/repository.ts
|
|
4
|
+
function toDate(v) {
|
|
5
|
+
return v == null ? void 0 : new Date(v);
|
|
6
|
+
}
|
|
7
|
+
function rowToJob(row) {
|
|
8
|
+
return {
|
|
9
|
+
_id: toJobId(row.id),
|
|
10
|
+
name: row.name,
|
|
11
|
+
type: row.type ?? "normal",
|
|
12
|
+
priority: row.priority,
|
|
13
|
+
nextRunAt: toDate(row.next_run_at) ?? null,
|
|
14
|
+
lastModifiedBy: row.last_modified_by ?? void 0,
|
|
15
|
+
lockedAt: toDate(row.locked_at),
|
|
16
|
+
lastFinishedAt: toDate(row.last_finished_at),
|
|
17
|
+
lastRunAt: toDate(row.last_run_at),
|
|
18
|
+
repeatInterval: row.repeat_interval ?? void 0,
|
|
19
|
+
repeatTimezone: row.repeat_timezone ?? void 0,
|
|
20
|
+
repeatAt: row.repeat_at ?? void 0,
|
|
21
|
+
data: row.data ?? {},
|
|
22
|
+
unique: row.is_unique ?? void 0,
|
|
23
|
+
uniqueOpts: row.unique_opts ?? void 0,
|
|
24
|
+
failReason: row.fail_reason ?? void 0,
|
|
25
|
+
failCount: row.fail_count,
|
|
26
|
+
failedAt: toDate(row.failed_at),
|
|
27
|
+
disabled: row.disabled,
|
|
28
|
+
progress: row.progress ?? void 0
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function rowToJobWithState(row) {
|
|
32
|
+
const job = rowToJob(row);
|
|
33
|
+
return {
|
|
34
|
+
...job,
|
|
35
|
+
_id: job._id,
|
|
36
|
+
state: computeJobState(job)
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const SORT_COLUMN_MAP = {
|
|
40
|
+
nextRunAt: "next_run_at",
|
|
41
|
+
lastRunAt: "last_run_at",
|
|
42
|
+
lastFinishedAt: "last_finished_at",
|
|
43
|
+
priority: "priority",
|
|
44
|
+
name: "name",
|
|
45
|
+
data: "data"
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Agenda's `unique` field uses dotted paths like 'data.userId' as keys.
|
|
49
|
+
* Build a nested JSON object (rooted at `data`) suitable for a `data @> ...`
|
|
50
|
+
* containment query, e.g. { 'data.userId': 5 } -> { userId: 5 }.
|
|
51
|
+
*/
|
|
52
|
+
function uniqueToDataContainment(unique) {
|
|
53
|
+
const root = {};
|
|
54
|
+
for (const [path, value] of Object.entries(unique)) {
|
|
55
|
+
const parts = (path.startsWith("data.") ? path.slice(5) : path).split(".");
|
|
56
|
+
let cursor = root;
|
|
57
|
+
for (let i = 0; i < parts.length - 1; i++) cursor = cursor[parts[i]] ??= {};
|
|
58
|
+
cursor[parts[parts.length - 1]] = value;
|
|
59
|
+
}
|
|
60
|
+
return root;
|
|
61
|
+
}
|
|
62
|
+
var PgliteRepository = class {
|
|
63
|
+
db;
|
|
64
|
+
table;
|
|
65
|
+
ready = null;
|
|
66
|
+
constructor(db, options = {}) {
|
|
67
|
+
this.db = db;
|
|
68
|
+
this.table = options.table ?? "agenda_jobs";
|
|
69
|
+
}
|
|
70
|
+
async connect() {
|
|
71
|
+
if (this.ready) return this.ready;
|
|
72
|
+
this.ready = (async () => {
|
|
73
|
+
await this.db.exec(migratePGlite(this.table));
|
|
74
|
+
})();
|
|
75
|
+
return this.ready;
|
|
76
|
+
}
|
|
77
|
+
async queryJobs(options = {}) {
|
|
78
|
+
const where = [];
|
|
79
|
+
const params = [];
|
|
80
|
+
const add = (clause, value) => {
|
|
81
|
+
params.push(value);
|
|
82
|
+
where.push(clause.replace("?", `$${params.length}`));
|
|
83
|
+
};
|
|
84
|
+
if (options.name) add("name = ?", options.name);
|
|
85
|
+
if (options.names?.length) add("name = ANY(?)", options.names);
|
|
86
|
+
if (options.id) add("id = ?", options.id);
|
|
87
|
+
if (options.ids?.length) add("id = ANY(?)", options.ids);
|
|
88
|
+
if (options.search) add("name ILIKE ?", `%${options.search}%`);
|
|
89
|
+
if (options.data !== void 0) add("data @> ?::jsonb", JSON.stringify(options.data));
|
|
90
|
+
if (options.includeDisabled === false) where.push("disabled = FALSE");
|
|
91
|
+
let sql = `SELECT * FROM "${this.table}"`;
|
|
92
|
+
if (where.length) sql += ` WHERE ${where.join(" AND ")}`;
|
|
93
|
+
const sortField = options.sort ? Object.keys(options.sort)[0] : void 0;
|
|
94
|
+
const sortDir = options.sort && Object.values(options.sort)[0] === -1 ? "DESC" : "ASC";
|
|
95
|
+
const column = sortField && SORT_COLUMN_MAP[sortField] || "next_run_at";
|
|
96
|
+
sql += ` ORDER BY ${column} ${sortDir} NULLS LAST`;
|
|
97
|
+
if (options.limit !== void 0) {
|
|
98
|
+
params.push(options.limit);
|
|
99
|
+
sql += ` LIMIT $${params.length}`;
|
|
100
|
+
}
|
|
101
|
+
if (options.skip !== void 0) {
|
|
102
|
+
params.push(options.skip);
|
|
103
|
+
sql += ` OFFSET $${params.length}`;
|
|
104
|
+
}
|
|
105
|
+
let jobs = (await this.db.query(sql, params)).rows.map(rowToJobWithState);
|
|
106
|
+
if (options.state) jobs = jobs.filter((j) => j.state === options.state);
|
|
107
|
+
const countResult = await this.db.query(`SELECT COUNT(*)::text AS count FROM "${this.table}"${where.length ? ` WHERE ${where.join(" AND ")}` : ""}`, params.slice(0, where.length));
|
|
108
|
+
return {
|
|
109
|
+
jobs,
|
|
110
|
+
total: Number(countResult.rows[0]?.count ?? jobs.length)
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
async getJobsOverview() {
|
|
114
|
+
return (await this.db.query(`
|
|
115
|
+
SELECT
|
|
116
|
+
name,
|
|
117
|
+
COUNT(*)::text AS total,
|
|
118
|
+
COUNT(*) FILTER (WHERE disabled = TRUE)::text AS paused,
|
|
119
|
+
COUNT(*) FILTER (WHERE locked_at IS NOT NULL AND last_finished_at IS NULL)::text AS running,
|
|
120
|
+
COUNT(*) FILTER (WHERE repeat_interval IS NOT NULL)::text AS repeating,
|
|
121
|
+
COUNT(*) FILTER (WHERE fail_count > 0 AND last_finished_at IS NOT NULL AND failed_at IS NOT NULL)::text AS failed,
|
|
122
|
+
COUNT(*) FILTER (WHERE next_run_at IS NULL AND last_finished_at IS NOT NULL AND fail_count = 0)::text AS completed,
|
|
123
|
+
COUNT(*) FILTER (WHERE next_run_at IS NOT NULL AND locked_at IS NULL AND disabled = FALSE)::text AS scheduled
|
|
124
|
+
FROM "${this.table}"
|
|
125
|
+
GROUP BY name
|
|
126
|
+
ORDER BY name
|
|
127
|
+
`)).rows.map((r) => ({
|
|
128
|
+
name: r.name,
|
|
129
|
+
total: Number(r.total),
|
|
130
|
+
running: Number(r.running),
|
|
131
|
+
scheduled: Number(r.scheduled),
|
|
132
|
+
queued: Number(r.scheduled),
|
|
133
|
+
completed: Number(r.completed),
|
|
134
|
+
failed: Number(r.failed),
|
|
135
|
+
repeating: Number(r.repeating),
|
|
136
|
+
paused: Number(r.paused)
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
async getDistinctJobNames() {
|
|
140
|
+
return (await this.db.query(`SELECT DISTINCT name FROM "${this.table}" ORDER BY name`)).rows.map((r) => r.name);
|
|
141
|
+
}
|
|
142
|
+
async getJobById(id) {
|
|
143
|
+
const result = await this.db.query(`SELECT * FROM "${this.table}" WHERE id = $1`, [id]);
|
|
144
|
+
return result.rows[0] ? rowToJob(result.rows[0]) : null;
|
|
145
|
+
}
|
|
146
|
+
async getQueueSize() {
|
|
147
|
+
const result = await this.db.query(`SELECT COUNT(*)::text AS count FROM "${this.table}" WHERE next_run_at <= now() AND disabled = FALSE AND locked_at IS NULL`);
|
|
148
|
+
return Number(result.rows[0]?.count ?? 0);
|
|
149
|
+
}
|
|
150
|
+
buildRemoveWhere(options) {
|
|
151
|
+
const where = [];
|
|
152
|
+
const params = [];
|
|
153
|
+
const add = (clause, value) => {
|
|
154
|
+
params.push(value);
|
|
155
|
+
where.push(clause.replace("?", `$${params.length}`));
|
|
156
|
+
};
|
|
157
|
+
if (options.id) add("id = ?", String(options.id));
|
|
158
|
+
if (options.ids?.length) add("id = ANY(?)", options.ids.map(String));
|
|
159
|
+
if (options.name) add("name = ?", options.name);
|
|
160
|
+
if (options.names?.length) add("name = ANY(?)", options.names);
|
|
161
|
+
if (options.notNames?.length) add("NOT (name = ANY(?))", options.notNames);
|
|
162
|
+
if (options.data !== void 0) add("data @> ?::jsonb", JSON.stringify(options.data));
|
|
163
|
+
return {
|
|
164
|
+
clause: where.join(" AND "),
|
|
165
|
+
params
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
async removeJobs(options) {
|
|
169
|
+
const { clause, params } = this.buildRemoveWhere(options);
|
|
170
|
+
if (!clause) return 0;
|
|
171
|
+
return (await this.db.query(`DELETE FROM "${this.table}" WHERE ${clause}`, params)).affectedRows ?? 0;
|
|
172
|
+
}
|
|
173
|
+
async disableJobs(options) {
|
|
174
|
+
const { clause, params } = this.buildRemoveWhere(options);
|
|
175
|
+
if (!clause) return 0;
|
|
176
|
+
return (await this.db.query(`UPDATE "${this.table}" SET disabled = TRUE, updated_at = now() WHERE ${clause}`, params)).affectedRows ?? 0;
|
|
177
|
+
}
|
|
178
|
+
async enableJobs(options) {
|
|
179
|
+
const { clause, params } = this.buildRemoveWhere(options);
|
|
180
|
+
if (!clause) return 0;
|
|
181
|
+
return (await this.db.query(`UPDATE "${this.table}" SET disabled = FALSE, updated_at = now() WHERE ${clause}`, params)).affectedRows ?? 0;
|
|
182
|
+
}
|
|
183
|
+
async purgeAllJobs() {
|
|
184
|
+
return (await this.db.query(`DELETE FROM "${this.table}"`)).affectedRows ?? 0;
|
|
185
|
+
}
|
|
186
|
+
async saveJob(job, _options) {
|
|
187
|
+
const values = {
|
|
188
|
+
name: job.name,
|
|
189
|
+
type: job.type ?? "normal",
|
|
190
|
+
priority: job.priority ?? 0,
|
|
191
|
+
next_run_at: job.nextRunAt ?? null,
|
|
192
|
+
last_modified_by: job.lastModifiedBy ?? null,
|
|
193
|
+
locked_at: job.lockedAt ?? null,
|
|
194
|
+
last_finished_at: job.lastFinishedAt ?? null,
|
|
195
|
+
last_run_at: job.lastRunAt ?? null,
|
|
196
|
+
repeat_interval: job.repeatInterval != null ? String(job.repeatInterval) : null,
|
|
197
|
+
repeat_timezone: job.repeatTimezone ?? null,
|
|
198
|
+
repeat_at: job.repeatAt ?? null,
|
|
199
|
+
data: JSON.stringify(job.data ?? {}),
|
|
200
|
+
is_unique: job.unique != null ? JSON.stringify(job.unique) : null,
|
|
201
|
+
unique_opts: job.uniqueOpts != null ? JSON.stringify(job.uniqueOpts) : null,
|
|
202
|
+
fail_reason: job.failReason ?? null,
|
|
203
|
+
fail_count: job.failCount ?? 0,
|
|
204
|
+
failed_at: job.failedAt ?? null,
|
|
205
|
+
disabled: job.disabled ?? false,
|
|
206
|
+
progress: job.progress ?? null
|
|
207
|
+
};
|
|
208
|
+
if (job._id) return rowToJob(await this.upsertById(String(job._id), values));
|
|
209
|
+
if (job.type === "single") return rowToJob(await this.upsertByMatch("name = $1 AND type = $2", [job.name, "single"], values));
|
|
210
|
+
if (job.unique != null) {
|
|
211
|
+
const insertOnly = job.uniqueOpts?.insertOnly;
|
|
212
|
+
return rowToJob(await this.upsertByMatch("name = $1 AND data @> $2::jsonb", [job.name, JSON.stringify(uniqueToDataContainment(job.unique))], values, { insertOnly: !!insertOnly }));
|
|
213
|
+
}
|
|
214
|
+
return rowToJob(await this.insert(values));
|
|
215
|
+
}
|
|
216
|
+
async saveJobState(job, _options) {
|
|
217
|
+
if (!job._id) throw new Error("saveJobState requires job._id");
|
|
218
|
+
await this.db.query(`UPDATE "${this.table}" SET
|
|
219
|
+
locked_at = $1, last_finished_at = $2, last_run_at = $3, next_run_at = $4,
|
|
220
|
+
progress = $5, fail_reason = $6, fail_count = $7, failed_at = $8, updated_at = now()
|
|
221
|
+
WHERE id = $9`, [
|
|
222
|
+
job.lockedAt ?? null,
|
|
223
|
+
job.lastFinishedAt ?? null,
|
|
224
|
+
job.lastRunAt ?? null,
|
|
225
|
+
job.nextRunAt ?? null,
|
|
226
|
+
job.progress ?? null,
|
|
227
|
+
job.failReason ?? null,
|
|
228
|
+
job.failCount ?? 0,
|
|
229
|
+
job.failedAt ?? null,
|
|
230
|
+
String(job._id)
|
|
231
|
+
]);
|
|
232
|
+
}
|
|
233
|
+
async lockJob(job, _options) {
|
|
234
|
+
if (!job._id) return void 0;
|
|
235
|
+
const result = await this.db.query(`UPDATE "${this.table}"
|
|
236
|
+
SET locked_at = now(), updated_at = now()
|
|
237
|
+
WHERE id = $1 AND locked_at IS NULL
|
|
238
|
+
RETURNING *`, [String(job._id)]);
|
|
239
|
+
return result.rows[0] ? rowToJob(result.rows[0]) : void 0;
|
|
240
|
+
}
|
|
241
|
+
async unlockJob(job) {
|
|
242
|
+
if (!job._id) return;
|
|
243
|
+
await this.db.query(`UPDATE "${this.table}" SET locked_at = NULL, updated_at = now() WHERE id = $1`, [String(job._id)]);
|
|
244
|
+
}
|
|
245
|
+
async unlockJobs(jobIds) {
|
|
246
|
+
if (!jobIds.length) return;
|
|
247
|
+
await this.db.query(`UPDATE "${this.table}" SET locked_at = NULL, updated_at = now() WHERE id = ANY($1)`, [jobIds.map(String)]);
|
|
248
|
+
}
|
|
249
|
+
async getNextJobToRun(jobName, nextScanAt, lockDeadline, now = /* @__PURE__ */ new Date(), _options) {
|
|
250
|
+
const result = await this.db.query(`WITH candidate AS (
|
|
251
|
+
SELECT id FROM "${this.table}"
|
|
252
|
+
WHERE name = $1
|
|
253
|
+
AND disabled = FALSE
|
|
254
|
+
AND next_run_at <= $2
|
|
255
|
+
AND (locked_at IS NULL OR locked_at <= $3)
|
|
256
|
+
ORDER BY priority DESC, next_run_at ASC
|
|
257
|
+
FOR UPDATE SKIP LOCKED
|
|
258
|
+
LIMIT 1
|
|
259
|
+
)
|
|
260
|
+
UPDATE "${this.table}" t
|
|
261
|
+
SET locked_at = $4, updated_at = now()
|
|
262
|
+
FROM candidate
|
|
263
|
+
WHERE t.id = candidate.id
|
|
264
|
+
RETURNING t.*`, [
|
|
265
|
+
jobName,
|
|
266
|
+
nextScanAt,
|
|
267
|
+
lockDeadline,
|
|
268
|
+
now
|
|
269
|
+
]);
|
|
270
|
+
return result.rows[0] ? rowToJob(result.rows[0]) : void 0;
|
|
271
|
+
}
|
|
272
|
+
async insert(values) {
|
|
273
|
+
const cols = Object.keys(values);
|
|
274
|
+
const placeholders = cols.map((_, i) => `$${i + 1}`);
|
|
275
|
+
return (await this.db.query(`INSERT INTO "${this.table}" (${cols.join(", ")})
|
|
276
|
+
VALUES (${placeholders.join(", ")})
|
|
277
|
+
RETURNING *`, Object.values(values))).rows[0];
|
|
278
|
+
}
|
|
279
|
+
async upsertById(id, values) {
|
|
280
|
+
const setClause = Object.keys(values).map((c, i) => `${c} = $${i + 2}`).join(", ");
|
|
281
|
+
const result = await this.db.query(`UPDATE "${this.table}" SET ${setClause}, updated_at = now() WHERE id = $1 RETURNING *`, [id, ...Object.values(values)]);
|
|
282
|
+
if (result.rows[0]) return result.rows[0];
|
|
283
|
+
return this.insert({
|
|
284
|
+
id,
|
|
285
|
+
...values
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
async upsertByMatch(matchClause, matchParams, values, opts = {}) {
|
|
289
|
+
const existing = await this.db.query(`SELECT * FROM "${this.table}" WHERE ${matchClause} LIMIT 1`, matchParams);
|
|
290
|
+
if (existing.rows[0]) {
|
|
291
|
+
if (opts.insertOnly) return existing.rows[0];
|
|
292
|
+
const setClause = Object.keys(values).map((c, i) => `${c} = $${i + 2}`).join(", ");
|
|
293
|
+
return (await this.db.query(`UPDATE "${this.table}" SET ${setClause}, updated_at = now() WHERE id = $1 RETURNING *`, [existing.rows[0].id, ...Object.values(values)])).rows[0];
|
|
294
|
+
}
|
|
295
|
+
return this.insert(values);
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
//#endregion
|
|
299
|
+
export { PgliteRepository };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "@agendajs/redis-backend";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { routes } from "../endpoints/routes.js";
|
|
2
|
+
import { InferEndpoint } from "../../lib/utils.js";
|
|
3
|
+
import { Endpoint } from "better-call";
|
|
4
|
+
|
|
5
|
+
//#region queue/client/index.d.ts
|
|
6
|
+
interface QueueClientOptions {
|
|
7
|
+
baseURL?: string;
|
|
8
|
+
}
|
|
9
|
+
declare function createQueueClient<TServer extends Record<string, any> = typeof routes>(options?: QueueClientOptions): InferRouteAPI<TServer>;
|
|
10
|
+
type InferRouteAPI<T> = { [K in keyof T]: T[K] extends Endpoint<any, any, any, any, any, any, any, any> ? (request: InferEndpoint<T[K]>["request"]) => Promise<InferEndpoint<T[K]>["response"]> : T[K] extends Record<string, any> ? InferRouteAPI<T[K]> : never };
|
|
11
|
+
//#endregion
|
|
12
|
+
export { InferRouteAPI, QueueClientOptions, createQueueClient };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createFetch } from "../../node_modules/.pnpm/@better-fetch_fetch@1.3.1/node_modules/@better-fetch/fetch/dist/index.js";
|
|
2
|
+
//#region queue/client/index.ts
|
|
3
|
+
function createQueueClient(options) {
|
|
4
|
+
const baseUrl = (options?.baseURL ?? "/queue").replace(/\/+$/, "");
|
|
5
|
+
let queueUrl;
|
|
6
|
+
if (baseUrl === "") queueUrl = "/queue";
|
|
7
|
+
else if (baseUrl.endsWith("/queue")) queueUrl = baseUrl;
|
|
8
|
+
else queueUrl = `${baseUrl}/queue`;
|
|
9
|
+
const supportsCredentials = typeof globalThis.Request !== "undefined" && "credentials" in Request.prototype;
|
|
10
|
+
const $fetch = createFetch({
|
|
11
|
+
baseURL: queueUrl,
|
|
12
|
+
throw: true,
|
|
13
|
+
...supportsCredentials ? { credentials: "include" } : {}
|
|
14
|
+
});
|
|
15
|
+
const createProxy = (path = []) => new Proxy(() => {}, {
|
|
16
|
+
get(_, prop) {
|
|
17
|
+
if (typeof prop !== "string") return;
|
|
18
|
+
if (prop === "then" || prop === "catch" || prop === "finally") return;
|
|
19
|
+
return createProxy([...path, prop]);
|
|
20
|
+
},
|
|
21
|
+
async apply(_, __, args) {
|
|
22
|
+
return $fetch("/" + path.map((segment) => segment.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`)).join("/"), {
|
|
23
|
+
method: "POST",
|
|
24
|
+
body: args[0] ?? {}
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
return createProxy();
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
export { createQueueClient };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import z from "zod";
|
|
2
|
+
|
|
3
|
+
//#region queue/endpoints/api.d.ts
|
|
4
|
+
declare const CreateBillingSchema: z.ZodObject<{
|
|
5
|
+
id: z.ZodString;
|
|
6
|
+
customerId: z.ZodString;
|
|
7
|
+
productId: z.ZodString;
|
|
8
|
+
currentPeriodStart: z.ZodString;
|
|
9
|
+
currentPeriodEnd: z.ZodString;
|
|
10
|
+
interval: z.ZodString;
|
|
11
|
+
billingDetails: z.ZodObject<{
|
|
12
|
+
tokenKey: z.ZodString;
|
|
13
|
+
}, z.core.$strip>;
|
|
14
|
+
retryPolicy: z.ZodTuple<[z.ZodLiteral<"5s">, z.ZodLiteral<"6s">, z.ZodLiteral<"10s">], null>;
|
|
15
|
+
retryCount: z.ZodNumber;
|
|
16
|
+
skipImmediate: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
|
|
17
|
+
}, z.core.$strip>;
|
|
18
|
+
type CreateBillingInfo = z.infer<typeof CreateBillingSchema>;
|
|
19
|
+
declare const createJob: import("better-call").Endpoint<"/create-job", "POST", {
|
|
20
|
+
id: string;
|
|
21
|
+
customerId: string;
|
|
22
|
+
productId: string;
|
|
23
|
+
currentPeriodStart: string;
|
|
24
|
+
currentPeriodEnd: string;
|
|
25
|
+
interval: string;
|
|
26
|
+
billingDetails: {
|
|
27
|
+
tokenKey: string;
|
|
28
|
+
};
|
|
29
|
+
retryPolicy: ["5s", "6s", "10s"];
|
|
30
|
+
retryCount: number;
|
|
31
|
+
skipImmediate?: boolean | undefined;
|
|
32
|
+
}, Record<string, any> | undefined, [], {
|
|
33
|
+
status: string;
|
|
34
|
+
}, {
|
|
35
|
+
scope: "http";
|
|
36
|
+
}, undefined>;
|
|
37
|
+
declare const DeleteBillingSchema: z.ZodObject<{
|
|
38
|
+
id: z.ZodString;
|
|
39
|
+
customerId: z.ZodString;
|
|
40
|
+
productId: z.ZodString;
|
|
41
|
+
}, z.core.$strip>;
|
|
42
|
+
type DeleteBillingInfo = z.infer<typeof DeleteBillingSchema>;
|
|
43
|
+
declare const deleteJob: import("better-call").Endpoint<"/delete-job", "POST", {
|
|
44
|
+
id: string;
|
|
45
|
+
customerId: string;
|
|
46
|
+
productId: string;
|
|
47
|
+
}, Record<string, any> | undefined, [], {
|
|
48
|
+
status: string;
|
|
49
|
+
}, {
|
|
50
|
+
scope: "http";
|
|
51
|
+
}, undefined>;
|
|
52
|
+
//#endregion
|
|
53
|
+
export { CreateBillingInfo, DeleteBillingInfo, createJob, deleteJob };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { __exportAll } from "../../_virtual/_rolldown/runtime.js";
|
|
2
|
+
import { defineQueueMethod } from "../lib/utils.js";
|
|
3
|
+
import z from "zod";
|
|
4
|
+
//#region queue/endpoints/api.ts
|
|
5
|
+
var api_exports = /* @__PURE__ */ __exportAll({
|
|
6
|
+
createJob: () => createJob,
|
|
7
|
+
deleteJob: () => deleteJob
|
|
8
|
+
});
|
|
9
|
+
const createJob = defineQueueMethod({
|
|
10
|
+
input: z.object({
|
|
11
|
+
id: z.string("Subscription identifier is required").min(1).max(70),
|
|
12
|
+
customerId: z.string().min(1).max(70),
|
|
13
|
+
productId: z.string("Product identifier is required").min(1).max(70),
|
|
14
|
+
currentPeriodStart: z.string().min(1).max(40),
|
|
15
|
+
currentPeriodEnd: z.string().min(1).max(40),
|
|
16
|
+
interval: z.string({ error: "product interval is required" }).min(1).max(20),
|
|
17
|
+
billingDetails: z.object({ tokenKey: z.string() }),
|
|
18
|
+
retryPolicy: z.tuple([
|
|
19
|
+
z.literal("5s"),
|
|
20
|
+
z.literal("6s"),
|
|
21
|
+
z.literal("10s")
|
|
22
|
+
]),
|
|
23
|
+
retryCount: z.number(),
|
|
24
|
+
skipImmediate: z.boolean().default(false).optional()
|
|
25
|
+
}),
|
|
26
|
+
route: { path: "/create-job" }
|
|
27
|
+
}, async (ctx) => {
|
|
28
|
+
const input = ctx.input;
|
|
29
|
+
await ctx.queue.billing.subscribe(input);
|
|
30
|
+
return { status: "queued" };
|
|
31
|
+
});
|
|
32
|
+
const deleteJob = defineQueueMethod({
|
|
33
|
+
input: z.object({
|
|
34
|
+
id: z.string(),
|
|
35
|
+
customerId: z.string().min(1).max(70),
|
|
36
|
+
productId: z.string("Product identifier is required").min(1).max(70)
|
|
37
|
+
}),
|
|
38
|
+
route: { path: "/delete-job" }
|
|
39
|
+
}, async (ctx) => {
|
|
40
|
+
const input = ctx.input;
|
|
41
|
+
await ctx.queue.billing.unsubscribe(input);
|
|
42
|
+
return { status: "deleted" };
|
|
43
|
+
});
|
|
44
|
+
//#endregion
|
|
45
|
+
export { api_exports, createJob, deleteJob };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//#region queue/endpoints/routes.d.ts
|
|
2
|
+
declare const routes: {
|
|
3
|
+
readonly createJob: import("better-call").Endpoint<"/create-job", "POST", {
|
|
4
|
+
id: string;
|
|
5
|
+
customerId: string;
|
|
6
|
+
productId: string;
|
|
7
|
+
currentPeriodStart: string;
|
|
8
|
+
currentPeriodEnd: string;
|
|
9
|
+
interval: string;
|
|
10
|
+
billingDetails: {
|
|
11
|
+
tokenKey: string;
|
|
12
|
+
};
|
|
13
|
+
retryPolicy: ["5s", "6s", "10s"];
|
|
14
|
+
retryCount: number;
|
|
15
|
+
skipImmediate?: boolean | undefined;
|
|
16
|
+
}, Record<string, any> | undefined, [], {
|
|
17
|
+
status: string;
|
|
18
|
+
}, {
|
|
19
|
+
scope: "http";
|
|
20
|
+
}, undefined>;
|
|
21
|
+
readonly deleteJob: import("better-call").Endpoint<"/delete-job", "POST", {
|
|
22
|
+
id: string;
|
|
23
|
+
customerId: string;
|
|
24
|
+
productId: string;
|
|
25
|
+
}, Record<string, any> | undefined, [], {
|
|
26
|
+
status: string;
|
|
27
|
+
}, {
|
|
28
|
+
scope: "http";
|
|
29
|
+
}, undefined>;
|
|
30
|
+
};
|
|
31
|
+
//#endregion
|
|
32
|
+
export { routes };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { InferRouteAPI } from "./client/index.js";
|
|
2
|
+
import { PGliteBackend } from "./backends/pglite/backend.js";
|
|
3
|
+
import { index_d_exports } from "./backends/redis/index.js";
|
|
4
|
+
import { Billing } from "./lib/billing.js";
|
|
5
|
+
import { nomkitWebhooks } from "../endpoints/routes.js";
|
|
6
|
+
import { Agenda } from "agenda";
|
|
7
|
+
|
|
8
|
+
//#region queue/init.d.ts
|
|
9
|
+
type Backends = PGliteBackend | index_d_exports.RedisBackend;
|
|
10
|
+
interface QueueContext {
|
|
11
|
+
billing: Billing;
|
|
12
|
+
agenda: Agenda;
|
|
13
|
+
nomkit: InferRouteAPI<typeof nomkitWebhooks>;
|
|
14
|
+
}
|
|
15
|
+
interface CreateQueueOptions {
|
|
16
|
+
backend: Backends;
|
|
17
|
+
nomkitConfig: {
|
|
18
|
+
baseURL: string;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
declare function createQueue(args: CreateQueueOptions): {
|
|
22
|
+
init(): Promise<void>;
|
|
23
|
+
terminate(): Promise<void>;
|
|
24
|
+
handler: (request: Request) => Promise<Response>;
|
|
25
|
+
};
|
|
26
|
+
//#endregion
|
|
27
|
+
export { Backends, CreateQueueOptions, QueueContext, createQueue };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { routes } from "./endpoints/routes.js";
|
|
2
|
+
import { Billing } from "./lib/billing.js";
|
|
3
|
+
import { createNomKitClient } from "../client/index.js";
|
|
4
|
+
import { createRouter } from "better-call";
|
|
5
|
+
import { Agenda } from "agenda";
|
|
6
|
+
//#region queue/init.ts
|
|
7
|
+
function createQueue(args) {
|
|
8
|
+
if (!args) throw new Error("Missing required createQueue arguments");
|
|
9
|
+
if (!args?.nomkitConfig) throw new Error("Missing required nomkit arguments");
|
|
10
|
+
if (!args?.nomkitConfig?.baseURL) throw new Error("Missing required nomkit baseURL");
|
|
11
|
+
const agenda = new Agenda({ backend: args.backend });
|
|
12
|
+
const nomkitAPI = createNomKitClient({ baseURL: args.nomkitConfig.baseURL });
|
|
13
|
+
return {
|
|
14
|
+
async init() {
|
|
15
|
+
return agenda.start();
|
|
16
|
+
},
|
|
17
|
+
async terminate() {
|
|
18
|
+
return agenda.stop(true);
|
|
19
|
+
},
|
|
20
|
+
handler: createRouter(routes, { routerContext: {
|
|
21
|
+
agenda,
|
|
22
|
+
billing: new Billing({
|
|
23
|
+
agenda,
|
|
24
|
+
nomkit: nomkitAPI
|
|
25
|
+
}),
|
|
26
|
+
nomkit: nomkitAPI
|
|
27
|
+
} }).handler
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
export { createQueue };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { CreateBillingInfo, DeleteBillingInfo } from "../endpoints/api.js";
|
|
2
|
+
import { QueueContext } from "../init.js";
|
|
3
|
+
|
|
4
|
+
//#region queue/lib/billing.d.ts
|
|
5
|
+
type Result<T, E = Error> = {
|
|
6
|
+
ok: true;
|
|
7
|
+
value: T;
|
|
8
|
+
error?: never;
|
|
9
|
+
} | {
|
|
10
|
+
ok: false;
|
|
11
|
+
value?: never;
|
|
12
|
+
error: E;
|
|
13
|
+
};
|
|
14
|
+
declare class Billing {
|
|
15
|
+
private hasInit;
|
|
16
|
+
private agenda;
|
|
17
|
+
private nomkit;
|
|
18
|
+
private readonly ID;
|
|
19
|
+
private wf;
|
|
20
|
+
constructor(args: Omit<QueueContext, "billing">);
|
|
21
|
+
subscribe(args: CreateBillingInfo): Promise<void>;
|
|
22
|
+
unsubscribe(args: DeleteBillingInfo): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
export { Billing, Result };
|