Rewrite internal jobs API (#1722)

This commit is contained in:
Mihovil Ilakovac 2024-02-09 12:52:27 +01:00 committed by GitHub
parent 03d234fd7d
commit b873df960d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 463 additions and 164 deletions

View File

@ -74,7 +74,7 @@
"./server/jobs/*": "./dist/server/jobs/*.js",
{=! Used by our code, uncodumented (but accessible) for users. =}
{=! Todo(filip): This export becomes problematic once we start supporting different executors =}
"./server/jobs/pgBoss/types": "./dist/server/jobs/pgBoss/types.js",
"./server/jobs/core/pgBoss": "./dist/server/jobs/core/pgBoss/index.js",
{=! Used by the framework client code, reconsider during refactoring. =}
"./client/webSocket/WebSocketProvider": "./dist/client/webSocket/WebSocketProvider.jsx",

View File

@ -1,14 +1,21 @@
{{={= =}=}}
import { prisma } from 'wasp/server'
import type { JSONValue, JSONObject } from 'wasp/server/_types/serialization'
import { type JobFn } from '{= jobExecutorTypesImportPath =}'
import { type JobFn, createJobDefinition } from '{= jobExecutorImportPath =}'
// PRIVATE API
export const entities = {
const entities = {
{=# entities =}
{= name =}: prisma.{= prismaIdentifier =},
{=/ entities =}
};
}
// PUBLIC API
export type {= typeName =}<Input extends JSONObject, Output extends JSONValue | void> = JobFn<Input, Output, typeof entities>
// PUBLIC API
export const {= jobName =} = createJobDefinition({
jobName: '{= jobName =}',
defaultJobOptions: {=& jobPerformOptions =},
jobSchedule: {=& jobSchedule =},
entities,
})

View File

@ -0,0 +1,3 @@
export { type JobFn } from './types'
export { registerJob, createJobDefinition } from './pgBossJob.js'
export { startPgBoss } from './pgBoss.js'

View File

@ -24,6 +24,7 @@ function createPgBoss() {
let resolvePgBossStarted: (boss: PgBoss) => void
let rejectPgBossStarted: (boss: PgBoss) => void
// PRIVATE API
// Code that wants to access pg-boss must wait until it has been started.
export const pgBossStarted = new Promise<PgBoss>((resolve, reject) => {
resolvePgBossStarted = resolve
@ -39,6 +40,7 @@ enum PgBossStatus {
let pgBossStatus: PgBossStatus = PgBossStatus.Unstarted
// PRIVATE API
/**
* Prepares the target PostgreSQL database and begins job monitoring.
* If the required database objects do not exist in the specified database,

View File

@ -3,40 +3,61 @@ import { pgBossStarted } from './pgBoss.js'
import { Job, SubmittedJob } from '../job.js'
import type { JSONValue, JSONObject } from 'wasp/server/_types/serialization'
import { PrismaDelegate } from 'wasp/server/_types'
import type { JobFn } from 'wasp/server/jobs/pgBoss/types'
import type { JobFn } from 'wasp/server/jobs/core/pgBoss'
export const PG_BOSS_EXECUTOR_NAME = Symbol('PgBoss')
type JobSchedule = {
cron: Parameters<PgBoss['schedule']>[1]
args: Parameters<PgBoss['schedule']>[2]
options: Parameters<PgBoss['schedule']>[3]
}
// PRIVATE API
/**
* Creates an instance of PgBossJob and initializes the PgBoss executor by registering this job function.
* We expect this to be called once per job name. If called multiple times with the same name and different
* functions, we will override the previous calls.
* Creates an instance of PgBossJob which contains all of the necessary
* information to submit a job to pg-boss.
*/
export function createJob<
export function createJobDefinition<
Input extends JSONObject,
Output extends JSONValue | void,
Entities extends Partial<PrismaDelegate>
>({
jobName,
jobFn,
defaultJobOptions,
jobSchedule,
entities,
}: {
// jobName - The user-defined job name in their .wasp file.
jobName: Parameters<PgBoss['schedule']>[0]
// jobFn - The user-defined async job callback function.
jobFn: JobFn<Input, Output, Entities>
// defaultJobOptions - pg-boss specific options for `boss.send()` applied to every `submit()` invocation,
// which can overriden in that call.
defaultJobOptions: PgBoss.Schedule['options']
jobSchedule?: {
cron: Parameters<PgBoss['schedule']>[1]
args: Parameters<PgBoss['schedule']>[2]
options: Parameters<PgBoss['schedule']>[3]
}
jobSchedule: JobSchedule | null
// Entities used by job, passed into callback context.
entities: Entities
}) {
return new PgBossJob<Input, Output, Entities>(
jobName,
defaultJobOptions,
entities,
jobSchedule,
)
}
// PRIVATE API
/**
* Uses the info about a job in PgBossJob to register a user defined job handler with pg-boss.
* We expect this to be called once per job name. If called multiple times with the same name and different
* functions, we will override the previous calls.
*/
export function registerJob<
Input extends JSONObject,
Output extends JSONValue | void,
Entities extends Partial<PrismaDelegate>
>({ job, jobFn }: {
job: PgBossJob<Input, Output, Entities>,
jobFn: JobFn<Input, Output, Entities>,
}) {
// NOTE(shayne): We are not awaiting `pgBossStarted` here since we need to return an instance to the job
// template, or else the NodeJS module bootstrapping process will block and fail as it would then depend
@ -49,33 +70,31 @@ export function createJob<
pgBossStarted.then(async (boss) => {
// As a safety precaution against undefined behavior of registering different
// functions for the same job name, remove all registered functions first.
await boss.offWork(jobName)
await boss.offWork(job.jobName)
// This tells pg-boss to run given worker function when job with that name is submitted.
// Ref: https://github.com/timgit/pg-boss/blob/master/docs/readme.md#work
await boss.work<Input, Output>(
jobName,
pgBossCallbackWrapper<Input, Output, Entities>(jobFn, entities)
job.jobName,
pgBossCallbackWrapper<Input, Output, Entities>(jobFn, job.entities)
)
// If a job schedule is provided, we should schedule the recurring job.
// If the schedule name already exists, it's updated to the provided cron expression, arguments, and options.
// Ref: https://github.com/timgit/pg-boss/blob/master/docs/readme.md#scheduling
if (jobSchedule) {
if (job.jobSchedule) {
const options: PgBoss.ScheduleOptions = {
...defaultJobOptions,
...jobSchedule.options,
...job.defaultJobOptions,
...job.jobSchedule.options,
}
await boss.schedule(
jobName,
jobSchedule.cron,
jobSchedule.args || null,
job.jobName,
job.jobSchedule.cron,
job.jobSchedule.args || null,
options
)
}
})
return new PgBossJob<Input, Output>(jobName, defaultJobOptions)
}
/**
@ -85,24 +104,33 @@ export function createJob<
*/
class PgBossJob<
Input extends JSONObject,
Output extends JSONValue | void
Output extends JSONValue | void,
Entities extends Partial<PrismaDelegate>
> extends Job {
public readonly defaultJobOptions: Parameters<PgBoss['send']>[2]
public readonly startAfter: number | string | Date
public readonly entities: Entities
public readonly jobSchedule: JobSchedule | null
constructor(
jobName: string,
defaultJobOptions: Parameters<PgBoss['send']>[2],
entities: Entities,
jobSchedule: JobSchedule | null,
startAfter?: number | string | Date
) {
super(jobName, PG_BOSS_EXECUTOR_NAME)
this.defaultJobOptions = defaultJobOptions
this.entities = entities
this.jobSchedule = jobSchedule
this.startAfter = startAfter
}
delay(startAfter: number | string | Date) {
return new PgBossJob<Input, Output>(
return new PgBossJob<Input, Output, Entities>(
this.jobName,
this.defaultJobOptions,
this.entities,
this.jobSchedule,
startAfter
)
}
@ -113,7 +141,7 @@ class PgBossJob<
...(this.startAfter && { startAfter: this.startAfter }),
...jobOptions,
})
return new PgBossSubmittedJob<Input, Output>(boss, this, jobId)
return new PgBossSubmittedJob<Input, Output, Entities>(boss, this, jobId)
}
}
@ -122,7 +150,8 @@ class PgBossJob<
*/
class PgBossSubmittedJob<
Input extends JSONObject,
Output extends JSONValue | void
Output extends JSONValue | void,
Entities extends Partial<PrismaDelegate>
> extends SubmittedJob {
public readonly pgBoss: {
readonly cancel: () => ReturnType<PgBoss['cancel']>
@ -132,7 +161,7 @@ class PgBossSubmittedJob<
constructor(
boss: PgBoss,
job: PgBossJob<Input, Output>,
job: PgBossJob<Input, Output, Entities>,
jobId: SubmittedJob['jobId']
) {
super(job, jobId)

View File

@ -1,5 +1,5 @@
{{={= =}=}}
{=# jobs =}
export type { {= typeName =} } from './{= jobName =}'
export { type {= typeName =}, {= jobName =} } from './{= jobName =}.js'
{=/ jobs =}

View File

@ -1,12 +1,9 @@
{{={= =}=}}
import { createJob } from './{= jobExecutorRelativePath =}'
{=& jobEntitiesImportStatement =}
{=& jobPerformFnImportStatement =}
import { registerJob } from '{= jobExecutorImportPath =}'
{=& jobPerformFn.importStatement =}
{=& jobDefinition.importStatement =}
export const {= jobName =} = createJob({
jobName: "{= jobName =}",
jobFn: {= jobPerformFnName =},
defaultJobOptions: {=& jobPerformOptions =},
jobSchedule: {=& jobSchedule =},
{= jobEntitiesIdentifier =},
registerJob({
job: {= jobDefinition.importIdentifier =},
jobFn: {= jobPerformFn.importIdentifier =},
})

View File

@ -1,8 +1,8 @@
{{={= =}=}}
// This module exports all jobs and is imported by the server to ensure
// This module imports all jobs and is imported by the server to ensure
// any schedules that are not referenced are still loaded by NodeJS.
{=# jobs =}
export { {= name =} } from '../{= name =}.js'
import '../{= name =}.js'
{=/ jobs =}

View File

@ -11,7 +11,7 @@ import { ServerSetupFnContext } from 'wasp/server/types'
{=/ setupFn.isDefined =}
{=# isPgBossJobExecutorUsed =}
import { startPgBoss } from './jobs/core/pgBoss/pgBoss.js'
import { startPgBoss } from 'wasp/server/jobs/core/pgBoss'
import './jobs/core/allJobs.js'
{=/ isPgBossJobExecutorUsed =}

View File

@ -151,7 +151,7 @@ action deleteTasks {
entities: [Task],
}
job PrintTimeAndNumberOfTasks {
job printTimeAndNumberOfTasks {
executor: PgBoss,
perform: {
fn: import { printTimeAndNumberOfTasks } from "@src/jobs/print"

View File

@ -40,6 +40,7 @@
"lucia": "^3.0.0-beta.14",
"mitt": "3.0.0",
"msw": "^1.1.0",
"pg-boss": "^8.4.2",
"prisma": "4.16.2",
"react": "^18.2.0",
"react-hook-form": "^7.45.4",
@ -1425,6 +1426,18 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/aggregate-error": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
"integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
"dependencies": {
"clean-stack": "^2.0.0",
"indent-string": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/ansi-escapes": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
@ -1645,6 +1658,14 @@
"version": "1.0.1",
"license": "BSD-3-Clause"
},
"node_modules/buffer-writer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz",
"integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==",
"engines": {
"node": ">=4"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"license": "MIT",
@ -1743,6 +1764,14 @@
"fsevents": "~2.3.2"
}
},
"node_modules/clean-stack": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
"integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
"engines": {
"node": ">=6"
}
},
"node_modules/cli-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
@ -1888,6 +1917,17 @@
"node": ">= 0.10"
}
},
"node_modules/cron-parser": {
"version": "4.9.0",
"resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz",
"integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==",
"dependencies": {
"luxon": "^3.2.1"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@ -2032,6 +2072,17 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/delay": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz",
"integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"license": "MIT",
@ -3824,6 +3875,11 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="
},
"node_modules/lodash.includes": {
"version": "4.3.0",
"license": "MIT"
@ -3911,6 +3967,14 @@
"oslo": "^0.27.0"
}
},
"node_modules/luxon": {
"version": "3.4.4",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz",
"integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==",
"engines": {
"node": ">=12"
}
},
"node_modules/lz-string": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
@ -4383,6 +4447,25 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-map": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
"integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
"dependencies": {
"aggregate-error": "^3.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/packet-reader": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz",
"integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ=="
},
"node_modules/parse5": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
@ -4426,6 +4509,106 @@
"node": "*"
}
},
"node_modules/pg": {
"version": "8.11.3",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz",
"integrity": "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==",
"dependencies": {
"buffer-writer": "2.0.0",
"packet-reader": "1.0.0",
"pg-connection-string": "^2.6.2",
"pg-pool": "^3.6.1",
"pg-protocol": "^1.6.0",
"pg-types": "^2.1.0",
"pgpass": "1.x"
},
"engines": {
"node": ">= 8.0.0"
},
"optionalDependencies": {
"pg-cloudflare": "^1.1.1"
},
"peerDependencies": {
"pg-native": ">=3.0.1"
},
"peerDependenciesMeta": {
"pg-native": {
"optional": true
}
}
},
"node_modules/pg-boss": {
"version": "8.4.2",
"resolved": "https://registry.npmjs.org/pg-boss/-/pg-boss-8.4.2.tgz",
"integrity": "sha512-xcl/G8C7qlCyrcvlQvgLVBIe68zO0XfZc6K86/G9fq/mL+YQMEo1spW6lHqsPpNi2KGlpXwBEL/XZxkMa19eRA==",
"dependencies": {
"cron-parser": "^4.0.0",
"delay": "^5.0.0",
"lodash.debounce": "^4.0.8",
"p-map": "^4.0.0",
"pg": "^8.5.1",
"serialize-error": "^8.1.0",
"uuid": "^9.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/pg-cloudflare": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz",
"integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==",
"optional": true
},
"node_modules/pg-connection-string": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz",
"integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA=="
},
"node_modules/pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/pg-pool": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz",
"integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==",
"peerDependencies": {
"pg": ">=8.0"
}
},
"node_modules/pg-protocol": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz",
"integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q=="
},
"node_modules/pg-types": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
"dependencies": {
"pg-int8": "1.0.1",
"postgres-array": "~2.0.0",
"postgres-bytea": "~1.0.0",
"postgres-date": "~1.0.4",
"postgres-interval": "^1.1.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/pgpass": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
"dependencies": {
"split2": "^4.1.0"
}
},
"node_modules/picocolors": {
"version": "1.0.0",
"license": "ISC"
@ -4477,6 +4660,41 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
"engines": {
"node": ">=4"
}
},
"node_modules/postgres-bytea": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-date": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-interval": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"dependencies": {
"xtend": "^4.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/pretty-format": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
@ -4933,6 +5151,31 @@
"version": "2.1.3",
"license": "MIT"
},
"node_modules/serialize-error": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-8.1.0.tgz",
"integrity": "sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==",
"dependencies": {
"type-fest": "^0.20.2"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/serialize-error/node_modules/type-fest": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/serve-static": {
"version": "1.15.0",
"license": "MIT",
@ -5203,6 +5446,14 @@
"node": ">=0.10.0"
}
},
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
"engines": {
"node": ">= 10.x"
}
},
"node_modules/stackback": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
@ -5535,6 +5786,18 @@
"node": ">= 0.4.0"
}
},
"node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/value-equal": {
"version": "1.0.1",
"license": "MIT"
@ -6186,6 +6449,14 @@
"node": ">=0.4.0"
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"engines": {
"node": ">=0.4"
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",

View File

@ -6,6 +6,7 @@ import type {
} from 'wasp/server/operations'
import type { Task } from 'wasp/entities'
import { emailSender } from 'wasp/server/email'
import { printTimeAndNumberOfTasks } from 'wasp/server/jobs'
type CreateArgs = Pick<Task, 'description'>
@ -17,6 +18,9 @@ export const createTask: CreateTask<CreateArgs, Task> = async (
throw new HttpError(401)
}
console.log("Executing 'printTimeAndNumberOfTasks' task.")
await printTimeAndNumberOfTasks.submit({})
emailSender.send({
to: 'test@test.com',
from: {

View File

@ -47,7 +47,7 @@ import Wasp.Generator.SdkGenerator.CrudG (genCrud)
import Wasp.Generator.SdkGenerator.Server.AuthG (genNewServerApi)
import Wasp.Generator.SdkGenerator.Server.CrudG (genNewServerCrudApi)
import Wasp.Generator.SdkGenerator.Server.EmailSenderG (depsRequiredByEmail, genNewEmailSenderApi)
import Wasp.Generator.SdkGenerator.Server.JobGenerator (genNewJobsApi)
import Wasp.Generator.SdkGenerator.Server.JobGenerator (depsRequiredByJobs, genNewJobsApi)
import qualified Wasp.Generator.SdkGenerator.Server.OperationsGenerator as ServerOpsGen
import Wasp.Generator.SdkGenerator.ServerApiG (genServerApi)
import Wasp.Generator.SdkGenerator.WebSocketGenerator (depsRequiredByWebSockets, genWebSockets)
@ -86,7 +86,6 @@ genSdkReal spec =
genFileCopy [relfile|server/index.ts|],
genFileCopy [relfile|server/HttpError.ts|],
genFileCopy [relfile|types/index.ts|],
genFileCopy [relfile|server/jobs/pgBoss/types.ts|],
genFileCopy [relfile|client/test/vitest/helpers.tsx|],
genFileCopy [relfile|client/test/index.ts|],
genServerConfigFile spec,
@ -224,7 +223,8 @@ npmDepsForSdk spec =
++ ServerAuthG.depsRequiredByAuth spec
++ depsRequiredByEmail spec
++ depsRequiredByWebSockets spec
++ depsRequiredForTesting,
++ depsRequiredForTesting
++ depsRequiredByJobs spec,
N.devDependencies =
AS.Dependency.fromList
[ ("@tsconfig/node" <> majorNodeVersionStr, "latest")

View File

@ -1,24 +1,33 @@
module Wasp.Generator.SdkGenerator.Server.JobGenerator
( genNewJobsApi,
getImportPathForJobName,
getJobExecutorTypesImportPath,
genJobExecutors,
depsRequiredByJobs,
getJobExecutorImportPath,
getImportJsonForJobDefinition,
)
where
import Data.Aeson (object, (.=))
import Data.Maybe (fromJust)
import qualified Data.Aeson as Aeson
import qualified Data.Aeson.Text as Aeson.Text
import Data.Maybe (fromJust, fromMaybe)
import StrongPath (File', Path, Posix, Rel, reldir, relfile, relfileP, (</>))
import qualified StrongPath as SP
import StrongPath.TH (reldirP)
import Wasp.AppSpec (AppSpec, getJobs)
import qualified Wasp.AppSpec as AS
import Wasp.AppSpec.Job (Job, JobExecutor (PgBoss))
import qualified Wasp.AppSpec.App.Dependency as AS.Dependency
import qualified Wasp.AppSpec.JSON as AS.JSON
import Wasp.AppSpec.Job (Job, JobExecutor (PgBoss), jobExecutors)
import qualified Wasp.AppSpec.Job as J
import Wasp.AppSpec.Util (isPgBossJobExecutorUsed)
import Wasp.Generator.Common (makeJsonWithEntityData)
import Wasp.Generator.FileDraft (FileDraft)
import qualified Wasp.Generator.JsImport as GJI
import Wasp.Generator.Monad (Generator)
import Wasp.Generator.SdkGenerator.Common (makeSdkImportPath)
import qualified Wasp.Generator.SdkGenerator.Common as C
import qualified Wasp.JsImport as JI
import qualified Wasp.SemanticVersion as SV
import Wasp.Util
genNewJobsApi :: AppSpec -> Generator [FileDraft]
@ -29,7 +38,8 @@ genNewJobsApi spec =
sequence
[ genIndexTs jobs
]
<++> mapM genJobType jobs
<++> mapM genJob jobs
<++> genJobExecutors spec
genIndexTs :: [(String, Job)] -> Generator FileDraft
genIndexTs jobs = return $ C.mkTmplFdWithData tmplFile tmplData
@ -42,29 +52,81 @@ genIndexTs jobs = return $ C.mkTmplFdWithData tmplFile tmplData
"jobName" .= jobName
]
genJobType :: (String, Job) -> Generator FileDraft
genJobType (jobName, job) =
genJob :: (String, Job) -> Generator FileDraft
genJob (jobName, job) =
return $
C.mkTmplFdWithDstAndData
tmplFile
dstFile
$ Just tmplData
where
tmplFile = [relfile|server/jobs/_jobTypes.ts|]
tmplFile = [relfile|server/jobs/_job.ts|]
dstFile = [reldir|server/jobs|] </> fromJust (SP.parseRelFile $ jobName ++ ".ts")
tmplData =
object
[ "typeName" .= toUpperFirst jobName,
"jobExecutorTypesImportPath" .= SP.fromRelFileP jobExecutorTypesImportPath,
"entities" .= maybe [] (map (makeJsonWithEntityData . AS.refName)) (J.entities job)
[ "jobName" .= jobName,
"typeName" .= toUpperFirst jobName,
"jobExecutorImportPath" .= SP.fromRelFileP jobExecutorImportPath,
"entities" .= maybe [] (map (makeJsonWithEntityData . AS.refName)) (J.entities job),
-- NOTE: You cannot directly input an Aeson.object for Mustache to substitute.
-- This is why we must get a text representation of the object, either by
-- `Aeson.Text.encodeToLazyText` on an Aeson.Object, or `show` on an AS.JSON.
"jobSchedule" .= Aeson.Text.encodeToLazyText (fromMaybe Aeson.Null maybeJobSchedule),
"jobPerformOptions" .= show (fromMaybe AS.JSON.emptyObject maybeJobPerformOptions)
]
jobExecutorTypesImportPath = getJobExecutorTypesImportPath (J.executor job)
jobExecutorImportPath = getJobExecutorImportPath (J.executor job)
getImportPathForJobName :: String -> Path Posix (Rel d) File'
getImportPathForJobName jobName = makeSdkImportPath $ [reldirP|server/jobs|] </> fromJust (SP.parseRelFileP jobName)
maybeJobPerformOptions = J.performExecutorOptionsJson job
jobScheduleTmplData s =
object
[ "cron" .= J.cron s,
"args" .= J.args s,
"options" .= fromMaybe AS.JSON.emptyObject (J.scheduleExecutorOptionsJson job)
]
maybeJobSchedule = jobScheduleTmplData <$> J.schedule job
-- | We are importing relevant types per executor e.g. JobFn, this functions maps
-- the executor to the import path of the relevant types.
getJobExecutorTypesImportPath :: JobExecutor -> Path Posix (Rel r) File'
getJobExecutorTypesImportPath PgBoss = makeSdkImportPath [relfileP|server/jobs/pgBoss/types|]
-- | We are importing relevant functions and types per executor e.g. JobFn or registerJob,
-- this functions maps the executor to the import path from SDK.
getJobExecutorImportPath :: JobExecutor -> Path Posix (Rel r) File'
getJobExecutorImportPath PgBoss = makeSdkImportPath [relfileP|server/jobs/core/pgBoss|]
getImportJsonForJobDefinition :: String -> Aeson.Value
getImportJsonForJobDefinition jobName =
GJI.jsImportToImportJson $
Just $
JI.JsImport
{ JI._path = JI.ModuleImportPath $ makeSdkImportPath [relfileP|server/jobs|],
JI._name = JI.JsImportField jobName,
-- NOTE: We are using alias to avoid name conflicts with user defined imports.
JI._importAlias = Just "_waspJobDefinition"
}
genJobExecutors :: AppSpec -> Generator [FileDraft]
genJobExecutors spec = case getJobs spec of
[] -> return []
_someJobs ->
return $
C.mkTmplFd [relfile|server/jobs/core/job.ts|] : genAllJobExecutors
where
genAllJobExecutors = concatMap genJobExecutor jobExecutors
-- Per each defined job executor, we generate the needed files.
genJobExecutor :: JobExecutor -> [FileDraft]
genJobExecutor PgBoss =
[ C.mkTmplFd [relfile|server/jobs/core/pgBoss/pgBoss.ts|],
C.mkTmplFd [relfile|server/jobs/core/pgBoss/pgBossJob.ts|],
C.mkTmplFd [relfile|server/jobs/core/pgBoss/types.ts|],
C.mkTmplFd [relfile|server/jobs/core/pgBoss/index.ts|]
]
-- NOTE: Our pg-boss related documentation references this version in URLs.
-- Please update the docs when this changes (until we solve: https://github.com/wasp-lang/wasp/issues/596).
pgBossVersionRange :: SV.Range
pgBossVersionRange = SV.Range [SV.backwardsCompatibleWith (SV.Version 8 4 2)]
pgBossDependency :: AS.Dependency.Dependency
pgBossDependency = AS.Dependency.make ("pg-boss", show pgBossVersionRange)
depsRequiredByJobs :: AppSpec -> [AS.Dependency.Dependency]
depsRequiredByJobs spec = [pgBossDependency | isPgBossJobExecutorUsed spec]

View File

@ -51,7 +51,7 @@ import Wasp.Generator.ServerGenerator.AuthG (genAuth)
import qualified Wasp.Generator.ServerGenerator.Common as C
import Wasp.Generator.ServerGenerator.CrudG (genCrud)
import Wasp.Generator.ServerGenerator.Db.Seed (genDbSeed, getDbSeeds, getPackageJsonPrismaSeedField)
import Wasp.Generator.ServerGenerator.JobGenerator (depsRequiredByJobs, genJobExecutors, genJobs)
import Wasp.Generator.ServerGenerator.JobGenerator (genJobs)
import Wasp.Generator.ServerGenerator.JsImport (extImportToImportJson, getAliasedJsImportStmtAndIdentifier)
import Wasp.Generator.ServerGenerator.OperationsG (genOperations)
import Wasp.Generator.ServerGenerator.OperationsRoutesG (genOperationsRoutes)
@ -75,7 +75,6 @@ genServer spec =
<++> genSrcDir spec
<++> genDotEnv spec
<++> genJobs spec
<++> genJobExecutors spec
<++> genPatches spec
<++> genEnvValidationScript
<++> genApis spec
@ -164,7 +163,6 @@ npmDepsForWasp spec =
("superjson", "^1.12.2")
]
++ depsRequiredByPassport spec
++ depsRequiredByJobs spec
++ depsRequiredByWebSockets spec,
N.waspDevDependencies =
AS.Dependency.fromList

View File

@ -1,19 +1,13 @@
module Wasp.Generator.ServerGenerator.JobGenerator
( genJobs,
genJobExecutors,
pgBossVersionRange,
pgBossDependency,
depsRequiredByJobs,
)
where
import Data.Aeson (object, (.=))
import qualified Data.Aeson as Aeson
import qualified Data.Aeson.Text as Aeson.Text
import Data.Maybe (fromJust, fromMaybe)
import Data.Maybe (fromJust)
import StrongPath
( Dir,
File',
Path,
Path',
Posix,
@ -21,81 +15,50 @@ import StrongPath
reldir,
reldirP,
relfile,
toFilePath,
(</>),
)
import qualified StrongPath as SP
import Wasp.AppSpec (AppSpec, getJobs)
import qualified Wasp.AppSpec.App.Dependency as AS.Dependency
import qualified Wasp.AppSpec.JSON as AS.JSON
import Wasp.AppSpec.Job (Job, JobExecutor (PgBoss), jobExecutors)
import Wasp.AppSpec.Job (Job)
import qualified Wasp.AppSpec.Job as J
import Wasp.AppSpec.Util (isPgBossJobExecutorUsed)
import Wasp.Generator.Common (ServerRootDir)
import Wasp.Generator.FileDraft (FileDraft)
import Wasp.Generator.Monad (Generator)
import Wasp.Generator.SdkGenerator.Server.JobGenerator (getImportPathForJobName, getJobExecutorTypesImportPath)
import Wasp.Generator.SdkGenerator.Server.JobGenerator
( getImportJsonForJobDefinition,
getJobExecutorImportPath,
)
import Wasp.Generator.ServerGenerator.Common
( ServerTemplatesDir,
srcDirInServerTemplatesDir,
)
import qualified Wasp.Generator.ServerGenerator.Common as C
import qualified Wasp.Generator.ServerGenerator.JsImport as SJI
import Wasp.JsImport (JsImportName (JsImportField), JsImportPath (ModuleImportPath), makeJsImport)
import qualified Wasp.JsImport as JI
import qualified Wasp.SemanticVersion as SV
import Wasp.Util (toUpperFirst)
genJobs :: AppSpec -> Generator [FileDraft]
genJobs spec = case getJobs spec of
[] -> return []
jobs -> return $ genAllJobImports spec : (genJob <$> jobs)
jobs -> return $ genAllJobImports spec : (genRegisterJob <$> jobs)
genJob :: (String, Job) -> FileDraft
genJob (jobName, job) =
genRegisterJob :: (String, Job) -> FileDraft
genRegisterJob (jobName, job) =
C.mkTmplFdWithDstAndData
tmplFile
dstFile
( Just $
object
[ "jobName" .= jobName,
"typeName" .= toUpperFirst jobName,
"jobPerformFnName" .= jobPerformFnName,
"jobPerformFnImportStatement" .= jobPerformFnImportStatement,
-- NOTE: You cannot directly input an Aeson.object for Mustache to substitute.
-- This is why we must get a text representation of the object, either by
-- `Aeson.Text.encodeToLazyText` on an Aeson.Object, or `show` on an AS.JSON.
"jobSchedule" .= Aeson.Text.encodeToLazyText (fromMaybe Aeson.Null maybeJobSchedule),
"jobPerformOptions" .= show (fromMaybe AS.JSON.emptyObject maybeJobPerformOptions),
"jobExecutorRelativePath" .= toFilePath (executorJobTemplateInJobsDir "js" (J.executor job)),
"jobExecutorTypesImportPath" .= SP.fromRelFileP jobExecutorTypesImportPath,
"jobEntitiesImportStatement" .= jobEntitiesImportStatement,
"jobEntitiesIdentifier" .= jobEntitiesIdentifier
[ "jobPerformFn" .= jobPerformFn,
"jobExecutorImportPath" .= SP.fromRelFileP (getJobExecutorImportPath (J.executor job)),
"jobDefinition" .= getImportJsonForJobDefinition jobName
]
)
where
tmplFile = C.asTmplFile $ jobsDirInServerTemplatesDir </> [relfile|_job.ts|]
dstFile = jobsDirInServerRootDir </> fromJust (SP.parseRelFile $ jobName ++ ".ts")
-- Users import job types from the SDK, so the types for each job are generated
-- separately and imported from the SDK.
(jobEntitiesImportStatement, jobEntitiesIdentifier) =
JI.getJsImportStmtAndIdentifier $
makeJsImport (ModuleImportPath $ getImportPathForJobName jobName) (JsImportField "entities")
(jobPerformFnImportStatement, jobPerformFnName) =
SJI.getJsImportStmtAndIdentifier relPathFromJobsDirToServerSrcDir $ (J.fn . J.perform) job
maybeJobPerformOptions = J.performExecutorOptionsJson job
jobScheduleTmplData s =
object
[ "cron" .= J.cron s,
"args" .= J.args s,
"options" .= fromMaybe AS.JSON.emptyObject (J.scheduleExecutorOptionsJson job)
]
maybeJobSchedule = jobScheduleTmplData <$> J.schedule job
jobExecutorTypesImportPath = getJobExecutorTypesImportPath (J.executor job)
jobPerformFn =
SJI.extImportToImportJson relPathFromJobsDirToServerSrcDir $
Just $ (J.fn . J.perform) job
relPathFromJobsDirToServerSrcDir :: Path Posix (Rel importLocation) (Dir C.ServerSrcDir)
relPathFromJobsDirToServerSrcDir = [reldirP|../|]
@ -104,7 +67,7 @@ genJob (jobName, job) =
-- even if they are not referenced by user code. This ensures schedules are started, etc.
genAllJobImports :: AppSpec -> FileDraft
genAllJobImports spec =
let tmplFile = C.asTmplFile $ jobsDirInServerTemplatesDir </> [relfile|core/_allJobs.ts|]
let tmplFile = C.asTmplFile $ jobsDirInServerTemplatesDir </> [relfile|core/allJobs.ts|]
dstFile = jobsDirInServerRootDir </> [relfile|core/allJobs.ts|]
in C.mkTmplFdWithDstAndData
tmplFile
@ -115,50 +78,13 @@ genAllJobImports spec =
)
where
buildJobInfo :: String -> Aeson.Value
buildJobInfo jobName =
object
[ "name" .= jobName
]
genJobExecutors :: AppSpec -> Generator [FileDraft]
genJobExecutors spec = case getJobs spec of
[] -> return []
_someJobs -> return $ jobExecutorFds ++ jobExecutorHelperFds
where
jobExecutorFds :: [FileDraft]
jobExecutorFds = genJobExecutor <$> jobExecutors
genJobExecutor :: JobExecutor -> FileDraft
genJobExecutor jobExecutor = C.mkTmplFd $ executorJobTemplateInServerTemplatesDir jobExecutor
jobExecutorHelperFds :: [FileDraft]
jobExecutorHelperFds =
[ C.mkTmplFd $ jobsDirInServerTemplatesDir </> [relfile|core/pgBoss/pgBoss.ts|],
C.mkTmplFd $ jobsDirInServerTemplatesDir </> [relfile|core/job.ts|]
]
executorJobTemplateInServerTemplatesDir :: JobExecutor -> Path SP.System (Rel ServerTemplatesDir) File'
executorJobTemplateInServerTemplatesDir = (jobsDirInServerTemplatesDir </>) . executorJobTemplateInJobsDir "ts"
buildJobInfo jobName = object ["name" .= jobName]
data JobsDir
jobsDirInServerTemplatesDir :: Path' (Rel ServerTemplatesDir) (Dir JobsDir)
jobsDirInServerTemplatesDir = srcDirInServerTemplatesDir </> [reldir|jobs|]
executorJobTemplateInJobsDir :: String -> JobExecutor -> Path' (Rel JobsDir) File'
executorJobTemplateInJobsDir ext PgBoss = fromJust $ SP.parseRelFile $ "core/pgBoss/pgBossJob" <> "." <> ext
-- Path to destination files are the same as in templates dir.
jobsDirInServerRootDir :: Path' (Rel ServerRootDir) (Dir JobsDir)
jobsDirInServerRootDir = SP.castRel jobsDirInServerTemplatesDir
-- NOTE: Our pg-boss related documentation references this version in URLs.
-- Please update the docs when this changes (until we solve: https://github.com/wasp-lang/wasp/issues/596).
pgBossVersionRange :: SV.Range
pgBossVersionRange = SV.Range [SV.backwardsCompatibleWith (SV.Version 8 4 2)]
pgBossDependency :: AS.Dependency.Dependency
pgBossDependency = AS.Dependency.make ("pg-boss", show pgBossVersionRange)
depsRequiredByJobs :: AppSpec -> [AS.Dependency.Dependency]
depsRequiredByJobs spec = [pgBossDependency | isPgBossJobExecutorUsed spec]