orchid-orm 1.4.20 → 1.4.21

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orchid-orm",
3
- "version": "1.4.20",
3
+ "version": "1.4.21",
4
4
  "description": "Postgres ORM",
5
5
  "homepage": "https://orchid-orm.netlify.app/guide/orm-setup-and-overview.html",
6
6
  "repository": {
@@ -31,14 +31,14 @@
31
31
  "author": "Roman Kushyn",
32
32
  "license": "ISC",
33
33
  "dependencies": {
34
- "pqb": "0.8.3"
34
+ "pqb": "0.8.4"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@swc/core": "^1.3.19",
38
38
  "rollup": "^2.79.0",
39
39
  "rollup-plugin-dts": "^4.2.2",
40
40
  "rollup-plugin-esbuild": "^4.10.1",
41
- "orchid-orm-schema-to-zod": "0.2.3",
41
+ "orchid-orm-schema-to-zod": "0.2.4",
42
42
  "@swc/jest": "^0.2.21",
43
43
  "@types/jest": "^28.1.2",
44
44
  "@types/node": "^18.0.1",
@@ -50,7 +50,7 @@
50
50
  "rimraf": "^3.0.2",
51
51
  "tslib": "^2.4.0",
52
52
  "typescript": "^4.7.4",
53
- "rake-db": "2.2.3"
53
+ "rake-db": "2.2.4"
54
54
  },
55
55
  "peerDependencies": {
56
56
  "typescript": "*"
@@ -3,6 +3,7 @@ import { asMock, ast } from './testUtils';
3
3
  import { updateMainFile } from './updateMainFile';
4
4
  import * as path from 'path';
5
5
  import { updateTableFile } from './updateTableFile/updateTableFile';
6
+ import { createBaseTableFile } from './createBaseTableFile';
6
7
 
7
8
  jest.mock('./updateMainFile', () => ({
8
9
  updateMainFile: jest.fn(),
@@ -12,18 +13,24 @@ jest.mock('./updateTableFile/updateTableFile', () => ({
12
13
  updateTableFile: jest.fn(),
13
14
  }));
14
15
 
16
+ jest.mock('./createBaseTableFile', () => ({
17
+ createBaseTableFile: jest.fn(() => Promise.resolve()),
18
+ }));
19
+
15
20
  describe('appCodeUpdater', () => {
16
- it('should call table and file updaters with proper arguments', async () => {
17
- const params = {
18
- tablePath: (table: string) => `tables/${table}.ts`,
19
- baseTablePath: 'baseTable.ts',
20
- baseTableName: 'BaseTable',
21
- mainFilePath: 'db.ts',
22
- };
21
+ beforeEach(jest.clearAllMocks);
23
22
 
24
- const fn = appCodeUpdater(params);
23
+ const params = {
24
+ tablePath: (table: string) => `tables/${table}.ts`,
25
+ baseTablePath: 'baseTable.ts',
26
+ baseTableName: 'BaseTable',
27
+ mainFilePath: 'db.ts',
28
+ };
25
29
 
26
- await fn(ast.addTable);
30
+ const fn = appCodeUpdater(params);
31
+
32
+ it('should call table and file updaters with proper arguments', async () => {
33
+ await fn({ ast: ast.addTable, options: {}, cache: {} });
27
34
 
28
35
  const mainFilePath = path.resolve(params.mainFilePath);
29
36
  const tablePath = path.resolve(params.tablePath('table'));
@@ -38,5 +45,22 @@ describe('appCodeUpdater', () => {
38
45
  expect(table.baseTablePath).toBe(params.baseTablePath);
39
46
  expect(table.baseTableName).toBe(params.baseTableName);
40
47
  expect(table.mainFilePath).toBe(mainFilePath);
48
+
49
+ const [base] = asMock(createBaseTableFile).mock.calls[0];
50
+ expect(base.baseTablePath).toBe(params.baseTablePath);
51
+ expect(base.baseTableName).toBe(params.baseTableName);
52
+ });
53
+
54
+ it('should call createBaseTable only on first call', async () => {
55
+ const cache = {};
56
+ expect(createBaseTableFile).not.toBeCalled();
57
+
58
+ await fn({ ast: ast.addTable, options: {}, cache });
59
+
60
+ expect(createBaseTableFile).toBeCalledTimes(1);
61
+
62
+ await fn({ ast: ast.addTable, options: {}, cache });
63
+
64
+ expect(createBaseTableFile).toBeCalledTimes(1);
41
65
  });
42
66
  });
@@ -2,6 +2,7 @@ import { AppCodeUpdater } from 'rake-db';
2
2
  import * as path from 'path';
3
3
  import { updateMainFile } from './updateMainFile';
4
4
  import { updateTableFile } from './updateTableFile/updateTableFile';
5
+ import { createBaseTableFile } from './createBaseTableFile';
5
6
 
6
7
  export class AppCodeUpdaterError extends Error {}
7
8
 
@@ -21,10 +22,21 @@ export const appCodeUpdater = (
21
22
  mainFilePath: path.resolve(config.mainFilePath),
22
23
  };
23
24
 
24
- return async (ast) => {
25
- await Promise.all([
26
- updateMainFile(params.mainFilePath, params.tablePath, ast),
25
+ return async ({ ast, options, cache: cacheObject }) => {
26
+ const promises: Promise<void>[] = [
27
+ updateMainFile(params.mainFilePath, params.tablePath, ast, options),
27
28
  updateTableFile({ ...params, ast }),
28
- ]);
29
+ ];
30
+
31
+ const cache = cacheObject as { createdBaseTable?: true };
32
+ if (!cache.createdBaseTable) {
33
+ promises.push(
34
+ createBaseTableFile(params).then(() => {
35
+ cache.createdBaseTable = true;
36
+ }),
37
+ );
38
+ }
39
+
40
+ await Promise.all(promises);
29
41
  };
30
42
  };
@@ -0,0 +1,58 @@
1
+ import path from 'path';
2
+ import { createBaseTableFile } from './createBaseTableFile';
3
+ import fs from 'fs/promises';
4
+ import { asMock } from './testUtils';
5
+
6
+ jest.mock('fs/promises', () => ({
7
+ mkdir: jest.fn(),
8
+ writeFile: jest.fn(),
9
+ }));
10
+
11
+ const params = {
12
+ baseTablePath: path.resolve('baseTable.ts'),
13
+ baseTableName: 'CustomName',
14
+ };
15
+
16
+ describe('createBaseTableFile', () => {
17
+ it('should call mkdir with recursive option', async () => {
18
+ asMock(fs.writeFile).mockResolvedValue(null);
19
+
20
+ await createBaseTableFile(params);
21
+
22
+ expect(fs.mkdir).toBeCalledWith(path.dirname(params.baseTablePath), {
23
+ recursive: true,
24
+ });
25
+ });
26
+
27
+ it('should write file with wx flag to not overwrite', async () => {
28
+ asMock(fs.writeFile).mockRejectedValueOnce(
29
+ Object.assign(new Error(), { code: 'EEXIST' }),
30
+ );
31
+
32
+ await createBaseTableFile(params);
33
+
34
+ expect(asMock(fs.writeFile)).toBeCalledWith(
35
+ params.baseTablePath,
36
+ `import { createBaseTable } from 'orchid-orm';
37
+ import { columnTypes } from 'pqb';
38
+
39
+ export const ${params.baseTableName} = createBaseTable({
40
+ columnTypes: {
41
+ ...columnTypes,
42
+ },
43
+ });
44
+ `,
45
+ {
46
+ flag: 'wx',
47
+ },
48
+ );
49
+ });
50
+
51
+ it('should throw if error is not EEXIST', async () => {
52
+ asMock(fs.writeFile).mockRejectedValueOnce(
53
+ Object.assign(new Error('custom'), { code: 'other' }),
54
+ );
55
+
56
+ await expect(() => createBaseTableFile(params)).rejects.toThrow('custom');
57
+ });
58
+ });
@@ -0,0 +1,36 @@
1
+ import { AppCodeUpdaterConfig } from './appCodeUpdater';
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
+
5
+ type CreateBaseTableFileParams = Pick<
6
+ AppCodeUpdaterConfig,
7
+ 'baseTablePath' | 'baseTableName'
8
+ >;
9
+
10
+ export const createBaseTableFile = async ({
11
+ baseTableName,
12
+ baseTablePath,
13
+ }: CreateBaseTableFileParams) => {
14
+ await fs.mkdir(path.dirname(baseTablePath), { recursive: true });
15
+
16
+ await fs
17
+ .writeFile(
18
+ baseTablePath,
19
+ `import { createBaseTable } from 'orchid-orm';
20
+ import { columnTypes } from 'pqb';
21
+
22
+ export const ${baseTableName} = createBaseTable({
23
+ columnTypes: {
24
+ ...columnTypes,
25
+ },
26
+ });
27
+ `,
28
+ {
29
+ flag: 'wx',
30
+ },
31
+ )
32
+ .catch((err) => {
33
+ if (err.code === 'EEXIST') return;
34
+ throw err;
35
+ });
36
+ };
File without changes
File without changes
File without changes
@@ -6,25 +6,44 @@ import { asMock, ast, makeTestWritten, tablePath } from './testUtils';
6
6
  jest.mock('fs/promises', () => ({
7
7
  readFile: jest.fn(),
8
8
  writeFile: jest.fn(),
9
+ mkdir: jest.fn(),
9
10
  }));
10
11
 
11
12
  const mainFilePath = path.resolve('db.ts');
12
13
  const testWritten = makeTestWritten(mainFilePath);
14
+ const options = { databaseURL: 'url' };
13
15
 
14
16
  describe('updateMainFile', () => {
15
17
  beforeEach(() => {
16
18
  jest.resetAllMocks();
17
19
  });
18
20
 
19
- it('should throw when file is not found', async () => {
20
- asMock(fs.readFile).mockRejectedValue(new Error());
21
+ describe('add table', () => {
22
+ it('should create file if not exist and add a table', async () => {
23
+ asMock(fs.readFile).mockRejectedValue(
24
+ Object.assign(new Error(), { code: 'ENOENT' }),
25
+ );
21
26
 
22
- await expect(() =>
23
- updateMainFile(mainFilePath, tablePath, ast.addTable),
24
- ).rejects.toThrow();
25
- });
27
+ await updateMainFile(mainFilePath, tablePath, ast.addTable, options);
28
+
29
+ expect(asMock(fs.mkdir)).toBeCalledWith(path.dirname(mainFilePath), {
30
+ recursive: true,
31
+ });
32
+
33
+ testWritten(`import { orchidORM } from 'orchid-orm';
34
+ import { Table } from './tables/table';
35
+
36
+ export const db = orchidORM(
37
+ {
38
+ databaseURL: 'url',
39
+ },
40
+ {
41
+ table: Table,
42
+ }
43
+ );
44
+ `);
45
+ });
26
46
 
27
- describe('add table', () => {
28
47
  it('should add table', async () => {
29
48
  asMock(fs.readFile).mockResolvedValue(`
30
49
  import { orchidORM } from 'orchid-orm';
@@ -32,7 +51,7 @@ import { orchidORM } from 'orchid-orm';
32
51
  export const db = orchidORM({}, {});
33
52
  `);
34
53
 
35
- await updateMainFile(mainFilePath, tablePath, ast.addTable);
54
+ await updateMainFile(mainFilePath, tablePath, ast.addTable, options);
36
55
 
37
56
  testWritten(`
38
57
  import { orchidORM } from 'orchid-orm';
@@ -51,7 +70,7 @@ import { orchidORM as custom } from 'orchid-orm';
51
70
  export const db = custom({}, {});
52
71
  `);
53
72
 
54
- await updateMainFile(mainFilePath, tablePath, ast.addTable);
73
+ await updateMainFile(mainFilePath, tablePath, ast.addTable, options);
55
74
 
56
75
  testWritten(`
57
76
  import { orchidORM as custom } from 'orchid-orm';
@@ -73,7 +92,7 @@ export const db = orchidORM({}, {
73
92
  });
74
93
  `);
75
94
 
76
- await updateMainFile(mainFilePath, tablePath, ast.addTable);
95
+ await updateMainFile(mainFilePath, tablePath, ast.addTable, options);
77
96
 
78
97
  testWritten(`
79
98
  import { orchidORM } from 'orchid-orm';
@@ -97,7 +116,7 @@ export const db = orchidORM({}, {
97
116
  });
98
117
  `);
99
118
 
100
- await updateMainFile(mainFilePath, tablePath, ast.addTable);
119
+ await updateMainFile(mainFilePath, tablePath, ast.addTable, options);
101
120
 
102
121
  testWritten(`
103
122
  import { orchidORM } from 'orchid-orm';
@@ -123,7 +142,7 @@ export const db = orchidORM({}, {
123
142
  });
124
143
  `);
125
144
 
126
- await updateMainFile(mainFilePath, tablePath, ast.dropTable);
145
+ await updateMainFile(mainFilePath, tablePath, ast.dropTable, options);
127
146
 
128
147
  testWritten(`
129
148
  import { orchidORM } from 'orchid-orm';
@@ -143,7 +162,7 @@ export const db = orchidORM({}, {
143
162
  });
144
163
  `);
145
164
 
146
- await updateMainFile(mainFilePath, tablePath, ast.dropTable);
165
+ await updateMainFile(mainFilePath, tablePath, ast.dropTable, options);
147
166
 
148
167
  testWritten(`
149
168
  import { orchidORM } from 'orchid-orm';
@@ -163,7 +182,7 @@ export const db = orchidORM({}, {
163
182
  });
164
183
  `);
165
184
 
166
- await updateMainFile(mainFilePath, tablePath, ast.dropTable);
185
+ await updateMainFile(mainFilePath, tablePath, ast.dropTable, options);
167
186
 
168
187
  testWritten(`
169
188
  import { orchidORM } from 'orchid-orm';
@@ -187,7 +206,7 @@ export const db = orchidORM({}, {
187
206
  });
188
207
  `);
189
208
 
190
- await updateMainFile(mainFilePath, tablePath, ast.dropTable);
209
+ await updateMainFile(mainFilePath, tablePath, ast.dropTable, options);
191
210
 
192
211
  testWritten(`
193
212
  import { orchidORM } from 'orchid-orm';
@@ -1,14 +1,16 @@
1
1
  import { RakeDbAst } from 'rake-db';
2
2
  import fs from 'fs/promises';
3
+ import path from 'path';
3
4
  import { NodeArray, ObjectLiteralExpression, Statement } from 'typescript';
4
5
  import { toCamelCase, toPascalCase } from '../utils';
5
6
  import { AppCodeUpdaterError } from './appCodeUpdater';
6
7
  import { FileChanges } from './fileChanges';
7
8
  import { ts } from './tsUtils';
8
9
  import { getImportPath } from './utils';
10
+ import { AdapterOptions, singleQuote } from 'pqb';
9
11
 
10
12
  type Context = {
11
- path: string;
13
+ filePath: string;
12
14
  tablePath: (name: string) => string;
13
15
  statements: NodeArray<Statement>;
14
16
  object: ObjectLiteralExpression;
@@ -19,12 +21,48 @@ type Context = {
19
21
  const libraryName = 'orchid-orm';
20
22
  const importKey = 'orchidORM';
21
23
 
24
+ const newFile = (
25
+ options: AdapterOptions,
26
+ ) => `import { orchidORM } from 'orchid-orm';
27
+
28
+ export const db = orchidORM(
29
+ {
30
+ ${optionsToString(options)}
31
+ },
32
+ {
33
+ }
34
+ );
35
+ `;
36
+
37
+ const optionsToString = (options: AdapterOptions) => {
38
+ const lines: string[] = [];
39
+ for (const key in options) {
40
+ const value = options[key as keyof AdapterOptions];
41
+ if (typeof value !== 'object' && typeof value !== 'function') {
42
+ lines.push(
43
+ `${key}: ${typeof value === 'string' ? singleQuote(value) : value},`,
44
+ );
45
+ }
46
+ }
47
+ return lines.join('\n ');
48
+ };
49
+
22
50
  export const updateMainFile = async (
23
- path: string,
51
+ filePath: string,
24
52
  tablePath: (name: string) => string,
25
53
  ast: RakeDbAst,
54
+ options: AdapterOptions,
26
55
  ) => {
27
- const content = await fs.readFile(path, 'utf-8');
56
+ const result = await fs.readFile(filePath, 'utf-8').then(
57
+ (content) => ({ error: undefined, content }),
58
+ (error) => {
59
+ return { error, content: undefined };
60
+ },
61
+ );
62
+
63
+ if (result.error && result.error.code !== 'ENOENT') throw result.error;
64
+ const content = result.content || newFile(options);
65
+
28
66
  const statements = ts.getStatements(content);
29
67
 
30
68
  const importName = ts.import.getStatementsImportedName(
@@ -46,7 +84,7 @@ export const updateMainFile = async (
46
84
  const spaces = ts.spaces.getAtLine(content, object.end);
47
85
 
48
86
  const context: Context = {
49
- path,
87
+ filePath,
50
88
  tablePath,
51
89
  statements,
52
90
  object,
@@ -54,20 +92,28 @@ export const updateMainFile = async (
54
92
  spaces,
55
93
  };
56
94
 
95
+ let write: string | undefined;
57
96
  if (ast.type === 'table') {
58
97
  if (ast.action === 'create') {
59
- return fs.writeFile(path, createTable(context, ast));
98
+ write = createTable(context, ast);
60
99
  } else {
61
- return fs.writeFile(path, dropTable(context, ast));
100
+ write = dropTable(context, ast);
62
101
  }
63
102
  }
64
-
65
103
  // rename table is not handled because renaming of the class and the file is better to be done by the editor,
66
104
  // editor can scan all project files, rename import path and imported class name
105
+
106
+ if (write) {
107
+ if (result.error) {
108
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
109
+ }
110
+
111
+ await fs.writeFile(filePath, write);
112
+ }
67
113
  };
68
114
 
69
115
  const createTable = (
70
- { path, tablePath, statements, object, content, spaces }: Context,
116
+ { filePath, tablePath, statements, object, content, spaces }: Context,
71
117
  ast: RakeDbAst.Table,
72
118
  ) => {
73
119
  const key = toCamelCase(ast.name);
@@ -75,7 +121,7 @@ const createTable = (
75
121
 
76
122
  const changes = new FileChanges(content);
77
123
 
78
- const importPath = getImportPath(path, tablePath(ast.name));
124
+ const importPath = getImportPath(filePath, tablePath(ast.name));
79
125
  const importPos = ts.import.getEndPos(statements);
80
126
  changes.add(
81
127
  importPos,
@@ -95,12 +141,12 @@ const createTable = (
95
141
  };
96
142
 
97
143
  const dropTable = (
98
- { path, tablePath, statements, object, content }: Context,
144
+ { filePath, tablePath, statements, object, content }: Context,
99
145
  ast: RakeDbAst.Table,
100
146
  ) => {
101
147
  const changes = new FileChanges(content);
102
148
 
103
- const importPath = getImportPath(path, tablePath(ast.name));
149
+ const importPath = getImportPath(filePath, tablePath(ast.name));
104
150
  const tableClassName = toPascalCase(ast.name);
105
151
  const importNames: string[] = [];
106
152
  for (const node of ts.import.iterateWithSource(statements, importPath)) {
@@ -6,6 +6,7 @@ import { columnTypes, newTableData, TableData } from 'pqb';
6
6
  import { RakeDbAst } from 'rake-db';
7
7
 
8
8
  jest.mock('fs/promises', () => ({
9
+ mkdir: jest.fn(),
9
10
  readFile: jest.fn(),
10
11
  writeFile: jest.fn(),
11
12
  }));
@@ -1,10 +1,12 @@
1
1
  import { updateTableFile } from './updateTableFile';
2
- import { ast, makeTestWritten, tablePath } from '../testUtils';
2
+ import { asMock, ast, makeTestWritten, tablePath } from '../testUtils';
3
3
  import path from 'path';
4
+ import fs from 'fs/promises';
4
5
 
5
6
  jest.mock('fs/promises', () => ({
6
7
  readFile: jest.fn(),
7
8
  writeFile: jest.fn(),
9
+ mkdir: jest.fn(),
8
10
  }));
9
11
 
10
12
  const baseTablePath = path.resolve('baseTable.ts');
@@ -44,6 +46,10 @@ describe('createTable', () => {
44
46
  },
45
47
  });
46
48
 
49
+ expect(asMock(fs.mkdir)).toBeCalledWith(path.dirname(tablePath('table')), {
50
+ recursive: true,
51
+ });
52
+
47
53
  testWritten(`import { BaseTable } from '../baseTable';
48
54
 
49
55
  export class Table extends BaseTable {
@@ -64,7 +70,8 @@ export class Table extends BaseTable {
64
70
  },
65
71
  ),
66
72
  }));
67
- }`);
73
+ }
74
+ `);
68
75
  });
69
76
 
70
77
  it('should add noPrimaryKey prop when noPrimaryKey is `ignore` in ast', async () => {
@@ -81,6 +88,7 @@ export class Table extends BaseTable {
81
88
  columns = this.setColumns((t) => ({
82
89
  id: t.serial().primaryKey(),
83
90
  }));
84
- }`);
91
+ }
92
+ `);
85
93
  });
86
94
  });
@@ -4,6 +4,7 @@ import { Code, codeToString, columnsShapeToCode, singleQuote } from 'pqb';
4
4
  import { toPascalCase } from '../../utils';
5
5
  import fs from 'fs/promises';
6
6
  import { UpdateTableFileParams } from './updateTableFile';
7
+ import path from 'path';
7
8
 
8
9
  export const createTable = async ({
9
10
  ast,
@@ -27,8 +28,9 @@ export const createTable = async ({
27
28
  `import { ${params.baseTableName} } from '${baseTablePath}';\n`,
28
29
  `export class ${toPascalCase(ast.name)} extends ${params.baseTableName} {`,
29
30
  props,
30
- '}',
31
+ '}\n',
31
32
  ];
32
33
 
34
+ await fs.mkdir(path.dirname(tablePath), { recursive: true });
33
35
  await fs.writeFile(tablePath, codeToString(code, '', ' '));
34
36
  };
@@ -4,6 +4,7 @@ import path from 'path';
4
4
  import fs from 'fs/promises';
5
5
 
6
6
  jest.mock('fs/promises', () => ({
7
+ mkdir: jest.fn(),
7
8
  readFile: jest.fn(),
8
9
  writeFile: jest.fn(),
9
10
  }));
File without changes
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export * from './table';
2
2
  export * from './orm';
3
3
  export * from './repo';
4
- export * from './appCodeUpdater/appCodeUpdater';
4
+ export * from './codegen/appCodeUpdater';