@devmoods/express-extras 0.64.0 → 0.65.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +0 -111
  2. package/dist/auth/emailAndPassword.js +2 -1
  3. package/dist/auth/google.d.ts +4 -4
  4. package/dist/auth/google.js +14 -11
  5. package/dist/auth/passwordless.js +2 -1
  6. package/dist/auth/token.js +6 -6
  7. package/dist/auth/types.d.ts +1 -1
  8. package/dist/auth.d.ts +1 -1
  9. package/dist/auth.js +17 -10
  10. package/dist/cache.d.ts +3 -0
  11. package/dist/cache.js +3 -2
  12. package/dist/channels.d.ts +6 -5
  13. package/dist/channels.js +9 -4
  14. package/dist/cli.js +1 -1
  15. package/dist/config.d.ts +66 -66
  16. package/dist/config.js +35 -36
  17. package/dist/context.d.ts +5 -2
  18. package/dist/context.js +10 -9
  19. package/dist/csrf.js +6 -6
  20. package/dist/email.d.ts +1 -1
  21. package/dist/email.js +3 -3
  22. package/dist/errors.js +1 -1
  23. package/dist/faktory.d.ts +1 -1
  24. package/dist/faktory.js +5 -15
  25. package/dist/index.d.ts +2 -2
  26. package/dist/index.js +4 -3
  27. package/dist/listen.d.ts +4 -4
  28. package/dist/listen.js +41 -43
  29. package/dist/logger.d.ts +1 -0
  30. package/dist/logger.js +11 -5
  31. package/dist/manage.d.ts +4 -6
  32. package/dist/manage.js +41 -37
  33. package/dist/middleware.d.ts +3 -0
  34. package/dist/middleware.js +24 -7
  35. package/dist/rate-limit.d.ts +1 -0
  36. package/dist/rate-limit.js +25 -3
  37. package/dist/redis.d.ts +1 -1
  38. package/dist/redis.js +4 -4
  39. package/dist/sentry.js +7 -3
  40. package/dist/setupTests.global.d.ts +2 -0
  41. package/dist/setupTests.global.js +4 -0
  42. package/dist/setupTests.js +3 -0
  43. package/dist/slack.js +2 -2
  44. package/dist/utils.d.ts +0 -13
  45. package/dist/utils.js +0 -21
  46. package/dist/validate.js +7 -4
  47. package/dist/vite/__tests__/fixtures/src/entry-server.d.ts +5 -0
  48. package/dist/vite/__tests__/fixtures/src/entry-server.js +15 -0
  49. package/dist/vite/defineViteConfig.js +11 -1
  50. package/dist/vite/index.d.ts +1 -0
  51. package/dist/vite/index.js +3 -0
  52. package/dist/vite/ssr.d.ts +1 -0
  53. package/dist/vite/ssr.js +9 -4
  54. package/dist/vitest/index.d.ts +5 -0
  55. package/dist/vitest/index.js +24 -0
  56. package/dist/vitest/matchers.d.ts +1 -1
  57. package/dist/vitest/matchers.js +6 -2
  58. package/dist/vitest/testClient.d.ts +30 -0
  59. package/dist/vitest/testClient.js +98 -0
  60. package/dist/webapi.js +4 -0
  61. package/package.json +31 -42
  62. package/dist/__tests__/__migrations__/1_a.d.ts +0 -3
  63. package/dist/__tests__/__migrations__/1_a.js +0 -9
  64. package/dist/migrate.d.ts +0 -47
  65. package/dist/migrate.js +0 -255
  66. package/dist/postgres-ts-generator.d.ts +0 -11
  67. package/dist/postgres-ts-generator.js +0 -114
  68. package/dist/postgres.d.ts +0 -71
  69. package/dist/postgres.js +0 -261
  70. package/dist/sql.d.ts +0 -33
  71. package/dist/sql.js +0 -135
  72. package/dist/vite.d.ts +0 -1
  73. package/dist/vite.js +0 -1
