@geekmidas/constructs 10.0.0-alpha.54 → 10.0.0-alpha.55
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/dist/{HonoEndpointAdaptor-ZDmoZ5LJ.cjs → HonoEndpointAdaptor-XyxwoFnE.cjs} +4 -3
- package/dist/HonoEndpointAdaptor-XyxwoFnE.cjs.map +1 -0
- package/dist/{HonoEndpointAdaptor-DW4q9db0.mjs → HonoEndpointAdaptor-ja49poBI.mjs} +4 -3
- package/dist/HonoEndpointAdaptor-ja49poBI.mjs.map +1 -0
- package/dist/adaptors/aws.cjs +10 -4
- package/dist/adaptors/aws.cjs.map +1 -1
- package/dist/adaptors/aws.d.cts.map +1 -1
- package/dist/adaptors/aws.d.mts.map +1 -1
- package/dist/adaptors/aws.mjs +10 -4
- package/dist/adaptors/aws.mjs.map +1 -1
- package/dist/adaptors/hono.cjs +1 -1
- package/dist/adaptors/hono.d.cts.map +1 -1
- package/dist/adaptors/hono.d.mts.map +1 -1
- package/dist/adaptors/hono.mjs +1 -1
- package/dist/adaptors/testing.cjs +14 -6
- package/dist/adaptors/testing.cjs.map +1 -1
- package/dist/adaptors/testing.d.cts +1 -1
- package/dist/adaptors/testing.d.cts.map +1 -1
- package/dist/adaptors/testing.d.mts +1 -1
- package/dist/adaptors/testing.d.mts.map +1 -1
- package/dist/adaptors/testing.mjs +13 -5
- package/dist/adaptors/testing.mjs.map +1 -1
- package/dist/crons/index.cjs +3 -2
- package/dist/crons/index.cjs.map +1 -1
- package/dist/crons/index.mjs +3 -2
- package/dist/crons/index.mjs.map +1 -1
- package/dist/database/kysely.cjs +3 -205
- package/dist/database/kysely.d.cts +2 -2
- package/dist/database/kysely.d.mts +2 -2
- package/dist/database/kysely.mjs +2 -203
- package/dist/index.cjs +3 -0
- package/dist/index.d.cts +23 -1
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +23 -1
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2 -1
- package/dist/{kysely-DTnkZsub.d.cts → kysely-BEm5sDlU.d.cts} +16 -2
- package/dist/kysely-BEm5sDlU.d.cts.map +1 -0
- package/dist/kysely-BRzzawr2.cjs +301 -0
- package/dist/kysely-BRzzawr2.cjs.map +1 -0
- package/dist/kysely-BYfM6Zby.mjs +288 -0
- package/dist/kysely-BYfM6Zby.mjs.map +1 -0
- package/dist/{kysely-Bqov3fwv.d.mts → kysely-CjuvdXEV.d.mts} +16 -2
- package/dist/kysely-CjuvdXEV.d.mts.map +1 -0
- package/dist/shutdown-BMJmAhSt.cjs +38 -0
- package/dist/shutdown-BMJmAhSt.cjs.map +1 -0
- package/dist/shutdown-C7xa8x9S.mjs +27 -0
- package/dist/shutdown-C7xa8x9S.mjs.map +1 -0
- package/dist/subscribers/index.cjs +2 -1
- package/dist/subscribers/index.cjs.map +1 -1
- package/dist/subscribers/index.mjs +2 -1
- package/dist/subscribers/index.mjs.map +1 -1
- package/package.json +31 -31
- package/dist/HonoEndpointAdaptor-DW4q9db0.mjs.map +0 -1
- package/dist/HonoEndpointAdaptor-ZDmoZ5LJ.cjs.map +0 -1
- package/dist/database/kysely.cjs.map +0 -1
- package/dist/database/kysely.mjs.map +0 -1
- package/dist/kysely-Bqov3fwv.d.mts.map +0 -1
- package/dist/kysely-DTnkZsub.d.cts.map +0 -1
package/dist/crons/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["CronExpressionParser","runWithRequestContext","publishEvents"],"sources":["../../src/crons/inProcess.ts","../../src/crons/runCron.ts","../../src/crons/schedule.ts"],"sourcesContent":["import { CronExpressionParser } from 'cron-parser';\n\n/**\n * The longest delay a timer takes. Node clamps anything longer to 1ms, so a\n * monthly cron would fire at once and then every millisecond.\n */\nconst MAX_DELAY = 2 ** 31 - 1;\n\n/** A schedule that is running, and the way to stop it. */\nexport interface InProcessSchedule {\n\tstop(): void;\n}\n\n/**\n * Run `fire` on a cron expression, in this process, by timer.\n *\n * For the one process that has no broker to keep a schedule: `gkm dev` on a\n * target whose crons are infrastructure once deployed — an EventBridge rule on\n * AWS. Deployed, a timer is the wrong tool: each replica would fire every job\n * once, and nothing would say so. That is why a server schedules through\n * pg-boss, and why this is never what a deployed process does.\n *\n * In UTC, as the deployed schedule is. The next firing is worked out from the\n * clock after each run rather than read off a sequence, so a run that outlasts\n * its interval skips what it overran instead of firing them back to back.\n */\nexport function scheduleInProcess(\n\texpression: string,\n\tfire: () => Promise<unknown> | unknown,\n): InProcessSchedule {\n\t// Parsed once up front, so a bad expression throws here — to the caller\n\t// scheduling it — rather than inside a timer.\n\tCronExpressionParser.parse(expression, { tz: 'UTC' });\n\n\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\tlet stopped = false;\n\n\tconst arm = (at: number) => {\n\t\tif (stopped) return;\n\t\tconst delay = at - Date.now();\n\t\ttimer = setTimeout(\n\t\t\tasync () => {\n\t\t\t\tif (Date.now() < at) return arm(at);\n\t\t\t\tawait fire();\n\t\t\t\tarm(nextAfter(expression, new Date()));\n\t\t\t},\n\t\t\tMath.min(Math.max(delay, 0), MAX_DELAY),\n\t\t);\n\t\t// The server keeps the process alive; a schedule alone should not.\n\t\ttimer.unref?.();\n\t};\n\n\tarm(nextAfter(expression, new Date()));\n\n\treturn {\n\t\tstop() {\n\t\t\tstopped = true;\n\t\t\tclearTimeout(timer);\n\t\t},\n\t};\n}\n\nfunction nextAfter(expression: string, from: Date): number {\n\treturn CronExpressionParser.parse(expression, {\n\t\ttz: 'UTC',\n\t\tcurrentDate: from,\n\t})\n\t\t.next()\n\t\t.getTime();\n}\n","import type { AuditStorage } from '@geekmidas/audit';\nimport {\n\trunWithRequestContext,\n\ttype ServiceDiscovery,\n} from '@geekmidas/services';\nimport { publishEvents } from '../publisher';\nimport type { Cron } from './Cron';\n\n/**\n * Run a cron once, in this process: what an EventBridge rule's invocation does\n * on Lambda, for a server that schedules its own.\n *\n * The same steps as the Lambda adaptor — its services, its worker's database\n * as `db`, an auditor when it declared one, its output parsed, its events\n * published — because a cron that ran differently on a server than deployed\n * would pass locally and fail where it matters. Services come through the\n * discovery, which keeps one instance of each, so running every minute does not\n * reconnect a database every minute.\n */\nexport async function runCron(\n\tcron: Cron<any, any, any, any, any, any>,\n\tserviceDiscovery: ServiceDiscovery<any>,\n): Promise<unknown> {\n\tconst services =\n\t\tcron.services.length > 0\n\t\t\t? await serviceDiscovery.register(cron.services)\n\t\t\t: {};\n\n\tconst db = cron.databaseService\n\t\t? (await serviceDiscovery.register([cron.databaseService]))[\n\t\t\t\tcron.databaseService.serviceName\n\t\t\t]\n\t\t: undefined;\n\n\t// Only imported for a cron that audits: `@geekmidas/audit` is optional.\n\tlet auditor: { flush(): Promise<void> } | undefined;\n\tif (cron.auditorStorageService) {\n\t\tconst { DefaultAuditor } = await import('@geekmidas/audit');\n\t\tconst registered: Record<string, unknown> = await serviceDiscovery.register(\n\t\t\t[cron.auditorStorageService],\n\t\t);\n\t\tconst storage = registered[\n\t\t\tcron.auditorStorageService.serviceName\n\t\t] as AuditStorage;\n\t\tauditor = new DefaultAuditor({\n\t\t\tactor: { id: 'system', type: 'system' },\n\t\t\tstorage,\n\t\t\tmetadata: { function: cron.type },\n\t\t});\n\t}\n\n\tconst logger = cron.logger;\n\tconst requestId = `cron-${Date.now()}`;\n\n\treturn runWithRequestContext(\n\t\t{ logger, requestId, startTime: Date.now() },\n\t\tasync () => {\n\t\t\tconst response = await cron.fn({\n\t\t\t\tinput: undefined,\n\t\t\t\tservices,\n\t\t\t\tlogger,\n\t\t\t\tdb,\n\t\t\t\tauditor,\n\t\t\t} as any);\n\t\t\tconst output = await cron.parseOutput(response);\n\n\t\t\tawait auditor?.flush();\n\t\t\t// The cron's output schema is erased by the `any` it is typed with here.\n\t\t\tawait publishEvents(\n\t\t\t\tlogger,\n\t\t\t\tserviceDiscovery,\n\t\t\t\tcron.events,\n\t\t\t\toutput as never,\n\t\t\t);\n\n\t\t\treturn output;\n\t\t},\n\t);\n}\n","import type { ScheduleExpression } from './Cron';\n\n/**\n * A schedule a cron runner can take, as standard five-field cron.\n *\n * `ScheduleExpression` has two spellings. `cron(m h dom mon dow)` is already\n * five-field cron and unwraps; `rate(n unit)` is an interval, and an interval\n * is not a schedule — it says how often, where cron says when.\n *\n * Most intervals still have an exact cron equivalent, because they divide their\n * unit evenly: `rate(5 minutes)` is every fifth minute of every hour, and that\n * is `*/5 * * * *` precisely. `rate(7 hours)` has none. There is no cron field\n * that means \"every seven hours\" — `*/7` in the hour field fires at 0, 7, 14\n * and 21 and then starts over at midnight, so the gap between the last firing\n * of one day and the first of the next is three hours rather than seven.\n *\n * Those are refused rather than rounded. A job that runs at the wrong hour is\n * far harder to notice than one that refused to build, and a schedule quietly\n * adjusted by the framework is a bug nobody goes looking for.\n */\nexport class UnrepresentableSchedule extends Error {\n\tconstructor(\n\t\treadonly schedule: string,\n\t\treason: string,\n\t) {\n\t\tsuper(\n\t\t\t`\\`${schedule}\\` has no exact cron equivalent: ${reason}.\\n\\n` +\n\t\t\t\t'Cron says *when*, not *how often*, so an interval is only exact when ' +\n\t\t\t\t'it divides its unit evenly.\\n' +\n\t\t\t\t'Write the schedule you mean instead — `cron(0 */6 * * *)` for every ' +\n\t\t\t\t'sixth hour, say — or pick an interval that divides: minutes into 60, ' +\n\t\t\t\t'hours into 24.',\n\t\t);\n\t\tthis.name = 'UnrepresentableSchedule';\n\t}\n}\n\n/** Units `rate(…)` accepts, singular or plural. */\nconst UNITS = ['minute', 'hour', 'day'] as const;\ntype Unit = (typeof UNITS)[number];\n\n/**\n * Standard five-field cron for a schedule expression.\n *\n * @throws {UnrepresentableSchedule} when a `rate(…)` has no exact equivalent.\n */\nexport function toCronExpression(schedule: ScheduleExpression): string {\n\tconst cron = /^cron\\((.+)\\)$/.exec(schedule);\n\tif (cron?.[1]) return cron[1].trim();\n\n\tconst rate = /^rate\\(\\s*(\\d+)\\s+(\\w+?)s?\\s*\\)$/.exec(schedule);\n\tif (!rate) {\n\t\tthrow new UnrepresentableSchedule(\n\t\t\tschedule,\n\t\t\t'it is neither `cron(…)` nor `rate(n unit)`',\n\t\t);\n\t}\n\n\tconst every = Number(rate[1]);\n\tconst unit = rate[2] as Unit;\n\t/* c8 ignore next */ if (!unit)\n\t\tthrow new UnrepresentableSchedule(schedule, 'no unit');\n\n\tif (!UNITS.includes(unit)) {\n\t\tthrow new UnrepresentableSchedule(\n\t\t\tschedule,\n\t\t\t`\\`${unit}\\` is not one of ${UNITS.join(', ')}`,\n\t\t);\n\t}\n\n\tif (every < 1) {\n\t\tthrow new UnrepresentableSchedule(\n\t\t\tschedule,\n\t\t\t'an interval must be at least 1',\n\t\t);\n\t}\n\n\tswitch (unit) {\n\t\tcase 'minute':\n\t\t\tif (every === 1) return '* * * * *';\n\t\t\tif (60 % every !== 0) {\n\t\t\t\tthrow new UnrepresentableSchedule(\n\t\t\t\t\tschedule,\n\t\t\t\t\t`${every} does not divide 60, so the interval would reset at the top of each hour`,\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn `*/${every} * * * *`;\n\n\t\tcase 'hour':\n\t\t\tif (every === 1) return '0 * * * *';\n\t\t\tif (24 % every !== 0) {\n\t\t\t\tthrow new UnrepresentableSchedule(\n\t\t\t\t\tschedule,\n\t\t\t\t\t`${every} does not divide 24, so the interval would reset at midnight`,\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn `0 */${every} * * *`;\n\n\t\tcase 'day':\n\t\t\tif (every === 1) return '0 0 * * *';\n\t\t\t// `*/n` in day-of-month restarts on the 1st, so a 2-day interval fires\n\t\t\t// on the 1st and 3rd of a 31-day month and then again on the 1st — one\n\t\t\t// day later, not two. No month length makes that come out even.\n\t\t\tthrow new UnrepresentableSchedule(\n\t\t\t\tschedule,\n\t\t\t\t'day-of-month restarts every month, so no multi-day interval is even',\n\t\t\t);\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["CronExpressionParser","runWithRequestContext","publishEvents"],"sources":["../../src/crons/inProcess.ts","../../src/crons/runCron.ts","../../src/crons/schedule.ts"],"sourcesContent":["import { CronExpressionParser } from 'cron-parser';\n\n/**\n * The longest delay a timer takes. Node clamps anything longer to 1ms, so a\n * monthly cron would fire at once and then every millisecond.\n */\nconst MAX_DELAY = 2 ** 31 - 1;\n\n/** A schedule that is running, and the way to stop it. */\nexport interface InProcessSchedule {\n\tstop(): void;\n}\n\n/**\n * Run `fire` on a cron expression, in this process, by timer.\n *\n * For the one process that has no broker to keep a schedule: `gkm dev` on a\n * target whose crons are infrastructure once deployed — an EventBridge rule on\n * AWS. Deployed, a timer is the wrong tool: each replica would fire every job\n * once, and nothing would say so. That is why a server schedules through\n * pg-boss, and why this is never what a deployed process does.\n *\n * In UTC, as the deployed schedule is. The next firing is worked out from the\n * clock after each run rather than read off a sequence, so a run that outlasts\n * its interval skips what it overran instead of firing them back to back.\n */\nexport function scheduleInProcess(\n\texpression: string,\n\tfire: () => Promise<unknown> | unknown,\n): InProcessSchedule {\n\t// Parsed once up front, so a bad expression throws here — to the caller\n\t// scheduling it — rather than inside a timer.\n\tCronExpressionParser.parse(expression, { tz: 'UTC' });\n\n\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\tlet stopped = false;\n\n\tconst arm = (at: number) => {\n\t\tif (stopped) return;\n\t\tconst delay = at - Date.now();\n\t\ttimer = setTimeout(\n\t\t\tasync () => {\n\t\t\t\tif (Date.now() < at) return arm(at);\n\t\t\t\tawait fire();\n\t\t\t\tarm(nextAfter(expression, new Date()));\n\t\t\t},\n\t\t\tMath.min(Math.max(delay, 0), MAX_DELAY),\n\t\t);\n\t\t// The server keeps the process alive; a schedule alone should not.\n\t\ttimer.unref?.();\n\t};\n\n\tarm(nextAfter(expression, new Date()));\n\n\treturn {\n\t\tstop() {\n\t\t\tstopped = true;\n\t\t\tclearTimeout(timer);\n\t\t},\n\t};\n}\n\nfunction nextAfter(expression: string, from: Date): number {\n\treturn CronExpressionParser.parse(expression, {\n\t\ttz: 'UTC',\n\t\tcurrentDate: from,\n\t})\n\t\t.next()\n\t\t.getTime();\n}\n","import type { AuditStorage } from '@geekmidas/audit';\nimport {\n\trunWithRequestContext,\n\ttype ServiceDiscovery,\n} from '@geekmidas/services';\nimport { publishEvents } from '../publisher';\nimport type { Cron } from './Cron';\n\n/**\n * Run a cron once, in this process: what an EventBridge rule's invocation does\n * on Lambda, for a server that schedules its own.\n *\n * The same steps as the Lambda adaptor — its services, its worker's database\n * as `db`, an auditor when it declared one, its output parsed, its events\n * published — because a cron that ran differently on a server than deployed\n * would pass locally and fail where it matters. Services come through the\n * discovery, which keeps one instance of each, so running every minute does not\n * reconnect a database every minute.\n */\nexport async function runCron(\n\tcron: Cron<any, any, any, any, any, any>,\n\tserviceDiscovery: ServiceDiscovery<any>,\n): Promise<unknown> {\n\tconst services =\n\t\tcron.services.length > 0\n\t\t\t? await serviceDiscovery.register(cron.services)\n\t\t\t: {};\n\n\tconst db = cron.databaseService\n\t\t? (await serviceDiscovery.register([cron.databaseService]))[\n\t\t\t\tcron.databaseService.serviceName\n\t\t\t]\n\t\t: undefined;\n\n\t// Only imported for a cron that audits: `@geekmidas/audit` is optional.\n\tlet auditor: { flush(): Promise<void> } | undefined;\n\tif (cron.auditorStorageService) {\n\t\tconst { DefaultAuditor } = await import('@geekmidas/audit');\n\t\tconst registered: Record<string, unknown> = await serviceDiscovery.register(\n\t\t\t[cron.auditorStorageService],\n\t\t);\n\t\tconst storage = registered[\n\t\t\tcron.auditorStorageService.serviceName\n\t\t] as AuditStorage;\n\t\tauditor = new DefaultAuditor({\n\t\t\tactor: { id: 'system', type: 'system' },\n\t\t\tstorage,\n\t\t\tmetadata: { function: cron.type },\n\t\t});\n\t}\n\n\tconst logger = cron.logger;\n\tconst requestId = `cron-${Date.now()}`;\n\n\treturn runWithRequestContext(\n\t\t{\n\t\t\tlogger,\n\t\t\trequestId,\n\t\t\tstartTime: Date.now(),\n\t\t\toperation: `cron ${cron.schedule ?? 'manual'}`,\n\t\t},\n\t\tasync () => {\n\t\t\tconst response = await cron.fn({\n\t\t\t\tinput: undefined,\n\t\t\t\tservices,\n\t\t\t\tlogger,\n\t\t\t\tdb,\n\t\t\t\tauditor,\n\t\t\t} as any);\n\t\t\tconst output = await cron.parseOutput(response);\n\n\t\t\tawait auditor?.flush();\n\t\t\t// The cron's output schema is erased by the `any` it is typed with here.\n\t\t\tawait publishEvents(\n\t\t\t\tlogger,\n\t\t\t\tserviceDiscovery,\n\t\t\t\tcron.events,\n\t\t\t\toutput as never,\n\t\t\t);\n\n\t\t\treturn output;\n\t\t},\n\t);\n}\n","import type { ScheduleExpression } from './Cron';\n\n/**\n * A schedule a cron runner can take, as standard five-field cron.\n *\n * `ScheduleExpression` has two spellings. `cron(m h dom mon dow)` is already\n * five-field cron and unwraps; `rate(n unit)` is an interval, and an interval\n * is not a schedule — it says how often, where cron says when.\n *\n * Most intervals still have an exact cron equivalent, because they divide their\n * unit evenly: `rate(5 minutes)` is every fifth minute of every hour, and that\n * is `*/5 * * * *` precisely. `rate(7 hours)` has none. There is no cron field\n * that means \"every seven hours\" — `*/7` in the hour field fires at 0, 7, 14\n * and 21 and then starts over at midnight, so the gap between the last firing\n * of one day and the first of the next is three hours rather than seven.\n *\n * Those are refused rather than rounded. A job that runs at the wrong hour is\n * far harder to notice than one that refused to build, and a schedule quietly\n * adjusted by the framework is a bug nobody goes looking for.\n */\nexport class UnrepresentableSchedule extends Error {\n\tconstructor(\n\t\treadonly schedule: string,\n\t\treason: string,\n\t) {\n\t\tsuper(\n\t\t\t`\\`${schedule}\\` has no exact cron equivalent: ${reason}.\\n\\n` +\n\t\t\t\t'Cron says *when*, not *how often*, so an interval is only exact when ' +\n\t\t\t\t'it divides its unit evenly.\\n' +\n\t\t\t\t'Write the schedule you mean instead — `cron(0 */6 * * *)` for every ' +\n\t\t\t\t'sixth hour, say — or pick an interval that divides: minutes into 60, ' +\n\t\t\t\t'hours into 24.',\n\t\t);\n\t\tthis.name = 'UnrepresentableSchedule';\n\t}\n}\n\n/** Units `rate(…)` accepts, singular or plural. */\nconst UNITS = ['minute', 'hour', 'day'] as const;\ntype Unit = (typeof UNITS)[number];\n\n/**\n * Standard five-field cron for a schedule expression.\n *\n * @throws {UnrepresentableSchedule} when a `rate(…)` has no exact equivalent.\n */\nexport function toCronExpression(schedule: ScheduleExpression): string {\n\tconst cron = /^cron\\((.+)\\)$/.exec(schedule);\n\tif (cron?.[1]) return cron[1].trim();\n\n\tconst rate = /^rate\\(\\s*(\\d+)\\s+(\\w+?)s?\\s*\\)$/.exec(schedule);\n\tif (!rate) {\n\t\tthrow new UnrepresentableSchedule(\n\t\t\tschedule,\n\t\t\t'it is neither `cron(…)` nor `rate(n unit)`',\n\t\t);\n\t}\n\n\tconst every = Number(rate[1]);\n\tconst unit = rate[2] as Unit;\n\t/* c8 ignore next */ if (!unit)\n\t\tthrow new UnrepresentableSchedule(schedule, 'no unit');\n\n\tif (!UNITS.includes(unit)) {\n\t\tthrow new UnrepresentableSchedule(\n\t\t\tschedule,\n\t\t\t`\\`${unit}\\` is not one of ${UNITS.join(', ')}`,\n\t\t);\n\t}\n\n\tif (every < 1) {\n\t\tthrow new UnrepresentableSchedule(\n\t\t\tschedule,\n\t\t\t'an interval must be at least 1',\n\t\t);\n\t}\n\n\tswitch (unit) {\n\t\tcase 'minute':\n\t\t\tif (every === 1) return '* * * * *';\n\t\t\tif (60 % every !== 0) {\n\t\t\t\tthrow new UnrepresentableSchedule(\n\t\t\t\t\tschedule,\n\t\t\t\t\t`${every} does not divide 60, so the interval would reset at the top of each hour`,\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn `*/${every} * * * *`;\n\n\t\tcase 'hour':\n\t\t\tif (every === 1) return '0 * * * *';\n\t\t\tif (24 % every !== 0) {\n\t\t\t\tthrow new UnrepresentableSchedule(\n\t\t\t\t\tschedule,\n\t\t\t\t\t`${every} does not divide 24, so the interval would reset at midnight`,\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn `0 */${every} * * *`;\n\n\t\tcase 'day':\n\t\t\tif (every === 1) return '0 0 * * *';\n\t\t\t// `*/n` in day-of-month restarts on the 1st, so a 2-day interval fires\n\t\t\t// on the 1st and 3rd of a 31-day month and then again on the 1st — one\n\t\t\t// day later, not two. No month length makes that come out even.\n\t\t\tthrow new UnrepresentableSchedule(\n\t\t\t\tschedule,\n\t\t\t\t'day-of-month restarts every month, so no multi-day interval is even',\n\t\t\t);\n\t}\n}\n"],"mappings":"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"}
|
package/dist/crons/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { a as publishEvents } from "../functions-Di6n5ltG.mjs";
|
|
2
2
|
import { n as Cron, t as CronBuilder } from "../CronBuilder-CCgt0wCE.mjs";
|
|
3
|
-
import { CronExpressionParser } from "cron-parser";
|
|
4
3
|
import { runWithRequestContext } from "@geekmidas/services";
|
|
4
|
+
import { CronExpressionParser } from "cron-parser";
|
|
5
5
|
//#region src/crons/inProcess.ts
|
|
6
6
|
/**
|
|
7
7
|
* The longest delay a timer takes. Node clamps anything longer to 1ms, so a
|
|
@@ -81,7 +81,8 @@ async function runCron(cron, serviceDiscovery) {
|
|
|
81
81
|
return runWithRequestContext({
|
|
82
82
|
logger,
|
|
83
83
|
requestId,
|
|
84
|
-
startTime: Date.now()
|
|
84
|
+
startTime: Date.now(),
|
|
85
|
+
operation: `cron ${cron.schedule ?? "manual"}`
|
|
85
86
|
}, async () => {
|
|
86
87
|
const response = await cron.fn({
|
|
87
88
|
input: void 0,
|
package/dist/crons/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/crons/inProcess.ts","../../src/crons/runCron.ts","../../src/crons/schedule.ts"],"sourcesContent":["import { CronExpressionParser } from 'cron-parser';\n\n/**\n * The longest delay a timer takes. Node clamps anything longer to 1ms, so a\n * monthly cron would fire at once and then every millisecond.\n */\nconst MAX_DELAY = 2 ** 31 - 1;\n\n/** A schedule that is running, and the way to stop it. */\nexport interface InProcessSchedule {\n\tstop(): void;\n}\n\n/**\n * Run `fire` on a cron expression, in this process, by timer.\n *\n * For the one process that has no broker to keep a schedule: `gkm dev` on a\n * target whose crons are infrastructure once deployed — an EventBridge rule on\n * AWS. Deployed, a timer is the wrong tool: each replica would fire every job\n * once, and nothing would say so. That is why a server schedules through\n * pg-boss, and why this is never what a deployed process does.\n *\n * In UTC, as the deployed schedule is. The next firing is worked out from the\n * clock after each run rather than read off a sequence, so a run that outlasts\n * its interval skips what it overran instead of firing them back to back.\n */\nexport function scheduleInProcess(\n\texpression: string,\n\tfire: () => Promise<unknown> | unknown,\n): InProcessSchedule {\n\t// Parsed once up front, so a bad expression throws here — to the caller\n\t// scheduling it — rather than inside a timer.\n\tCronExpressionParser.parse(expression, { tz: 'UTC' });\n\n\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\tlet stopped = false;\n\n\tconst arm = (at: number) => {\n\t\tif (stopped) return;\n\t\tconst delay = at - Date.now();\n\t\ttimer = setTimeout(\n\t\t\tasync () => {\n\t\t\t\tif (Date.now() < at) return arm(at);\n\t\t\t\tawait fire();\n\t\t\t\tarm(nextAfter(expression, new Date()));\n\t\t\t},\n\t\t\tMath.min(Math.max(delay, 0), MAX_DELAY),\n\t\t);\n\t\t// The server keeps the process alive; a schedule alone should not.\n\t\ttimer.unref?.();\n\t};\n\n\tarm(nextAfter(expression, new Date()));\n\n\treturn {\n\t\tstop() {\n\t\t\tstopped = true;\n\t\t\tclearTimeout(timer);\n\t\t},\n\t};\n}\n\nfunction nextAfter(expression: string, from: Date): number {\n\treturn CronExpressionParser.parse(expression, {\n\t\ttz: 'UTC',\n\t\tcurrentDate: from,\n\t})\n\t\t.next()\n\t\t.getTime();\n}\n","import type { AuditStorage } from '@geekmidas/audit';\nimport {\n\trunWithRequestContext,\n\ttype ServiceDiscovery,\n} from '@geekmidas/services';\nimport { publishEvents } from '../publisher';\nimport type { Cron } from './Cron';\n\n/**\n * Run a cron once, in this process: what an EventBridge rule's invocation does\n * on Lambda, for a server that schedules its own.\n *\n * The same steps as the Lambda adaptor — its services, its worker's database\n * as `db`, an auditor when it declared one, its output parsed, its events\n * published — because a cron that ran differently on a server than deployed\n * would pass locally and fail where it matters. Services come through the\n * discovery, which keeps one instance of each, so running every minute does not\n * reconnect a database every minute.\n */\nexport async function runCron(\n\tcron: Cron<any, any, any, any, any, any>,\n\tserviceDiscovery: ServiceDiscovery<any>,\n): Promise<unknown> {\n\tconst services =\n\t\tcron.services.length > 0\n\t\t\t? await serviceDiscovery.register(cron.services)\n\t\t\t: {};\n\n\tconst db = cron.databaseService\n\t\t? (await serviceDiscovery.register([cron.databaseService]))[\n\t\t\t\tcron.databaseService.serviceName\n\t\t\t]\n\t\t: undefined;\n\n\t// Only imported for a cron that audits: `@geekmidas/audit` is optional.\n\tlet auditor: { flush(): Promise<void> } | undefined;\n\tif (cron.auditorStorageService) {\n\t\tconst { DefaultAuditor } = await import('@geekmidas/audit');\n\t\tconst registered: Record<string, unknown> = await serviceDiscovery.register(\n\t\t\t[cron.auditorStorageService],\n\t\t);\n\t\tconst storage = registered[\n\t\t\tcron.auditorStorageService.serviceName\n\t\t] as AuditStorage;\n\t\tauditor = new DefaultAuditor({\n\t\t\tactor: { id: 'system', type: 'system' },\n\t\t\tstorage,\n\t\t\tmetadata: { function: cron.type },\n\t\t});\n\t}\n\n\tconst logger = cron.logger;\n\tconst requestId = `cron-${Date.now()}`;\n\n\treturn runWithRequestContext(\n\t\t{ logger, requestId, startTime: Date.now() },\n\t\tasync () => {\n\t\t\tconst response = await cron.fn({\n\t\t\t\tinput: undefined,\n\t\t\t\tservices,\n\t\t\t\tlogger,\n\t\t\t\tdb,\n\t\t\t\tauditor,\n\t\t\t} as any);\n\t\t\tconst output = await cron.parseOutput(response);\n\n\t\t\tawait auditor?.flush();\n\t\t\t// The cron's output schema is erased by the `any` it is typed with here.\n\t\t\tawait publishEvents(\n\t\t\t\tlogger,\n\t\t\t\tserviceDiscovery,\n\t\t\t\tcron.events,\n\t\t\t\toutput as never,\n\t\t\t);\n\n\t\t\treturn output;\n\t\t},\n\t);\n}\n","import type { ScheduleExpression } from './Cron';\n\n/**\n * A schedule a cron runner can take, as standard five-field cron.\n *\n * `ScheduleExpression` has two spellings. `cron(m h dom mon dow)` is already\n * five-field cron and unwraps; `rate(n unit)` is an interval, and an interval\n * is not a schedule — it says how often, where cron says when.\n *\n * Most intervals still have an exact cron equivalent, because they divide their\n * unit evenly: `rate(5 minutes)` is every fifth minute of every hour, and that\n * is `*/5 * * * *` precisely. `rate(7 hours)` has none. There is no cron field\n * that means \"every seven hours\" — `*/7` in the hour field fires at 0, 7, 14\n * and 21 and then starts over at midnight, so the gap between the last firing\n * of one day and the first of the next is three hours rather than seven.\n *\n * Those are refused rather than rounded. A job that runs at the wrong hour is\n * far harder to notice than one that refused to build, and a schedule quietly\n * adjusted by the framework is a bug nobody goes looking for.\n */\nexport class UnrepresentableSchedule extends Error {\n\tconstructor(\n\t\treadonly schedule: string,\n\t\treason: string,\n\t) {\n\t\tsuper(\n\t\t\t`\\`${schedule}\\` has no exact cron equivalent: ${reason}.\\n\\n` +\n\t\t\t\t'Cron says *when*, not *how often*, so an interval is only exact when ' +\n\t\t\t\t'it divides its unit evenly.\\n' +\n\t\t\t\t'Write the schedule you mean instead — `cron(0 */6 * * *)` for every ' +\n\t\t\t\t'sixth hour, say — or pick an interval that divides: minutes into 60, ' +\n\t\t\t\t'hours into 24.',\n\t\t);\n\t\tthis.name = 'UnrepresentableSchedule';\n\t}\n}\n\n/** Units `rate(…)` accepts, singular or plural. */\nconst UNITS = ['minute', 'hour', 'day'] as const;\ntype Unit = (typeof UNITS)[number];\n\n/**\n * Standard five-field cron for a schedule expression.\n *\n * @throws {UnrepresentableSchedule} when a `rate(…)` has no exact equivalent.\n */\nexport function toCronExpression(schedule: ScheduleExpression): string {\n\tconst cron = /^cron\\((.+)\\)$/.exec(schedule);\n\tif (cron?.[1]) return cron[1].trim();\n\n\tconst rate = /^rate\\(\\s*(\\d+)\\s+(\\w+?)s?\\s*\\)$/.exec(schedule);\n\tif (!rate) {\n\t\tthrow new UnrepresentableSchedule(\n\t\t\tschedule,\n\t\t\t'it is neither `cron(…)` nor `rate(n unit)`',\n\t\t);\n\t}\n\n\tconst every = Number(rate[1]);\n\tconst unit = rate[2] as Unit;\n\t/* c8 ignore next */ if (!unit)\n\t\tthrow new UnrepresentableSchedule(schedule, 'no unit');\n\n\tif (!UNITS.includes(unit)) {\n\t\tthrow new UnrepresentableSchedule(\n\t\t\tschedule,\n\t\t\t`\\`${unit}\\` is not one of ${UNITS.join(', ')}`,\n\t\t);\n\t}\n\n\tif (every < 1) {\n\t\tthrow new UnrepresentableSchedule(\n\t\t\tschedule,\n\t\t\t'an interval must be at least 1',\n\t\t);\n\t}\n\n\tswitch (unit) {\n\t\tcase 'minute':\n\t\t\tif (every === 1) return '* * * * *';\n\t\t\tif (60 % every !== 0) {\n\t\t\t\tthrow new UnrepresentableSchedule(\n\t\t\t\t\tschedule,\n\t\t\t\t\t`${every} does not divide 60, so the interval would reset at the top of each hour`,\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn `*/${every} * * * *`;\n\n\t\tcase 'hour':\n\t\t\tif (every === 1) return '0 * * * *';\n\t\t\tif (24 % every !== 0) {\n\t\t\t\tthrow new UnrepresentableSchedule(\n\t\t\t\t\tschedule,\n\t\t\t\t\t`${every} does not divide 24, so the interval would reset at midnight`,\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn `0 */${every} * * *`;\n\n\t\tcase 'day':\n\t\t\tif (every === 1) return '0 0 * * *';\n\t\t\t// `*/n` in day-of-month restarts on the 1st, so a 2-day interval fires\n\t\t\t// on the 1st and 3rd of a 31-day month and then again on the 1st — one\n\t\t\t// day later, not two. No month length makes that come out even.\n\t\t\tthrow new UnrepresentableSchedule(\n\t\t\t\tschedule,\n\t\t\t\t'day-of-month restarts every month, so no multi-day interval is even',\n\t\t\t);\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/crons/inProcess.ts","../../src/crons/runCron.ts","../../src/crons/schedule.ts"],"sourcesContent":["import { CronExpressionParser } from 'cron-parser';\n\n/**\n * The longest delay a timer takes. Node clamps anything longer to 1ms, so a\n * monthly cron would fire at once and then every millisecond.\n */\nconst MAX_DELAY = 2 ** 31 - 1;\n\n/** A schedule that is running, and the way to stop it. */\nexport interface InProcessSchedule {\n\tstop(): void;\n}\n\n/**\n * Run `fire` on a cron expression, in this process, by timer.\n *\n * For the one process that has no broker to keep a schedule: `gkm dev` on a\n * target whose crons are infrastructure once deployed — an EventBridge rule on\n * AWS. Deployed, a timer is the wrong tool: each replica would fire every job\n * once, and nothing would say so. That is why a server schedules through\n * pg-boss, and why this is never what a deployed process does.\n *\n * In UTC, as the deployed schedule is. The next firing is worked out from the\n * clock after each run rather than read off a sequence, so a run that outlasts\n * its interval skips what it overran instead of firing them back to back.\n */\nexport function scheduleInProcess(\n\texpression: string,\n\tfire: () => Promise<unknown> | unknown,\n): InProcessSchedule {\n\t// Parsed once up front, so a bad expression throws here — to the caller\n\t// scheduling it — rather than inside a timer.\n\tCronExpressionParser.parse(expression, { tz: 'UTC' });\n\n\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\tlet stopped = false;\n\n\tconst arm = (at: number) => {\n\t\tif (stopped) return;\n\t\tconst delay = at - Date.now();\n\t\ttimer = setTimeout(\n\t\t\tasync () => {\n\t\t\t\tif (Date.now() < at) return arm(at);\n\t\t\t\tawait fire();\n\t\t\t\tarm(nextAfter(expression, new Date()));\n\t\t\t},\n\t\t\tMath.min(Math.max(delay, 0), MAX_DELAY),\n\t\t);\n\t\t// The server keeps the process alive; a schedule alone should not.\n\t\ttimer.unref?.();\n\t};\n\n\tarm(nextAfter(expression, new Date()));\n\n\treturn {\n\t\tstop() {\n\t\t\tstopped = true;\n\t\t\tclearTimeout(timer);\n\t\t},\n\t};\n}\n\nfunction nextAfter(expression: string, from: Date): number {\n\treturn CronExpressionParser.parse(expression, {\n\t\ttz: 'UTC',\n\t\tcurrentDate: from,\n\t})\n\t\t.next()\n\t\t.getTime();\n}\n","import type { AuditStorage } from '@geekmidas/audit';\nimport {\n\trunWithRequestContext,\n\ttype ServiceDiscovery,\n} from '@geekmidas/services';\nimport { publishEvents } from '../publisher';\nimport type { Cron } from './Cron';\n\n/**\n * Run a cron once, in this process: what an EventBridge rule's invocation does\n * on Lambda, for a server that schedules its own.\n *\n * The same steps as the Lambda adaptor — its services, its worker's database\n * as `db`, an auditor when it declared one, its output parsed, its events\n * published — because a cron that ran differently on a server than deployed\n * would pass locally and fail where it matters. Services come through the\n * discovery, which keeps one instance of each, so running every minute does not\n * reconnect a database every minute.\n */\nexport async function runCron(\n\tcron: Cron<any, any, any, any, any, any>,\n\tserviceDiscovery: ServiceDiscovery<any>,\n): Promise<unknown> {\n\tconst services =\n\t\tcron.services.length > 0\n\t\t\t? await serviceDiscovery.register(cron.services)\n\t\t\t: {};\n\n\tconst db = cron.databaseService\n\t\t? (await serviceDiscovery.register([cron.databaseService]))[\n\t\t\t\tcron.databaseService.serviceName\n\t\t\t]\n\t\t: undefined;\n\n\t// Only imported for a cron that audits: `@geekmidas/audit` is optional.\n\tlet auditor: { flush(): Promise<void> } | undefined;\n\tif (cron.auditorStorageService) {\n\t\tconst { DefaultAuditor } = await import('@geekmidas/audit');\n\t\tconst registered: Record<string, unknown> = await serviceDiscovery.register(\n\t\t\t[cron.auditorStorageService],\n\t\t);\n\t\tconst storage = registered[\n\t\t\tcron.auditorStorageService.serviceName\n\t\t] as AuditStorage;\n\t\tauditor = new DefaultAuditor({\n\t\t\tactor: { id: 'system', type: 'system' },\n\t\t\tstorage,\n\t\t\tmetadata: { function: cron.type },\n\t\t});\n\t}\n\n\tconst logger = cron.logger;\n\tconst requestId = `cron-${Date.now()}`;\n\n\treturn runWithRequestContext(\n\t\t{\n\t\t\tlogger,\n\t\t\trequestId,\n\t\t\tstartTime: Date.now(),\n\t\t\toperation: `cron ${cron.schedule ?? 'manual'}`,\n\t\t},\n\t\tasync () => {\n\t\t\tconst response = await cron.fn({\n\t\t\t\tinput: undefined,\n\t\t\t\tservices,\n\t\t\t\tlogger,\n\t\t\t\tdb,\n\t\t\t\tauditor,\n\t\t\t} as any);\n\t\t\tconst output = await cron.parseOutput(response);\n\n\t\t\tawait auditor?.flush();\n\t\t\t// The cron's output schema is erased by the `any` it is typed with here.\n\t\t\tawait publishEvents(\n\t\t\t\tlogger,\n\t\t\t\tserviceDiscovery,\n\t\t\t\tcron.events,\n\t\t\t\toutput as never,\n\t\t\t);\n\n\t\t\treturn output;\n\t\t},\n\t);\n}\n","import type { ScheduleExpression } from './Cron';\n\n/**\n * A schedule a cron runner can take, as standard five-field cron.\n *\n * `ScheduleExpression` has two spellings. `cron(m h dom mon dow)` is already\n * five-field cron and unwraps; `rate(n unit)` is an interval, and an interval\n * is not a schedule — it says how often, where cron says when.\n *\n * Most intervals still have an exact cron equivalent, because they divide their\n * unit evenly: `rate(5 minutes)` is every fifth minute of every hour, and that\n * is `*/5 * * * *` precisely. `rate(7 hours)` has none. There is no cron field\n * that means \"every seven hours\" — `*/7` in the hour field fires at 0, 7, 14\n * and 21 and then starts over at midnight, so the gap between the last firing\n * of one day and the first of the next is three hours rather than seven.\n *\n * Those are refused rather than rounded. A job that runs at the wrong hour is\n * far harder to notice than one that refused to build, and a schedule quietly\n * adjusted by the framework is a bug nobody goes looking for.\n */\nexport class UnrepresentableSchedule extends Error {\n\tconstructor(\n\t\treadonly schedule: string,\n\t\treason: string,\n\t) {\n\t\tsuper(\n\t\t\t`\\`${schedule}\\` has no exact cron equivalent: ${reason}.\\n\\n` +\n\t\t\t\t'Cron says *when*, not *how often*, so an interval is only exact when ' +\n\t\t\t\t'it divides its unit evenly.\\n' +\n\t\t\t\t'Write the schedule you mean instead — `cron(0 */6 * * *)` for every ' +\n\t\t\t\t'sixth hour, say — or pick an interval that divides: minutes into 60, ' +\n\t\t\t\t'hours into 24.',\n\t\t);\n\t\tthis.name = 'UnrepresentableSchedule';\n\t}\n}\n\n/** Units `rate(…)` accepts, singular or plural. */\nconst UNITS = ['minute', 'hour', 'day'] as const;\ntype Unit = (typeof UNITS)[number];\n\n/**\n * Standard five-field cron for a schedule expression.\n *\n * @throws {UnrepresentableSchedule} when a `rate(…)` has no exact equivalent.\n */\nexport function toCronExpression(schedule: ScheduleExpression): string {\n\tconst cron = /^cron\\((.+)\\)$/.exec(schedule);\n\tif (cron?.[1]) return cron[1].trim();\n\n\tconst rate = /^rate\\(\\s*(\\d+)\\s+(\\w+?)s?\\s*\\)$/.exec(schedule);\n\tif (!rate) {\n\t\tthrow new UnrepresentableSchedule(\n\t\t\tschedule,\n\t\t\t'it is neither `cron(…)` nor `rate(n unit)`',\n\t\t);\n\t}\n\n\tconst every = Number(rate[1]);\n\tconst unit = rate[2] as Unit;\n\t/* c8 ignore next */ if (!unit)\n\t\tthrow new UnrepresentableSchedule(schedule, 'no unit');\n\n\tif (!UNITS.includes(unit)) {\n\t\tthrow new UnrepresentableSchedule(\n\t\t\tschedule,\n\t\t\t`\\`${unit}\\` is not one of ${UNITS.join(', ')}`,\n\t\t);\n\t}\n\n\tif (every < 1) {\n\t\tthrow new UnrepresentableSchedule(\n\t\t\tschedule,\n\t\t\t'an interval must be at least 1',\n\t\t);\n\t}\n\n\tswitch (unit) {\n\t\tcase 'minute':\n\t\t\tif (every === 1) return '* * * * *';\n\t\t\tif (60 % every !== 0) {\n\t\t\t\tthrow new UnrepresentableSchedule(\n\t\t\t\t\tschedule,\n\t\t\t\t\t`${every} does not divide 60, so the interval would reset at the top of each hour`,\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn `*/${every} * * * *`;\n\n\t\tcase 'hour':\n\t\t\tif (every === 1) return '0 * * * *';\n\t\t\tif (24 % every !== 0) {\n\t\t\t\tthrow new UnrepresentableSchedule(\n\t\t\t\t\tschedule,\n\t\t\t\t\t`${every} does not divide 24, so the interval would reset at midnight`,\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn `0 */${every} * * *`;\n\n\t\tcase 'day':\n\t\t\tif (every === 1) return '0 0 * * *';\n\t\t\t// `*/n` in day-of-month restarts on the 1st, so a 2-day interval fires\n\t\t\t// on the 1st and 3rd of a 31-day month and then again on the 1st — one\n\t\t\t// day later, not two. No month length makes that come out even.\n\t\t\tthrow new UnrepresentableSchedule(\n\t\t\t\tschedule,\n\t\t\t\t'day-of-month restarts every month, so no multi-day interval is even',\n\t\t\t);\n\t}\n}\n"],"mappings":"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"}
|
package/dist/database/kysely.cjs
CHANGED
|
@@ -1,206 +1,4 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
let kysely = require("kysely");
|
|
6
|
-
let pg = require("pg");
|
|
7
|
-
pg = require_rolldown_runtime.__toESM(pg, 1);
|
|
8
|
-
//#region src/database/kysely.ts
|
|
9
|
-
/**
|
|
10
|
-
* `KyselyDatabase` — a declared Postgres database, typed by its schema.
|
|
11
|
-
*
|
|
12
|
-
* It declares a logical database, a schema, and the roles that reach it, and
|
|
13
|
-
* hands back a Kysely client built from the one URL the adapter supplied. It
|
|
14
|
-
* names no cloud: `--target=aws` fills that URL from RDS, local dev from the
|
|
15
|
-
* Postgres container.
|
|
16
|
-
*
|
|
17
|
-
* Transactions and RLS context are the execution wrapper's job — it opens the
|
|
18
|
-
* transaction and puts it in `ctx.db`. This construct does not resolve
|
|
19
|
-
* connections per request.
|
|
20
|
-
*/
|
|
21
|
-
/** The schema a database uses when it does not say otherwise. */
|
|
22
|
-
const DEFAULT_SCHEMA = "app";
|
|
23
|
-
/**
|
|
24
|
-
* @typeParam DB - the database's schema.
|
|
25
|
-
* @typeParam TName - the canonical id. TypeScript has no partial
|
|
26
|
-
* type-argument inference, so `new KyselyDatabase<OrdersDB>('orders')` leaves
|
|
27
|
-
* this at its default and the service key widens to `string`. `db` still types
|
|
28
|
-
* through `.database()`, which infers from the service.
|
|
29
|
-
*/
|
|
30
|
-
var KyselyDatabase = class KyselyDatabase {
|
|
31
|
-
options;
|
|
32
|
-
id;
|
|
33
|
-
service;
|
|
34
|
-
/**
|
|
35
|
-
* Declared once and read by both `declare()` and `connect()`, so the key the
|
|
36
|
-
* build publishes and the key the client reads cannot drift.
|
|
37
|
-
*/
|
|
38
|
-
config;
|
|
39
|
-
schemaName;
|
|
40
|
-
roles;
|
|
41
|
-
version;
|
|
42
|
-
derivedFrom;
|
|
43
|
-
constructor(id, options = {}, derivedFrom) {
|
|
44
|
-
this.options = options;
|
|
45
|
-
const canonical = (0, _geekmidas_manifest.canonicalId)(id);
|
|
46
|
-
this.id = canonical;
|
|
47
|
-
this.config = {
|
|
48
|
-
url: (0, _geekmidas_manifest.provideKey)(canonical, "url"),
|
|
49
|
-
ownerUrl: (0, _geekmidas_manifest.provideKey)(canonical, "ownerUrl")
|
|
50
|
-
};
|
|
51
|
-
this.schemaName = options.schema ?? DEFAULT_SCHEMA;
|
|
52
|
-
this.roles = options.roles ?? true;
|
|
53
|
-
this.version = options.version ?? _geekmidas_manifest.DEFAULT_POSTGRES_VERSION;
|
|
54
|
-
this.derivedFrom = derivedFrom;
|
|
55
|
-
this.service = {
|
|
56
|
-
serviceName: (0, _geekmidas_manifest.serviceKey)(canonical),
|
|
57
|
-
register: (registerOptions) => this.connect(registerOptions)
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
declare() {
|
|
61
|
-
if (this.derivedFrom) {
|
|
62
|
-
const { kind, of, schema } = this.derivedFrom;
|
|
63
|
-
return [kind === "database-schema" ? {
|
|
64
|
-
kind,
|
|
65
|
-
id: this.id,
|
|
66
|
-
of,
|
|
67
|
-
schema: schema ?? this.schemaName,
|
|
68
|
-
provides: [this.config.url]
|
|
69
|
-
} : {
|
|
70
|
-
kind,
|
|
71
|
-
id: this.id,
|
|
72
|
-
of,
|
|
73
|
-
provides: [this.config.url]
|
|
74
|
-
}];
|
|
75
|
-
}
|
|
76
|
-
return [{
|
|
77
|
-
kind: "database",
|
|
78
|
-
id: this.id,
|
|
79
|
-
engine: "postgres",
|
|
80
|
-
schema: this.schemaName,
|
|
81
|
-
version: this.version,
|
|
82
|
-
provides: [this.config.url],
|
|
83
|
-
...this.roles ? {} : { roles: false }
|
|
84
|
-
}];
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* A read-only endpoint on this database.
|
|
88
|
-
*
|
|
89
|
-
* Read-only is enforced by the reader role's grants rather than by which
|
|
90
|
-
* endpoint it resolves to, so falling back to the writer where no replica
|
|
91
|
-
* exists is safe rather than a silently writable connection behind a name
|
|
92
|
-
* that says reader. There is no `writer()`: the database *is* the writer, so
|
|
93
|
-
* a replica can never be reached by accident.
|
|
94
|
-
*
|
|
95
|
-
* The return type is narrower than this class on purpose — see
|
|
96
|
-
* {@link ReadOnlyDatabase}.
|
|
97
|
-
*/
|
|
98
|
-
reader() {
|
|
99
|
-
return new KyselyDatabase(`${this.id}Reader`, this.options, {
|
|
100
|
-
kind: "database-reader",
|
|
101
|
-
of: this.id
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
/**
|
|
105
|
-
* A cache that lives in this database.
|
|
106
|
-
*
|
|
107
|
-
* The same strengthening `schema()` is over a second database: `new
|
|
108
|
-
* Cache('Sessions')` says the app caches and leaves where to the deployment,
|
|
109
|
-
* while this says it caches *here* — which is a fact about the application
|
|
110
|
-
* and so belongs in its code.
|
|
111
|
-
*
|
|
112
|
-
* What it buys beyond being explicit: the table's schema and the role that
|
|
113
|
-
* reaches it come from this database rather than from a second convention,
|
|
114
|
-
* and the backend no longer has to guess which database "the database" meant
|
|
115
|
-
* when an app declares two.
|
|
116
|
-
*
|
|
117
|
-
* A cache is not a `KyselyDatabase`, so this returns a `Cache` — the client
|
|
118
|
-
* is a key/value store, not a query builder, and typing it as the parent
|
|
119
|
-
* would be a convenience that lies.
|
|
120
|
-
*/
|
|
121
|
-
cache(id = `${this.id}Cache`, options = {}) {
|
|
122
|
-
return new require_cache.Cache(id, {
|
|
123
|
-
of: this.id,
|
|
124
|
-
...options.table ? { table: options.table } : {}
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* A second schema in this database, with its own role(s) and its own URL —
|
|
129
|
-
* so this database's role holds no grant on those tables at all.
|
|
130
|
-
*
|
|
131
|
-
* Typed separately because a tenant has its own tables: the point is that
|
|
132
|
-
* `auth.selectFrom('sessions')` type-checks and
|
|
133
|
-
* `orders.selectFrom('sessions')` does not.
|
|
134
|
-
*/
|
|
135
|
-
schema(id, options = {}) {
|
|
136
|
-
return new KyselyDatabase(id, options, {
|
|
137
|
-
kind: "database-schema",
|
|
138
|
-
of: this.id,
|
|
139
|
-
schema: options.schema ?? id.toLowerCase()
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
/**
|
|
143
|
-
* A connection as the *owner* role — the one that may create, alter and drop.
|
|
144
|
-
*
|
|
145
|
-
* A `Service` rather than part of the construct, and deliberately so: a
|
|
146
|
-
* construct is what `.dependsOn()` accepts, and nothing should be able to
|
|
147
|
-
* depend on this. A migrator asks for it by name; a handler cannot ask at
|
|
148
|
-
* all, which is the security property the role split exists for.
|
|
149
|
-
*
|
|
150
|
-
* Falls back to the runtime URL where no owner URL was published — the
|
|
151
|
-
* `roles: false` downgrade, where there is only one credential and it is the
|
|
152
|
-
* cluster master's.
|
|
153
|
-
*/
|
|
154
|
-
get owner() {
|
|
155
|
-
return {
|
|
156
|
-
serviceName: `${(0, _geekmidas_manifest.serviceKey)(this.id)}Owner`,
|
|
157
|
-
register: (options) => this.connect(options, { owner: true })
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
/**
|
|
161
|
-
* What this database's clients are built with besides a dialect — the
|
|
162
|
-
* plugins and logging it was declared with.
|
|
163
|
-
*
|
|
164
|
-
* Public so that whatever opens a connection to it outside \`connect()\` —
|
|
165
|
-
* a test's transaction — builds the same client. A test that dropped the
|
|
166
|
-
* plugins would write \`createdAt\` where production writes \`created_at\`.
|
|
167
|
-
*/
|
|
168
|
-
get clientConfig() {
|
|
169
|
-
const { schema: _schema, roles: _roles, version: _version, ...kysely$1 } = this.options;
|
|
170
|
-
return kysely$1;
|
|
171
|
-
}
|
|
172
|
-
async connect(options, as = {}) {
|
|
173
|
-
const { url, ownerUrl } = options.envParser.create((get) => ({
|
|
174
|
-
url: get(this.config.url).string(),
|
|
175
|
-
ownerUrl: get(this.config.ownerUrl).string().optional()
|
|
176
|
-
})).parse();
|
|
177
|
-
return new kysely.Kysely({
|
|
178
|
-
...this.clientConfig,
|
|
179
|
-
dialect: new kysely.PostgresDialect({ pool: pool(as.owner ? ownerUrl ?? url : url) })
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
};
|
|
183
|
-
/**
|
|
184
|
-
* A pool for one URL, with its schema actually on the search path.
|
|
185
|
-
*
|
|
186
|
-
* `?search_path=` is not a libpq parameter — a URL carrying it connects
|
|
187
|
-
* happily and then resolves every unqualified name against `public`, which is
|
|
188
|
-
* how a schema tenant silently writes its tables into the database it was
|
|
189
|
-
* separated from. Postgres takes it as a startup option instead, so the URL
|
|
190
|
-
* keeps the readable form the target derives and this turns it into the thing
|
|
191
|
-
* the server understands.
|
|
192
|
-
*/
|
|
193
|
-
function pool(url) {
|
|
194
|
-
const parsed = new URL(url);
|
|
195
|
-
const searchPath = parsed.searchParams.get("search_path");
|
|
196
|
-
if (!searchPath) return new pg.default.Pool({ connectionString: url });
|
|
197
|
-
parsed.searchParams.delete("search_path");
|
|
198
|
-
return new pg.default.Pool({
|
|
199
|
-
connectionString: parsed.toString(),
|
|
200
|
-
options: `-c search_path=${searchPath}`
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
//#endregion
|
|
204
|
-
exports.KyselyDatabase = KyselyDatabase;
|
|
205
|
-
|
|
206
|
-
//# sourceMappingURL=kysely.cjs.map
|
|
2
|
+
const require_kysely = require("../kysely-BRzzawr2.cjs");
|
|
3
|
+
exports.KyselyDatabase = require_kysely.KyselyDatabase;
|
|
4
|
+
exports.closeDatabasePools = require_kysely.closeDatabasePools;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as KyselyDatabaseOptions, r as ReadOnlyDatabase, t as KyselyDatabase } from "../kysely-
|
|
2
|
-
export { KyselyDatabase, KyselyDatabaseOptions, ReadOnlyDatabase };
|
|
1
|
+
import { i as closeDatabasePools, n as KyselyDatabaseOptions, r as ReadOnlyDatabase, t as KyselyDatabase } from "../kysely-BEm5sDlU.cjs";
|
|
2
|
+
export { KyselyDatabase, KyselyDatabaseOptions, ReadOnlyDatabase, closeDatabasePools };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as KyselyDatabaseOptions, r as ReadOnlyDatabase, t as KyselyDatabase } from "../kysely-
|
|
2
|
-
export { KyselyDatabase, KyselyDatabaseOptions, ReadOnlyDatabase };
|
|
1
|
+
import { i as closeDatabasePools, n as KyselyDatabaseOptions, r as ReadOnlyDatabase, t as KyselyDatabase } from "../kysely-CjuvdXEV.mjs";
|
|
2
|
+
export { KyselyDatabase, KyselyDatabaseOptions, ReadOnlyDatabase, closeDatabasePools };
|
package/dist/database/kysely.mjs
CHANGED
|
@@ -1,203 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import { Kysely, PostgresDialect } from "kysely";
|
|
4
|
-
import pg from "pg";
|
|
5
|
-
//#region src/database/kysely.ts
|
|
6
|
-
/**
|
|
7
|
-
* `KyselyDatabase` — a declared Postgres database, typed by its schema.
|
|
8
|
-
*
|
|
9
|
-
* It declares a logical database, a schema, and the roles that reach it, and
|
|
10
|
-
* hands back a Kysely client built from the one URL the adapter supplied. It
|
|
11
|
-
* names no cloud: `--target=aws` fills that URL from RDS, local dev from the
|
|
12
|
-
* Postgres container.
|
|
13
|
-
*
|
|
14
|
-
* Transactions and RLS context are the execution wrapper's job — it opens the
|
|
15
|
-
* transaction and puts it in `ctx.db`. This construct does not resolve
|
|
16
|
-
* connections per request.
|
|
17
|
-
*/
|
|
18
|
-
/** The schema a database uses when it does not say otherwise. */
|
|
19
|
-
const DEFAULT_SCHEMA = "app";
|
|
20
|
-
/**
|
|
21
|
-
* @typeParam DB - the database's schema.
|
|
22
|
-
* @typeParam TName - the canonical id. TypeScript has no partial
|
|
23
|
-
* type-argument inference, so `new KyselyDatabase<OrdersDB>('orders')` leaves
|
|
24
|
-
* this at its default and the service key widens to `string`. `db` still types
|
|
25
|
-
* through `.database()`, which infers from the service.
|
|
26
|
-
*/
|
|
27
|
-
var KyselyDatabase = class KyselyDatabase {
|
|
28
|
-
options;
|
|
29
|
-
id;
|
|
30
|
-
service;
|
|
31
|
-
/**
|
|
32
|
-
* Declared once and read by both `declare()` and `connect()`, so the key the
|
|
33
|
-
* build publishes and the key the client reads cannot drift.
|
|
34
|
-
*/
|
|
35
|
-
config;
|
|
36
|
-
schemaName;
|
|
37
|
-
roles;
|
|
38
|
-
version;
|
|
39
|
-
derivedFrom;
|
|
40
|
-
constructor(id, options = {}, derivedFrom) {
|
|
41
|
-
this.options = options;
|
|
42
|
-
const canonical = canonicalId(id);
|
|
43
|
-
this.id = canonical;
|
|
44
|
-
this.config = {
|
|
45
|
-
url: provideKey(canonical, "url"),
|
|
46
|
-
ownerUrl: provideKey(canonical, "ownerUrl")
|
|
47
|
-
};
|
|
48
|
-
this.schemaName = options.schema ?? DEFAULT_SCHEMA;
|
|
49
|
-
this.roles = options.roles ?? true;
|
|
50
|
-
this.version = options.version ?? DEFAULT_POSTGRES_VERSION;
|
|
51
|
-
this.derivedFrom = derivedFrom;
|
|
52
|
-
this.service = {
|
|
53
|
-
serviceName: serviceKey(canonical),
|
|
54
|
-
register: (registerOptions) => this.connect(registerOptions)
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
declare() {
|
|
58
|
-
if (this.derivedFrom) {
|
|
59
|
-
const { kind, of, schema } = this.derivedFrom;
|
|
60
|
-
return [kind === "database-schema" ? {
|
|
61
|
-
kind,
|
|
62
|
-
id: this.id,
|
|
63
|
-
of,
|
|
64
|
-
schema: schema ?? this.schemaName,
|
|
65
|
-
provides: [this.config.url]
|
|
66
|
-
} : {
|
|
67
|
-
kind,
|
|
68
|
-
id: this.id,
|
|
69
|
-
of,
|
|
70
|
-
provides: [this.config.url]
|
|
71
|
-
}];
|
|
72
|
-
}
|
|
73
|
-
return [{
|
|
74
|
-
kind: "database",
|
|
75
|
-
id: this.id,
|
|
76
|
-
engine: "postgres",
|
|
77
|
-
schema: this.schemaName,
|
|
78
|
-
version: this.version,
|
|
79
|
-
provides: [this.config.url],
|
|
80
|
-
...this.roles ? {} : { roles: false }
|
|
81
|
-
}];
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* A read-only endpoint on this database.
|
|
85
|
-
*
|
|
86
|
-
* Read-only is enforced by the reader role's grants rather than by which
|
|
87
|
-
* endpoint it resolves to, so falling back to the writer where no replica
|
|
88
|
-
* exists is safe rather than a silently writable connection behind a name
|
|
89
|
-
* that says reader. There is no `writer()`: the database *is* the writer, so
|
|
90
|
-
* a replica can never be reached by accident.
|
|
91
|
-
*
|
|
92
|
-
* The return type is narrower than this class on purpose — see
|
|
93
|
-
* {@link ReadOnlyDatabase}.
|
|
94
|
-
*/
|
|
95
|
-
reader() {
|
|
96
|
-
return new KyselyDatabase(`${this.id}Reader`, this.options, {
|
|
97
|
-
kind: "database-reader",
|
|
98
|
-
of: this.id
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* A cache that lives in this database.
|
|
103
|
-
*
|
|
104
|
-
* The same strengthening `schema()` is over a second database: `new
|
|
105
|
-
* Cache('Sessions')` says the app caches and leaves where to the deployment,
|
|
106
|
-
* while this says it caches *here* — which is a fact about the application
|
|
107
|
-
* and so belongs in its code.
|
|
108
|
-
*
|
|
109
|
-
* What it buys beyond being explicit: the table's schema and the role that
|
|
110
|
-
* reaches it come from this database rather than from a second convention,
|
|
111
|
-
* and the backend no longer has to guess which database "the database" meant
|
|
112
|
-
* when an app declares two.
|
|
113
|
-
*
|
|
114
|
-
* A cache is not a `KyselyDatabase`, so this returns a `Cache` — the client
|
|
115
|
-
* is a key/value store, not a query builder, and typing it as the parent
|
|
116
|
-
* would be a convenience that lies.
|
|
117
|
-
*/
|
|
118
|
-
cache(id = `${this.id}Cache`, options = {}) {
|
|
119
|
-
return new Cache(id, {
|
|
120
|
-
of: this.id,
|
|
121
|
-
...options.table ? { table: options.table } : {}
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* A second schema in this database, with its own role(s) and its own URL —
|
|
126
|
-
* so this database's role holds no grant on those tables at all.
|
|
127
|
-
*
|
|
128
|
-
* Typed separately because a tenant has its own tables: the point is that
|
|
129
|
-
* `auth.selectFrom('sessions')` type-checks and
|
|
130
|
-
* `orders.selectFrom('sessions')` does not.
|
|
131
|
-
*/
|
|
132
|
-
schema(id, options = {}) {
|
|
133
|
-
return new KyselyDatabase(id, options, {
|
|
134
|
-
kind: "database-schema",
|
|
135
|
-
of: this.id,
|
|
136
|
-
schema: options.schema ?? id.toLowerCase()
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
|
-
* A connection as the *owner* role — the one that may create, alter and drop.
|
|
141
|
-
*
|
|
142
|
-
* A `Service` rather than part of the construct, and deliberately so: a
|
|
143
|
-
* construct is what `.dependsOn()` accepts, and nothing should be able to
|
|
144
|
-
* depend on this. A migrator asks for it by name; a handler cannot ask at
|
|
145
|
-
* all, which is the security property the role split exists for.
|
|
146
|
-
*
|
|
147
|
-
* Falls back to the runtime URL where no owner URL was published — the
|
|
148
|
-
* `roles: false` downgrade, where there is only one credential and it is the
|
|
149
|
-
* cluster master's.
|
|
150
|
-
*/
|
|
151
|
-
get owner() {
|
|
152
|
-
return {
|
|
153
|
-
serviceName: `${serviceKey(this.id)}Owner`,
|
|
154
|
-
register: (options) => this.connect(options, { owner: true })
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
/**
|
|
158
|
-
* What this database's clients are built with besides a dialect — the
|
|
159
|
-
* plugins and logging it was declared with.
|
|
160
|
-
*
|
|
161
|
-
* Public so that whatever opens a connection to it outside \`connect()\` —
|
|
162
|
-
* a test's transaction — builds the same client. A test that dropped the
|
|
163
|
-
* plugins would write \`createdAt\` where production writes \`created_at\`.
|
|
164
|
-
*/
|
|
165
|
-
get clientConfig() {
|
|
166
|
-
const { schema: _schema, roles: _roles, version: _version, ...kysely } = this.options;
|
|
167
|
-
return kysely;
|
|
168
|
-
}
|
|
169
|
-
async connect(options, as = {}) {
|
|
170
|
-
const { url, ownerUrl } = options.envParser.create((get) => ({
|
|
171
|
-
url: get(this.config.url).string(),
|
|
172
|
-
ownerUrl: get(this.config.ownerUrl).string().optional()
|
|
173
|
-
})).parse();
|
|
174
|
-
return new Kysely({
|
|
175
|
-
...this.clientConfig,
|
|
176
|
-
dialect: new PostgresDialect({ pool: pool(as.owner ? ownerUrl ?? url : url) })
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
};
|
|
180
|
-
/**
|
|
181
|
-
* A pool for one URL, with its schema actually on the search path.
|
|
182
|
-
*
|
|
183
|
-
* `?search_path=` is not a libpq parameter — a URL carrying it connects
|
|
184
|
-
* happily and then resolves every unqualified name against `public`, which is
|
|
185
|
-
* how a schema tenant silently writes its tables into the database it was
|
|
186
|
-
* separated from. Postgres takes it as a startup option instead, so the URL
|
|
187
|
-
* keeps the readable form the target derives and this turns it into the thing
|
|
188
|
-
* the server understands.
|
|
189
|
-
*/
|
|
190
|
-
function pool(url) {
|
|
191
|
-
const parsed = new URL(url);
|
|
192
|
-
const searchPath = parsed.searchParams.get("search_path");
|
|
193
|
-
if (!searchPath) return new pg.Pool({ connectionString: url });
|
|
194
|
-
parsed.searchParams.delete("search_path");
|
|
195
|
-
return new pg.Pool({
|
|
196
|
-
connectionString: parsed.toString(),
|
|
197
|
-
options: `-c search_path=${searchPath}`
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
//#endregion
|
|
201
|
-
export { KyselyDatabase };
|
|
202
|
-
|
|
203
|
-
//# sourceMappingURL=kysely.mjs.map
|
|
1
|
+
import { n as closeDatabasePools, t as KyselyDatabase } from "../kysely-BYfM6Zby.mjs";
|
|
2
|
+
export { KyselyDatabase, closeDatabasePools };
|
package/dist/index.cjs
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_Construct = require("./Construct-DmvXceVY.cjs");
|
|
3
|
+
const require_shutdown = require("./shutdown-BMJmAhSt.cjs");
|
|
3
4
|
exports.Construct = require_Construct.Construct;
|
|
4
5
|
exports.ConstructType = require_Construct.ConstructType;
|
|
6
|
+
exports.onShutdown = require_shutdown.onShutdown;
|
|
7
|
+
exports.runShutdownHooks = require_shutdown.runShutdownHooks;
|
|
5
8
|
exports.sniffService = require_Construct.sniffService;
|
|
6
9
|
exports.snifferContext = require_Construct.snifferContext;
|
package/dist/index.d.cts
CHANGED
|
@@ -2,4 +2,26 @@ import { i as snifferContext, n as ConstructType, r as sniffService, t as Constr
|
|
|
2
2
|
import { n as LowerHttpMethod, r as RemoveUndefined, t as HttpMethod } from "./types-DMuSQeLL.cjs";
|
|
3
3
|
import { i as TelemetryResponse, n as TelemetryContext, r as TelemetryRequest, t as Telemetry } from "./telemetry-DGcFlVpB.cjs";
|
|
4
4
|
import { Service, ServiceRecord } from "@geekmidas/services";
|
|
5
|
-
|
|
5
|
+
//#region src/shutdown.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* What a server releases when it stops: the connections constructs opened.
|
|
8
|
+
*
|
|
9
|
+
* A construct that holds something open — a database's pool — registers how to
|
|
10
|
+
* close it here, and the generated server runs every hook once it has stopped
|
|
11
|
+
* taking requests. Without that, a rolling deploy has the old task and the new
|
|
12
|
+
* one both holding connections until the old process dies.
|
|
13
|
+
*
|
|
14
|
+
* Kept free of any driver import so the server entry can always import it,
|
|
15
|
+
* whether or not the app has a database.
|
|
16
|
+
*/
|
|
17
|
+
type ShutdownHook = () => Promise<void> | void;
|
|
18
|
+
/** Run `hook` when the server shuts down. Returns a function that removes it. */
|
|
19
|
+
export declare function onShutdown(hook: ShutdownHook): () => void;
|
|
20
|
+
/**
|
|
21
|
+
* Run every registered hook, together, and wait for them. A hook that fails is
|
|
22
|
+
* reported to `onError` and does not stop the others.
|
|
23
|
+
*/
|
|
24
|
+
export declare function runShutdownHooks(onError?: (error: unknown) => void): Promise<void>;
|
|
25
|
+
//#endregion
|
|
26
|
+
export { Construct, ConstructType, type HttpMethod, type LowerHttpMethod, type RemoveUndefined, type Service, type ServiceRecord, type Telemetry, type TelemetryContext, type TelemetryRequest, type TelemetryResponse, sniffService, snifferContext };
|
|
27
|
+
//# sourceMappingURL=index.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/shutdown.ts"],"mappings":";;;;;;;;;;;;;;;;KAYK,qBAAqB;;wBAUV,WAAW,MAAM;;;;;wBASX,iBACrB,WAAU,0BACR"}
|
package/dist/index.d.mts
CHANGED
|
@@ -2,4 +2,26 @@ import { i as snifferContext, n as ConstructType, r as sniffService, t as Constr
|
|
|
2
2
|
import { n as LowerHttpMethod, r as RemoveUndefined, t as HttpMethod } from "./types-DMuSQeLL.mjs";
|
|
3
3
|
import { i as TelemetryResponse, n as TelemetryContext, r as TelemetryRequest, t as Telemetry } from "./telemetry-DGcFlVpB.mjs";
|
|
4
4
|
import { Service, ServiceRecord } from "@geekmidas/services";
|
|
5
|
-
|
|
5
|
+
//#region src/shutdown.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* What a server releases when it stops: the connections constructs opened.
|
|
8
|
+
*
|
|
9
|
+
* A construct that holds something open — a database's pool — registers how to
|
|
10
|
+
* close it here, and the generated server runs every hook once it has stopped
|
|
11
|
+
* taking requests. Without that, a rolling deploy has the old task and the new
|
|
12
|
+
* one both holding connections until the old process dies.
|
|
13
|
+
*
|
|
14
|
+
* Kept free of any driver import so the server entry can always import it,
|
|
15
|
+
* whether or not the app has a database.
|
|
16
|
+
*/
|
|
17
|
+
type ShutdownHook = () => Promise<void> | void;
|
|
18
|
+
/** Run `hook` when the server shuts down. Returns a function that removes it. */
|
|
19
|
+
export declare function onShutdown(hook: ShutdownHook): () => void;
|
|
20
|
+
/**
|
|
21
|
+
* Run every registered hook, together, and wait for them. A hook that fails is
|
|
22
|
+
* reported to `onError` and does not stop the others.
|
|
23
|
+
*/
|
|
24
|
+
export declare function runShutdownHooks(onError?: (error: unknown) => void): Promise<void>;
|
|
25
|
+
//#endregion
|
|
26
|
+
export { Construct, ConstructType, type HttpMethod, type LowerHttpMethod, type RemoveUndefined, type Service, type ServiceRecord, type Telemetry, type TelemetryContext, type TelemetryRequest, type TelemetryResponse, sniffService, snifferContext };
|
|
27
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/shutdown.ts"],"mappings":";;;;;;;;;;;;;;;;KAYK,qBAAqB;;wBAUV,WAAW,MAAM;;;;;wBASX,iBACrB,WAAU,0BACR"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
import { i as snifferContext, n as ConstructType, r as sniffService, t as Construct } from "./Construct-BO1X-kt9.mjs";
|
|
2
|
-
|
|
2
|
+
import { n as runShutdownHooks, t as onShutdown } from "./shutdown-C7xa8x9S.mjs";
|
|
3
|
+
export { Construct, ConstructType, onShutdown, runShutdownHooks, sniffService, snifferContext };
|