@@ -0,0 +1,30 @@
1
+ import { type Server } from 'node:http';
2
+ import { type FetchClientOptions } from '@devmoods/fetch';
3
+ import { type Application } from 'express';
4
+ import * as tough from 'tough-cookie';
5
+ export declare function createTestClient(server: Server | Application, options?: FetchClientOptions): Promise<{
6
+ duplicate(): Promise</*elided*/ any>;
7
+ cookieJar: tough.CookieJar;
8
+ fetch: {
9
+ <TResponseData, TRequestData = any, TTransformedResponseData = TResponseData, TTransformedRequestData = TRequestData, TSchema extends import("@standard-schema/spec", { with: { "resolution-mode": "require" } }).StandardSchemaV1<TTransformedResponseData, any> = import("@standard-schema/spec", { with: { "resolution-mode": "require" } }).StandardSchemaV1<TTransformedResponseData, TTransformedResponseData>>(url: string, options?: import("@devmoods/fetch").RequestOptions<TRequestData, TTransformedRequestData, TResponseData, TTransformedResponseData, TSchema>): Promise<import("@devmoods/fetch").HttpResponse<import("@standard-schema/spec", { with: { "resolution-mode": "require" } }).StandardSchemaV1.InferOutput<TSchema>>>;
10
+ intercept(interceptor: import("@devmoods/fetch").FetchInterceptor): () => void;
11
+ };
12
+ post: {
13
+ <TResponseData, TRequestData = any, TTransformedResponseData = TResponseData, TTransformedRequestData = TRequestData, TSchema extends import("@standard-schema/spec", { with: { "resolution-mode": "require" } }).StandardSchemaV1<TTransformedResponseData, any> = import("@standard-schema/spec", { with: { "resolution-mode": "require" } }).StandardSchemaV1<TTransformedResponseData, TTransformedResponseData>>(url: string, options?: import("@devmoods/fetch").RequestOptions<TRequestData, TTransformedRequestData, TResponseData, TTransformedResponseData, TSchema>): Promise<import("@devmoods/fetch").HttpResponse<import("@standard-schema/spec", { with: { "resolution-mode": "require" } }).StandardSchemaV1.InferOutput<TSchema>>>;
14
+ intercept(interceptor: import("@devmoods/fetch").FetchInterceptor): () => void;
15
+ };
16
+ get: {
17
+ <TResponseData, TRequestData = any, TTransformedResponseData = TResponseData, TTransformedRequestData = TRequestData, TSchema extends import("@standard-schema/spec", { with: { "resolution-mode": "require" } }).StandardSchemaV1<TTransformedResponseData, any> = import("@standard-schema/spec", { with: { "resolution-mode": "require" } }).StandardSchemaV1<TTransformedResponseData, TTransformedResponseData>>(url: string, options?: import("@devmoods/fetch").RequestOptions<TRequestData, TTransformedRequestData, TResponseData, TTransformedResponseData, TSchema>): Promise<import("@devmoods/fetch").HttpResponse<import("@standard-schema/spec", { with: { "resolution-mode": "require" } }).StandardSchemaV1.InferOutput<TSchema>>>;
18
+ intercept(interceptor: import("@devmoods/fetch").FetchInterceptor): () => void;
19
+ };
20
+ put: {
21
+ <TResponseData, TRequestData = any, TTransformedResponseData = TResponseData, TTransformedRequestData = TRequestData, TSchema extends import("@standard-schema/spec", { with: { "resolution-mode": "require" } }).StandardSchemaV1<TTransformedResponseData, any> = import("@standard-schema/spec", { with: { "resolution-mode": "require" } }).StandardSchemaV1<TTransformedResponseData, TTransformedResponseData>>(url: string, options?: import("@devmoods/fetch").RequestOptions<TRequestData, TTransformedRequestData, TResponseData, TTransformedResponseData, TSchema>): Promise<import("@devmoods/fetch").HttpResponse<import("@standard-schema/spec", { with: { "resolution-mode": "require" } }).StandardSchemaV1.InferOutput<TSchema>>>;
22
+ intercept(interceptor: import("@devmoods/fetch").FetchInterceptor): () => void;
23
+ };
24
+ delete: {
25
+ <TResponseData, TRequestData = any, TTransformedResponseData = TResponseData, TTransformedRequestData = TRequestData, TSchema extends import("@standard-schema/spec", { with: { "resolution-mode": "require" } }).StandardSchemaV1<TTransformedResponseData, any> = import("@standard-schema/spec", { with: { "resolution-mode": "require" } }).StandardSchemaV1<TTransformedResponseData, TTransformedResponseData>>(url: string, options?: import("@devmoods/fetch").RequestOptions<TRequestData, TTransformedRequestData, TResponseData, TTransformedResponseData, TSchema>): Promise<import("@devmoods/fetch").HttpResponse<import("@standard-schema/spec", { with: { "resolution-mode": "require" } }).StandardSchemaV1.InferOutput<TSchema>>>;
26
+ intercept(interceptor: import("@devmoods/fetch").FetchInterceptor): () => void;
27
+ };
28
+ close: () => Promise<void>;
29
+ [Symbol.asyncDispose]: () => Promise<void>;
30
+ }>;
@@ -0,0 +1,98 @@
1
+ import { createServer } from 'node:http';
2
+ import {} from 'node:net';
3
+ import { createFetch, HttpError, } from '@devmoods/fetch';
4
+ import {} from 'express';
5
+ import * as tough from 'tough-cookie';
6
+ export async function createTestClient(server, options) {
7
+ // Express app
8
+ if ('route' in server) {
9
+ server = createServer(server);
10
+ }
11
+ let address = server.address();
12
+ /* c8 ignore next 5 */
13
+ if (typeof address === 'string') {
14
+ throw new Error(`Can't use a server listening on a UNIX domain socket: ${address}`);
15
+ }
16
+ let started = false;
17
+ if (address == null) {
18
+ await new Promise((resolve) => {
19
+ server.once('listening', resolve);
20
+ server.listen(0);
21
+ });
22
+ started = true;
23
+ address = server.address();
24
+ }
25
+ const cookieJar = new tough.CookieJar();
26
+ const fetch = createFetch({
27
+ getRootUrl: () => `http://localhost:${address.port}`,
28
+ redirect: 'manual',
29
+ ...options,
30
+ });
31
+ fetch.intercept({
32
+ request: async (req) => {
33
+ req.headers.set('cookie', await cookieJar.getCookieString(req.url));
34
+ },
35
+ response: async (response) => {
36
+ const cookies = response.headers.getSetCookie();
37
+ await Promise.all(cookies.map(async (cookie) => await cookieJar.setCookie(cookie, response.url, {
38
+ ignoreError: false,
39
+ })));
40
+ },
41
+ });
42
+ const post = async (url, opt) => {
43
+ return fetch(url, { method: 'POST', ...opt }).catch((e) => {
44
+ if (e instanceof HttpError) {
45
+ return e.response;
46
+ }
47
+ throw e;
48
+ });
49
+ };
50
+ post.intercept = fetch.intercept;
51
+ const get = async (url, opt) => {
52
+ return fetch(url, { method: 'GET', ...opt }).catch((e) => {
53
+ if (e instanceof HttpError) {
54
+ return e.response;
55
+ }
56
+ throw e;
57
+ });
58
+ };
59
+ get.intercept = fetch.intercept;
60
+ const put = async (url, opt) => {
61
+ return fetch(url, { method: 'PUT', ...opt }).catch((e) => {
62
+ if (e instanceof HttpError) {
63
+ return e.response;
64
+ }
65
+ throw e;
66
+ });
67
+ };
68
+ put.intercept = fetch.intercept;
69
+ const delete_ = async (url, opt) => {
70
+ return fetch(url, { method: 'DELETE', ...opt }).catch((e) => {
71
+ if (e instanceof HttpError) {
72
+ return e.response;
73
+ }
74
+ throw e;
75
+ });
76
+ };
77
+ delete_.intercept = fetch.intercept;
78
+ const close = async () => {
79
+ if (started) {
80
+ server.close();
81
+ }
82
+ };
83
+ return {
84
+ duplicate() {
85
+ return createTestClient(server, options);
86
+ },
87
+ cookieJar,
88
+ fetch,
89
+ post,
90
+ get,
91
+ put,
92
+ delete: delete_,
93
+ close,
94
+ [Symbol.asyncDispose]: async () => {
95
+ await close();
96
+ },
97
+ };
98
+ }
package/dist/webapi.js CHANGED
@@ -4,6 +4,7 @@ export function createFetchHeaders(requestHeaders) {
4
4
  const headers = new Headers();
5
5
  for (const [key, values] of Object.entries(requestHeaders)) {
6
6
  if (values) {
7
+ /* c8 ignore next 4 */
7
8
  if (Array.isArray(values)) {
8
9
  for (const value of values) {
9
10
  headers.append(key, value);
@@ -20,8 +21,10 @@ export function createFetchRequest(req) {
20
21
  try {
21
22
  const origin = `${req.protocol}://${req.get('host')}`;
22
23
  // Note: This had to take originalUrl into account for presumably vite's proxying
24
+ /* c8 ignore next 1 */
23
25
  const url = new URL(req.originalUrl || req.url, origin);
24
26
  const controller = new AbortController();
27
+ /* c8 ignore next 3 */
25
28
  req.on('close', () => {
26
29
  controller.abort();
27
30
  });
@@ -34,6 +37,7 @@ export function createFetchRequest(req) {
34
37
  init.body = req.body;
35
38
  }
36
39
  return new Request(url.href, init);
40
+ /* c8 ignore next 7 */
37
41
  }
38
42
  catch (error) {
39
43
  if (error instanceof TypeError) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devmoods/express-extras",
3
- "version": "0.64.0",
3
+ "version": "0.65.0",
4
4
  "description": "Kubernetes-ready utilities for rapid Node.js application development",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -12,10 +12,16 @@
12
12
  "default": "./dist/index.js"
13
13
  }
14
14
  },
15
+ "./config": {
16
+ "import": {
17
+ "types": "./dist/config.d.ts",
18
+ "default": "./dist/config.js"
19
+ }
20
+ },
15
21
  "./vite": {
16
22
  "import": {
17
23
  "types": "./dist/vite/vite.d.ts",
18
- "default": "./dist/vite.js"
24
+ "default": "./dist/vite/index.js"
19
25
  }
20
26
  },
21
27
  "./vite/ssr": {
@@ -37,9 +43,13 @@
37
43
  }
38
44
  }
39
45
  },
40
- "author": {
41
- "name": "hanse",
42
- "email": "hanse@koren.im"
46
+ "scripts": {
47
+ "clean": "rm -rf dist",
48
+ "lint": "eslint",
49
+ "test": "NODE_ENV=test DOTENV_CONFIG_PATH=.env.test vitest",
50
+ "test:coverage": "pnpm run test --coverage",
51
+ "build": "pnpm run clean && tsc",
52
+ "prepublishOnly": "pnpm run clean && pnpm run build"
43
53
  },
44
54
  "repository": {
45
55
  "type": "git",
@@ -50,40 +60,31 @@
50
60
  ],
51
61
  "license": "ISC",
52
62
  "devDependencies": {
53
- "@devmoods/eslint-config": "^2.12.0",
54
63
  "@types/koa-compose": "^3.2.8",
55
- "@types/node": "24.3.1",
56
- "@types/supertest": "^6.0.3",
57
- "@vitejs/plugin-react": "^5.0.2",
58
- "@vitest/coverage-v8": "^3.2.4",
64
+ "@vitejs/plugin-react": "^5.0.3",
59
65
  "arktype": "^2.1.22",
60
- "cookiejar": "^2.1.4",
61
- "eslint": "^9.35.0",
62
66
  "faktory-worker": "^4.7.1",
63
- "prettier": "3.6.2",
64
- "supertest": "7.1.4",
65
- "ts-node": "10.9.2",
66
- "tsx": "^4.20.5",
67
- "typescript": "5.9.2",
68
- "vite": "^7.1.5",
69
- "vitest": "^3.2.4",
70
- "vitest-fail-on-console": "^0.10.1"
67
+ "react": "^19.1.1",
68
+ "react-dom": "^19.1.1",
69
+ "vite": "^7.1.7"
71
70
  },
72
71
  "dependencies": {
72
+ "@devmoods/config": "0.1.1",
73
73
  "@devmoods/fetch": "^4.2.0",
74
- "@hey-api/openapi-ts": "^0.83.1",
75
- "@sentry/node": "^10.11.0",
76
- "@sentry/profiling-node": "^10.11.0",
74
+ "@devmoods/observability": "0.1.1",
75
+ "@devmoods/postgres": "0.1.1",
76
+ "@hey-api/openapi-ts": "^0.84.3",
77
+ "@sentry/node": "^10.15.0",
78
+ "@sentry/node-native": "^10.15.0",
79
+ "@sentry/profiling-node": "^10.15.0",
77
80
  "@standard-schema/spec": "^1.0.0",
78
81
  "@types/bcrypt": "^6.0.0",
79
82
  "@types/cookie-parser": "^1.4.9",
80
83
  "@types/express": "^5.0.3",
81
84
  "@types/jsonwebtoken": "^9.0.10",
82
85
  "@types/nodemailer": "^7.0.1",
83
- "@types/pg": "^8.15.5",
84
86
  "@types/qs": "^6.14.0",
85
87
  "@types/superagent": "^8.1.9",
86
- "@types/uuid": "^10.0.0",
87
88
  "ajv": "^8.17.1",
88
89
  "argon2": "^0.44.0",
89
90
  "bcrypt": "^6.0.0",
@@ -97,15 +98,11 @@
97
98
  "helmet": "^8.1.0",
98
99
  "jsonwebtoken": "^9.0.2",
99
100
  "nodemailer": "^7.0.6",
100
- "pg": "^8.16.3",
101
- "pg-structure": "^7.15.3",
102
101
  "redis": "^5.8.2",
103
102
  "sonda": "^0.9.0",
104
- "sql-formatter": "^15.6.9",
105
- "uuid": "^13.0.0"
106
- },
107
- "prettier": {
108
- "singleQuote": true
103
+ "tough-cookie": "^6.0.0",
104
+ "uuid": "^13.0.0",
105
+ "vitest-fail-on-console": "^0.10.1"
109
106
  },
110
107
  "peerDependencies": {
111
108
  "@vitejs/plugin-react": ">=4.3.4",
@@ -117,13 +114,5 @@
117
114
  "engines": {
118
115
  "node": ">=20 <25"
119
116
  },
120
- "scripts": {
121
- "start": "pushd example; pnpm run start --watch; popd example;",
122
- "start:worker": "pushd example; pnpm run start:worker; popd example;",
123
- "clean": "rm -rf dist",
124
- "lint": "eslint",
125
- "test": "./test.sh",
126
- "build": "pnpm run clean && tsc --project tsconfig.build.json",
127
- "format": "prettier --write '**/*.{js,css,md,ts,tsx}'"
128
- }
129
- }
117
+ "gitHead": "600a2c41cd71ae45a5fc01058b84eeeb48be63b1"
118
+ }
@@ -1,3 +0,0 @@
1
- import { type Connection } from '../../postgres.js';
2
- export declare function up(query: Connection['query']): Promise<void>;
3
- export declare function down(query: Connection['query']): Promise<void>;
@@ -1,9 +0,0 @@
1
- import {} from '../../postgres.js';
2
- export async function up(query) {
3
- await query('CREATE TABLE baz (id serial primary key)');
4
- await query('CREATE TABLE foo (id serial primary key)');
5
- }
6
- export async function down(query) {
7
- await query('DROP TABLE foo');
8
- await query('DROP TABLE baz');
9
- }
package/dist/migrate.d.ts DELETED
@@ -1,47 +0,0 @@
1
- import { type Command } from 'commander';
2
- import { type CLI } from './cli.js';
3
- import { createPostgres } from './postgres.js';
4
- interface Migration {
5
- name: string;
6
- filename: string;
7
- version: number;
8
- }
9
- interface MigrationFileAPI {
10
- useTransaction?: boolean;
11
- up(fn: any): Promise<void> | Promise<void> | string | undefined;
12
- down(fn: any): Promise<string> | Promise<void> | string | undefined;
13
- }
14
- export declare const asCli: (command: Command, cli: CLI) => void;
15
- export interface MigratorOptions {
16
- postgres?: ReturnType<typeof createPostgres>;
17
- migrationTableName?: string;
18
- verbose?: boolean;
19
- pathToMigrations?: string;
20
- }
21
- export declare class Migrator {
22
- private postgres;
23
- private migrationTableName;
24
- private pathToMigrations;
25
- private filenameRegExp;
26
- private verbose;
27
- constructor({ postgres, pathToMigrations, migrationTableName, verbose, }?: MigratorOptions);
28
- end(): Promise<void>;
29
- init(): Promise<boolean>;
30
- isInstalled(): Promise<boolean>;
31
- create(name: string | null | undefined, format?: 'ts' | 'sql'): Promise<string>;
32
- runMigrations(direction: 'up' | 'down'): Promise<number>;
33
- log(...args: any[]): void;
34
- executeMigration(migrations: Migration[], currentVersion: number, direction: 'up' | 'down'): Promise<number>;
35
- readMigrationFile(filename: string): Promise<MigrationFileAPI>;
36
- migrate(migrator: MigrationFileAPI, direction: 'up' | 'down'): Promise<void>;
37
- updateCurrentVersion(version: number): Promise<import("pg").QueryResult<import("pg").QueryResultRow>>;
38
- getCurrentVersion(): Promise<number>;
39
- getMigrations(): Promise<{
40
- filename: string;
41
- version: number;
42
- name: string;
43
- }[]>;
44
- getPreviousMigrations(migrations: Migration[], currentVersion: number): Migration[];
45
- getPendingMigrations(migrations: Migration[], currentVersion: number): Migration[];
46
- }
47
- export {};
package/dist/migrate.js DELETED
@@ -1,255 +0,0 @@
1
- import { promises as fs } from 'node:fs';
2
- import * as path from 'node:path';
3
- import chalk from 'chalk';
4
- import {} from 'commander';
5
- import {} from './cli.js';
6
- import { createPostgres } from './postgres.js';
7
- import { formatSql, sql } from './sql.js';
8
- export const asCli = (command, cli) => {
9
- command.command('up').action(cli.wrap(async () => {
10
- const migrator = new Migrator();
11
- try {
12
- await migrator.runMigrations('up');
13
- }
14
- finally {
15
- await migrator.end();
16
- }
17
- }));
18
- command.command('down').action(cli.wrap(async () => {
19
- const migrator = new Migrator();
20
- try {
21
- await migrator.runMigrations('down');
22
- }
23
- finally {
24
- await migrator.end();
25
- }
26
- }));
27
- command.command('init').action(cli.wrap(async () => {
28
- const migrator = new Migrator();
29
- try {
30
- const created = await migrator.init();
31
- if (created) {
32
- cli.greenLine('Migrations table was successfully setup.');
33
- }
34
- else {
35
- cli.greenLine('Migrations table already existing.');
36
- }
37
- }
38
- finally {
39
- await migrator.end();
40
- }
41
- }));
42
- command
43
- .command('create')
44
- .argument('<string>', 'name of migration')
45
- .option('--typescript', 'use typescript')
46
- .action(cli.wrap(async (name, flags) => {
47
- const migrator = new Migrator();
48
- try {
49
- const createdMigration = await migrator.create(name, flags.typescript ? 'ts' : 'sql');
50
- migrator.log(chalk.green('Created migration', createdMigration));
51
- }
52
- finally {
53
- await migrator.end();
54
- }
55
- }));
56
- };
57
- export class Migrator {
58
- postgres;
59
- migrationTableName;
60
- pathToMigrations;
61
- filenameRegExp = /(\d+)_(.*)\.(js|ts|mjs|mts|sql)/;
62
- verbose;
63
- constructor({ postgres = createPostgres({ onLogStatement: () => { } }), pathToMigrations = path.resolve(process.cwd(), 'db', 'migrations'), migrationTableName = 'migrations', verbose = true, } = {}) {
64
- this.postgres = postgres;
65
- this.pathToMigrations = pathToMigrations;
66
- this.migrationTableName = migrationTableName;
67
- this.verbose = verbose;
68
- }
69
- end() {
70
- return this.postgres.end();
71
- }
72
- async init() {
73
- if (await this.isInstalled()) {
74
- return false;
75
- }
76
- await fs.mkdir(this.pathToMigrations, { recursive: true });
77
- await this.postgres.query(sql `
78
- CREATE TABLE IF NOT EXISTS ${sql.identifier(this.migrationTableName)} (
79
- id bigserial PRIMARY KEY,
80
- version int NOT NULL,
81
- run_at timestamp with time zone NOT NULL
82
- );
83
- `);
84
- return true;
85
- }
86
- async isInstalled() {
87
- try {
88
- await this.postgres.query(sql `SELECT * FROM ${sql.identifier(this.migrationTableName)}`);
89
- await fs.stat(this.pathToMigrations);
90
- return true;
91
- }
92
- catch {
93
- return false;
94
- }
95
- }
96
- async create(name, format = 'sql') {
97
- if (!name) {
98
- throw new Error('Missing positional argument <name>');
99
- }
100
- if (!(await this.isInstalled())) {
101
- await this.init();
102
- }
103
- const existingMigrations = await this.getMigrations();
104
- const lastVersion = existingMigrations.length
105
- ? existingMigrations[existingMigrations.length - 1].version
106
- : 0;
107
- const filename = path.join(this.pathToMigrations, `${lastVersion + 1}_${name}.${format}`);
108
- const getTemplate = () => {
109
- switch (format) {
110
- case 'sql':
111
- return `-- Migration ${name} created ${new Date()}\n\nSELECT 1;\n\n-- ROLLBACK --\n\nSELECT 1;\n`;
112
- case 'ts':
113
- return `// Migration ${name} created ${new Date()}\n\nexport function up(query: any) {}\n\nexport function down(query: any) {}\n`;
114
- }
115
- };
116
- await fs.writeFile(filename, getTemplate());
117
- return filename;
118
- }
119
- async runMigrations(direction) {
120
- if (direction !== 'up' && direction !== 'down') {
121
- throw new Error('Only up and down are supported.');
122
- }
123
- if (!(await this.isInstalled())) {
124
- await this.init();
125
- }
126
- const migrations = await this.getMigrations();
127
- const currentVersion = await this.getCurrentVersion();
128
- return this.executeMigration(migrations, currentVersion, direction);
129
- }
130
- log(...args) {
131
- if (this.verbose) {
132
- console.log(...args);
133
- }
134
- }
135
- async executeMigration(migrations, currentVersion, direction) {
136
- if (direction === 'up') {
137
- const pendingMigrations = this.getPendingMigrations(migrations, currentVersion);
138
- if (!pendingMigrations.length) {
139
- this.log(chalk.green('No pending migrations'));
140
- return currentVersion;
141
- }
142
- for (const migration of pendingMigrations) {
143
- this.log('Running migration', migration.filename);
144
- const migrator = await this.readMigrationFile(migration.filename);
145
- if (migrator.useTransaction !== false) {
146
- await this.postgres.transaction(async () => {
147
- await this.migrate(migrator, direction);
148
- await this.updateCurrentVersion(migration.version);
149
- });
150
- }
151
- else {
152
- await this.postgres.withConnection(async () => {
153
- await this.migrate(migrator, direction);
154
- await this.updateCurrentVersion(migration.version);
155
- });
156
- }
157
- }
158
- this.log(chalk.green('Done!'));
159
- return await this.getCurrentVersion();
160
- }
161
- else if (direction === 'down') {
162
- const previousMigrations = this.getPreviousMigrations(migrations, currentVersion);
163
- const previousMigration = previousMigrations[0];
164
- const updatedVersion = previousMigration ? previousMigration.version : 0;
165
- const migrationToRollback = migrations.find((migration) => migration.version === currentVersion);
166
- if (!migrationToRollback) {
167
- this.log(chalk.green('No migrations have been run, so nothing to rollback to.'));
168
- return currentVersion;
169
- }
170
- this.log('Rolling back migration', migrationToRollback.filename);
171
- const migrator = await this.readMigrationFile(migrationToRollback.filename);
172
- if (migrator.useTransaction !== false) {
173
- await this.postgres.transaction(async () => {
174
- await this.migrate(migrator, 'down');
175
- await this.updateCurrentVersion(updatedVersion);
176
- });
177
- }
178
- else {
179
- await this.postgres.withConnection(async () => {
180
- await this.migrate(migrator, 'down');
181
- await this.updateCurrentVersion(updatedVersion);
182
- });
183
- }
184
- return await this.getCurrentVersion();
185
- /* c8 ignore next 2 */
186
- }
187
- throw new Error(`Invalid direction ${direction}`);
188
- }
189
- async readMigrationFile(filename) {
190
- if (!filename.endsWith('.sql')) {
191
- return import(filename);
192
- }
193
- const content = await fs.readFile(filename, 'utf-8');
194
- const [up, down] = content.split(/[-\s]+ROLLBACK[-\s]+/i);
195
- return {
196
- useTransaction: !/[-\s]+NO[\s]*TRANSACTION[-\s]+/i.test(content),
197
- up() {
198
- return up.trim();
199
- },
200
- down() {
201
- return (down || '').trim();
202
- },
203
- };
204
- }
205
- async migrate(migrator, direction) {
206
- try {
207
- const queryWithLog = (statement) => {
208
- this.log(chalk.yellow(formatSql(statement)));
209
- return this.postgres.query(statement);
210
- };
211
- const returnValue = await migrator[direction](queryWithLog.bind(this.postgres));
212
- if (typeof returnValue === 'string') {
213
- if (returnValue !== '') {
214
- await queryWithLog(returnValue);
215
- }
216
- }
217
- else {
218
- await returnValue;
219
- }
220
- /* c8 ignore next 3 */
221
- }
222
- catch (error) {
223
- throw new Error(`Migration failed: ${error.message}`);
224
- }
225
- }
226
- updateCurrentVersion(version) {
227
- return this.postgres.query(`INSERT INTO ${this.migrationTableName} (version, run_at) VALUES ($1, $2)`, [version, new Date()]);
228
- }
229
- async getCurrentVersion() {
230
- const result = await this.postgres.first(`SELECT version FROM ${this.migrationTableName} ORDER BY run_at DESC LIMIT 1`);
231
- return result ? result.version : 0;
232
- }
233
- async getMigrations() {
234
- const files = await fs.readdir(this.pathToMigrations);
235
- return files
236
- .map((filename) => filename.match(this.filenameRegExp))
237
- .filter(notNullOrUndefined)
238
- .map(([filename, version, name]) => ({
239
- filename: path.join(this.pathToMigrations, filename),
240
- version: parseInt(version, 10),
241
- name,
242
- }))
243
- .sort((a, b) => a.version - b.version);
244
- }
245
- getPreviousMigrations(migrations, currentVersion) {
246
- const reversedMigrations = migrations.reverse();
247
- return reversedMigrations.filter((migration) => migration.version < currentVersion);
248
- }
249
- getPendingMigrations(migrations, currentVersion) {
250
- return migrations.filter((migration) => migration.version > currentVersion);
251
- }
252
- }
253
- function notNullOrUndefined(x) {
254
- return x != null;
255
- }
@@ -1,11 +0,0 @@
1
- export interface GenerateDatabaseTypesOptions {
2
- outputPath: string;
3
- connectionString: string;
4
- schema?: string;
5
- excludeTables?: string[];
6
- includeTables?: string[];
7
- }
8
- /**
9
- * Generates complete TypeScript definitions from database schema
10
- */
11
- export declare function generateDatabaseTypes(options: GenerateDatabaseTypesOptions): Promise<void>;