@smartive/graphql-magic 2.1.1 → 3.0.1
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/.eslintrc +1 -1
- package/.github/workflows/release.yml +1 -1
- package/.github/workflows/testing.yml +3 -2
- package/.nvmrc +1 -1
- package/CHANGELOG.md +2 -2
- package/dist/cjs/index.cjs +8 -6
- package/dist/esm/migrations/generate.js.map +1 -1
- package/dist/esm/permissions/check.js.map +1 -1
- package/dist/esm/permissions/generate.js.map +1 -1
- package/dist/esm/resolvers/filters.js.map +1 -1
- package/dist/esm/resolvers/mutations.js +7 -6
- package/dist/esm/resolvers/mutations.js.map +1 -1
- package/dist/esm/resolvers/node.js.map +1 -1
- package/dist/esm/resolvers/resolver.js.map +1 -1
- package/dist/esm/resolvers/utils.js.map +1 -1
- package/docker-compose.yml +17 -0
- package/package.json +16 -9
- package/src/migrations/generate.ts +9 -9
- package/src/permissions/check.ts +1 -1
- package/src/permissions/generate.ts +7 -7
- package/src/resolvers/filters.ts +3 -3
- package/src/resolvers/mutations.ts +17 -14
- package/src/resolvers/node.ts +1 -1
- package/src/resolvers/resolver.ts +2 -2
- package/src/resolvers/utils.ts +2 -2
- package/tests/api/__snapshots__/delete.spec.ts.snap +18 -0
- package/tests/api/__snapshots__/query.spec.ts.snap +22 -0
- package/tests/api/delete.spec.ts +28 -0
- package/tests/api/query.spec.ts +28 -0
- package/tests/unit/__snapshots__/generate.spec.ts.snap +20 -0
- package/tests/unit/__snapshots__/resolve.spec.ts.snap +3 -0
- package/tests/unit/generate.spec.ts +2 -2
- package/tests/unit/resolve.spec.ts +2 -2
- package/tests/utils/database/knex.ts +19 -0
- package/tests/utils/database/schema.ts +62 -0
- package/tests/utils/database/seed.ts +54 -0
- package/tests/utils/generate-migration.ts +35 -0
- package/tests/{unit/utils.ts → utils/models.ts} +26 -5
- package/tests/utils/server.ts +108 -0
- package/tsconfig.json +1 -1
|
@@ -71,7 +71,7 @@ export const generatePermissions = (models: Models, config: PermissionsConfig) =
|
|
|
71
71
|
rolePermissions[type] = {};
|
|
72
72
|
for (const action of ACTIONS) {
|
|
73
73
|
if (action === 'READ' || action in block) {
|
|
74
|
-
rolePermissions[type]
|
|
74
|
+
rolePermissions[type][action] = true;
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
}
|
|
@@ -95,7 +95,7 @@ export const generatePermissions = (models: Models, config: PermissionsConfig) =
|
|
|
95
95
|
};
|
|
96
96
|
|
|
97
97
|
const addPermissions = (models: Models, permissions: RolePermissions, links: PermissionLink[], block: PermissionsBlock) => {
|
|
98
|
-
const { type } = links[links.length - 1]
|
|
98
|
+
const { type } = links[links.length - 1];
|
|
99
99
|
const model = summonByName(models, type);
|
|
100
100
|
|
|
101
101
|
for (const action of ACTIONS) {
|
|
@@ -103,11 +103,11 @@ const addPermissions = (models: Models, permissions: RolePermissions, links: Per
|
|
|
103
103
|
if (!permissions[type]) {
|
|
104
104
|
permissions[type] = {};
|
|
105
105
|
}
|
|
106
|
-
if (!permissions[type]
|
|
107
|
-
permissions[type]
|
|
106
|
+
if (!permissions[type][action]) {
|
|
107
|
+
permissions[type][action] = [];
|
|
108
108
|
}
|
|
109
|
-
if (permissions[type]
|
|
110
|
-
(permissions[type]
|
|
109
|
+
if (permissions[type][action] !== true) {
|
|
110
|
+
(permissions[type][action] as PermissionStack).push(links);
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
113
|
}
|
|
@@ -123,7 +123,7 @@ const addPermissions = (models: Models, permissions: RolePermissions, links: Per
|
|
|
123
123
|
reverse: true,
|
|
124
124
|
};
|
|
125
125
|
} else {
|
|
126
|
-
const field = model.reverseRelationsByName[relation]
|
|
126
|
+
const field = model.reverseRelationsByName[relation];
|
|
127
127
|
|
|
128
128
|
if (!field) {
|
|
129
129
|
throw new Error(`Relation ${relation} in model ${model.name} does not exist.`);
|
package/src/resolvers/filters.ts
CHANGED
|
@@ -72,12 +72,12 @@ const applyWhere = (node: WhereNode, where: Where, ops: Ops<Knex.QueryBuilder>,
|
|
|
72
72
|
const specialFilter = key.match(/^(\w+)_(\w+)$/);
|
|
73
73
|
if (specialFilter) {
|
|
74
74
|
const [, actualKey, filter] = specialFilter;
|
|
75
|
-
if (!SPECIAL_FILTERS[filter
|
|
75
|
+
if (!SPECIAL_FILTERS[filter]) {
|
|
76
76
|
// Should not happen
|
|
77
77
|
throw new Error(`Invalid filter ${key}.`);
|
|
78
78
|
}
|
|
79
79
|
ops.push((query) =>
|
|
80
|
-
query.whereRaw(SPECIAL_FILTERS[filter
|
|
80
|
+
query.whereRaw(SPECIAL_FILTERS[filter], [`${node.shortTableAlias}.${actualKey}`, value as string])
|
|
81
81
|
);
|
|
82
82
|
continue;
|
|
83
83
|
}
|
|
@@ -154,7 +154,7 @@ const applyOrderBy = (node: FieldResolverNode, orderBy: OrderBy, query: Knex.Que
|
|
|
154
154
|
throw new UserInputError(`You need to specify exactly 1 value to order by for each orderBy entry.`);
|
|
155
155
|
}
|
|
156
156
|
const key = keys[0];
|
|
157
|
-
const value = vals[key
|
|
157
|
+
const value = vals[key];
|
|
158
158
|
|
|
159
159
|
// Simple field
|
|
160
160
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises -- we do not need to await knex here
|
|
@@ -12,7 +12,7 @@ export const mutationResolver = async (_parent: any, args: any, partialCtx: Cont
|
|
|
12
12
|
return await partialCtx.knex.transaction(async (knex) => {
|
|
13
13
|
const [, mutation, modelName] = it(info.fieldName.match(/^(create|update|delete|restore)(.+)$/));
|
|
14
14
|
const ctx = { ...partialCtx, knex, info, aliases: new AliasGenerator() };
|
|
15
|
-
const model = summonByName(ctx.models, modelName
|
|
15
|
+
const model = summonByName(ctx.models, modelName);
|
|
16
16
|
switch (mutation) {
|
|
17
17
|
case 'create':
|
|
18
18
|
return await create(model, args, ctx);
|
|
@@ -110,17 +110,20 @@ const del = async (model: Model, { where, dryRun }: { where: any; dryRun: boolea
|
|
|
110
110
|
return;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
if (
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
113
|
+
if (!(currentModel.name in toDelete)) {
|
|
114
|
+
toDelete[currentModel.name] = {};
|
|
115
|
+
}
|
|
116
|
+
if (entity.id in toDelete[currentModel.name]) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
toDelete[currentModel.name][entity.id] = entity[currentModel.displayField || 'id'] || entity.id;
|
|
120
|
+
|
|
121
|
+
if (!dryRun) {
|
|
119
122
|
const normalizedInput = { deleted: true, deletedAt: ctx.now, deletedById: ctx.user.id };
|
|
120
123
|
const data = { prev: entity, input: {}, normalizedInput, next: { ...entity, ...normalizedInput } };
|
|
121
124
|
if (ctx.mutationHook) {
|
|
122
125
|
beforeHooks.push(async () => {
|
|
123
|
-
await ctx.mutationHook
|
|
126
|
+
await ctx.mutationHook(currentModel, 'delete', 'before', data, ctx);
|
|
124
127
|
});
|
|
125
128
|
}
|
|
126
129
|
mutations.push(async () => {
|
|
@@ -129,7 +132,7 @@ const del = async (model: Model, { where, dryRun }: { where: any; dryRun: boolea
|
|
|
129
132
|
});
|
|
130
133
|
if (ctx.mutationHook) {
|
|
131
134
|
afterHooks.push(async () => {
|
|
132
|
-
await ctx.mutationHook
|
|
135
|
+
await ctx.mutationHook(currentModel, 'delete', 'after', data, ctx);
|
|
133
136
|
});
|
|
134
137
|
}
|
|
135
138
|
}
|
|
@@ -148,13 +151,13 @@ const del = async (model: Model, { where, dryRun }: { where: any; dryRun: boolea
|
|
|
148
151
|
if (!toUnlink[descendantModel.name]) {
|
|
149
152
|
toUnlink[descendantModel.name] = {};
|
|
150
153
|
}
|
|
151
|
-
if (!toUnlink[descendantModel.name]
|
|
152
|
-
toUnlink[descendantModel.name]
|
|
154
|
+
if (!toUnlink[descendantModel.name][descendant.id]) {
|
|
155
|
+
toUnlink[descendantModel.name][descendant.id] = {
|
|
153
156
|
display: descendant[descendantModel.displayField || 'id'] || entity.id,
|
|
154
157
|
fields: [],
|
|
155
158
|
};
|
|
156
159
|
}
|
|
157
|
-
toUnlink[descendantModel.name]
|
|
160
|
+
toUnlink[descendantModel.name][descendant.id].fields.push(name);
|
|
158
161
|
} else {
|
|
159
162
|
mutations.push(async () => {
|
|
160
163
|
await ctx
|
|
@@ -225,7 +228,7 @@ const restore = async (model: Model, { where }: { where: any }, ctx: FullContext
|
|
|
225
228
|
const data = { prev: relatedEntity, input: {}, normalizedInput, next: { ...relatedEntity, ...normalizedInput } };
|
|
226
229
|
if (ctx.mutationHook) {
|
|
227
230
|
beforeHooks.push(async () => {
|
|
228
|
-
await ctx.mutationHook
|
|
231
|
+
await ctx.mutationHook(model, 'restore', 'before', data, ctx);
|
|
229
232
|
});
|
|
230
233
|
}
|
|
231
234
|
mutations.push(async () => {
|
|
@@ -234,7 +237,7 @@ const restore = async (model: Model, { where }: { where: any }, ctx: FullContext
|
|
|
234
237
|
});
|
|
235
238
|
if (ctx.mutationHook) {
|
|
236
239
|
afterHooks.push(async () => {
|
|
237
|
-
await ctx.mutationHook
|
|
240
|
+
await ctx.mutationHook(model, 'restore', 'after', data, ctx);
|
|
238
241
|
});
|
|
239
242
|
}
|
|
240
243
|
|
package/src/resolvers/node.ts
CHANGED
|
@@ -132,7 +132,7 @@ export const getFragmentSpreads = (node: ResolverNode) =>
|
|
|
132
132
|
node.selectionSet.filter(isFragmentSpreadNode).map((subNode) =>
|
|
133
133
|
getResolverNode({
|
|
134
134
|
ctx: node.ctx,
|
|
135
|
-
node: node.ctx.info.fragments[subNode.name.value]
|
|
135
|
+
node: node.ctx.info.fragments[subNode.name.value],
|
|
136
136
|
tableAlias: node.tableAlias,
|
|
137
137
|
baseTypeDefinition: node.baseTypeDefinition,
|
|
138
138
|
typeName: node.model.name,
|
|
@@ -165,7 +165,7 @@ const applySubQueries = async (
|
|
|
165
165
|
if (!entriesById[entry[ID_ALIAS]]) {
|
|
166
166
|
entriesById[entry[ID_ALIAS]] = [];
|
|
167
167
|
}
|
|
168
|
-
entriesById[entry[ID_ALIAS]
|
|
168
|
+
entriesById[entry[ID_ALIAS]].push(entry);
|
|
169
169
|
}
|
|
170
170
|
const ids = Object.keys(entriesById);
|
|
171
171
|
|
|
@@ -189,7 +189,7 @@ const applySubQueries = async (
|
|
|
189
189
|
const children = hydrate(subNode, rawChildren);
|
|
190
190
|
|
|
191
191
|
for (const child of children) {
|
|
192
|
-
for (const entry of entriesById[child[foreignKey] as string]
|
|
192
|
+
for (const entry of entriesById[child[foreignKey] as string]) {
|
|
193
193
|
if (isList) {
|
|
194
194
|
(entry[fieldName] as Entry[]).push(cloneDeep(child));
|
|
195
195
|
} else {
|
package/src/resolvers/utils.ts
CHANGED
|
@@ -72,13 +72,13 @@ export function hydrate<T extends Entry>(
|
|
|
72
72
|
outer: for (const [column, value] of Object.entries(entry)) {
|
|
73
73
|
let current = res;
|
|
74
74
|
const shortParts = column.split('__');
|
|
75
|
-
const fieldName = shortParts.pop()
|
|
75
|
+
const fieldName = shortParts.pop();
|
|
76
76
|
const columnWithoutField = shortParts.join('__');
|
|
77
77
|
const longColumn = node.ctx.aliases.getLong(columnWithoutField);
|
|
78
78
|
const longColumnWithoutRoot = longColumn.replace(new RegExp(`^${tableAlias}(__)?`), '');
|
|
79
79
|
const allParts = [tableAlias, ...(longColumnWithoutRoot ? longColumnWithoutRoot.split('__') : []), fieldName];
|
|
80
80
|
for (let i = 0; i < allParts.length - 1; i++) {
|
|
81
|
-
const part = allParts[i]
|
|
81
|
+
const part = allParts[i];
|
|
82
82
|
|
|
83
83
|
if (!current[part]) {
|
|
84
84
|
const idField = [node.ctx.aliases.getShort(allParts.slice(0, i + 1).join('__')), ID_ALIAS].join('__');
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
+
|
|
3
|
+
exports[`delete works with self-referential entities 1`] = `
|
|
4
|
+
{
|
|
5
|
+
"deleteAnotherObject": "226a20e8-5c18-4423-99ca-eb0df6ff4fdd",
|
|
6
|
+
}
|
|
7
|
+
`;
|
|
8
|
+
|
|
9
|
+
exports[`delete works with self-referential entities 2`] = `
|
|
10
|
+
{
|
|
11
|
+
"anotherObjects": [
|
|
12
|
+
{
|
|
13
|
+
"deleted": true,
|
|
14
|
+
"id": "226a20e8-5c18-4423-99ca-eb0df6ff4fdd",
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
}
|
|
18
|
+
`;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
+
|
|
3
|
+
exports[`query can be executed 1`] = `
|
|
4
|
+
{
|
|
5
|
+
"manyObjects": [
|
|
6
|
+
{
|
|
7
|
+
"another": {
|
|
8
|
+
"id": "226a20e8-5c18-4423-99ca-eb0df6ff4fdd",
|
|
9
|
+
"manyObjects": [
|
|
10
|
+
{
|
|
11
|
+
"field": null,
|
|
12
|
+
"id": "604ab55d-ec3e-4857-9f27-219158f80e64",
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
},
|
|
16
|
+
"field": null,
|
|
17
|
+
"id": "604ab55d-ec3e-4857-9f27-219158f80e64",
|
|
18
|
+
"xyz": 1,
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
}
|
|
22
|
+
`;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { gql } from '../../src';
|
|
2
|
+
import { ANOTHER_ID } from '../utils/database/seed';
|
|
3
|
+
import { withServer } from '../utils/server';
|
|
4
|
+
|
|
5
|
+
describe('delete', () => {
|
|
6
|
+
it('works with self-referential entities', async () => {
|
|
7
|
+
await withServer(async (request) => {
|
|
8
|
+
expect(
|
|
9
|
+
await request(gql`
|
|
10
|
+
mutation DeleteAnotherObject {
|
|
11
|
+
deleteAnotherObject(where: { id: "${ANOTHER_ID}" })
|
|
12
|
+
}
|
|
13
|
+
`)
|
|
14
|
+
).toMatchSnapshot();
|
|
15
|
+
|
|
16
|
+
expect(
|
|
17
|
+
await request(gql`
|
|
18
|
+
query GetAnotherObject {
|
|
19
|
+
anotherObjects(where: { id: "${ANOTHER_ID}", deleted: true }) {
|
|
20
|
+
id
|
|
21
|
+
deleted
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
`)
|
|
25
|
+
).toMatchSnapshot();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { gql } from '../../src';
|
|
2
|
+
import { ANOTHER_ID, SOME_ID } from '../utils/database/seed';
|
|
3
|
+
import { withServer } from '../utils/server';
|
|
4
|
+
|
|
5
|
+
describe('query', () => {
|
|
6
|
+
it('can be executed', async () => {
|
|
7
|
+
await withServer(async (request) => {
|
|
8
|
+
expect(
|
|
9
|
+
await request(gql`
|
|
10
|
+
query SomeQuery {
|
|
11
|
+
manyObjects(where: { another: { id: "${ANOTHER_ID}" } }, orderBy: [{ xyz: DESC }]) {
|
|
12
|
+
id
|
|
13
|
+
field
|
|
14
|
+
xyz
|
|
15
|
+
another {
|
|
16
|
+
id
|
|
17
|
+
manyObjects(where: { id: "${SOME_ID}" }) {
|
|
18
|
+
id
|
|
19
|
+
field
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
`)
|
|
25
|
+
).toMatchSnapshot();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -3,11 +3,21 @@
|
|
|
3
3
|
exports[`generate generates a schema 1`] = `
|
|
4
4
|
"type AnotherObject {
|
|
5
5
|
id: ID!
|
|
6
|
+
myself: AnotherObject
|
|
7
|
+
deleted: Boolean!
|
|
8
|
+
deletedAt: DateTime
|
|
9
|
+
deletedBy: User
|
|
10
|
+
self(where: AnotherObjectWhere, orderBy: [AnotherObjectOrderBy!], limit: Int, offset: Int): AnotherObject
|
|
6
11
|
manyObjects(where: SomeObjectWhere, search: String, orderBy: [SomeObjectOrderBy!], limit: Int, offset: Int): [SomeObject!]!
|
|
7
12
|
}
|
|
8
13
|
|
|
14
|
+
input AnotherObjectOrderBy {
|
|
15
|
+
deletedAt: Order
|
|
16
|
+
}
|
|
17
|
+
|
|
9
18
|
input AnotherObjectWhere {
|
|
10
19
|
id: [ID!]
|
|
20
|
+
deleted: [Boolean!] = false
|
|
11
21
|
}
|
|
12
22
|
|
|
13
23
|
input AnotherObjectWhereUnique {
|
|
@@ -21,6 +31,8 @@ input CreateSomeObject {
|
|
|
21
31
|
scalar DateTime
|
|
22
32
|
|
|
23
33
|
type Mutation {
|
|
34
|
+
deleteAnotherObject(where: AnotherObjectWhereUnique!, dryRun: Boolean): ID!
|
|
35
|
+
restoreAnotherObject(where: AnotherObjectWhereUnique!): ID!
|
|
24
36
|
createSomeObject(data: CreateSomeObject!): SomeObject!
|
|
25
37
|
updateSomeObject(where: SomeObjectWhereUnique!, data: UpdateSomeObject!): SomeObject!
|
|
26
38
|
deleteSomeObject(where: SomeObjectWhereUnique!, dryRun: Boolean): ID!
|
|
@@ -35,9 +47,15 @@ enum Order {
|
|
|
35
47
|
type Query {
|
|
36
48
|
me: User
|
|
37
49
|
someObject(where: SomeObjectWhereUnique!): SomeObject!
|
|
50
|
+
anotherObjects(where: AnotherObjectWhere, orderBy: [AnotherObjectOrderBy!], limit: Int, offset: Int): [AnotherObject!]!
|
|
38
51
|
manyObjects(where: SomeObjectWhere, search: String, orderBy: [SomeObjectOrderBy!], limit: Int, offset: Int): [SomeObject!]!
|
|
39
52
|
}
|
|
40
53
|
|
|
54
|
+
enum Role {
|
|
55
|
+
ADMIN
|
|
56
|
+
USER
|
|
57
|
+
}
|
|
58
|
+
|
|
41
59
|
enum SomeEnum {
|
|
42
60
|
A
|
|
43
61
|
B
|
|
@@ -89,6 +107,8 @@ scalar Upload
|
|
|
89
107
|
type User {
|
|
90
108
|
id: ID!
|
|
91
109
|
username: String
|
|
110
|
+
role: Role
|
|
111
|
+
deletedAnotherObjects(where: AnotherObjectWhere, orderBy: [AnotherObjectOrderBy!], limit: Int, offset: Int): [AnotherObject!]!
|
|
92
112
|
createdManyObjects(where: SomeObjectWhere, search: String, orderBy: [SomeObjectOrderBy!], limit: Int, offset: Int): [SomeObject!]!
|
|
93
113
|
updatedManyObjects(where: SomeObjectWhere, search: String, orderBy: [SomeObjectOrderBy!], limit: Int, offset: Int): [SomeObject!]!
|
|
94
114
|
deletedManyObjects(where: SomeObjectWhere, search: String, orderBy: [SomeObjectOrderBy!], limit: Int, offset: Int): [SomeObject!]!
|
|
@@ -4,11 +4,14 @@ exports[`resolvers are generated correctly 1`] = `
|
|
|
4
4
|
{
|
|
5
5
|
"Mutation": {
|
|
6
6
|
"createSomeObject": [Function],
|
|
7
|
+
"deleteAnotherObject": [Function],
|
|
7
8
|
"deleteSomeObject": [Function],
|
|
9
|
+
"restoreAnotherObject": [Function],
|
|
8
10
|
"restoreSomeObject": [Function],
|
|
9
11
|
"updateSomeObject": [Function],
|
|
10
12
|
},
|
|
11
13
|
"Query": {
|
|
14
|
+
"anotherObjects": [Function],
|
|
12
15
|
"manyObjects": [Function],
|
|
13
16
|
"me": [Function],
|
|
14
17
|
"someObject": [Function],
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { makeExecutableSchema } from '@graphql-tools/schema';
|
|
2
2
|
import { execute, parse, Source } from 'graphql';
|
|
3
3
|
import knex from 'knex';
|
|
4
|
+
import { DateTime } from 'luxon';
|
|
4
5
|
import { gql } from '../../src/client/gql';
|
|
5
6
|
import { Context } from '../../src/context';
|
|
6
7
|
import { generate } from '../../src/generate';
|
|
7
8
|
import { getResolvers } from '../../src/resolvers';
|
|
8
|
-
import { models, permissions, rawModels } from '
|
|
9
|
-
import { DateTime } from 'luxon';
|
|
9
|
+
import { models, permissions, rawModels } from '../utils/models';
|
|
10
10
|
|
|
11
11
|
const test = async (operationName: string, query: string, variableValues: object, responses: unknown[]) => {
|
|
12
12
|
const knexInstance = knex({
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import knex from 'knex';
|
|
2
|
+
|
|
3
|
+
export const getKnex = (database = 'postgres') =>
|
|
4
|
+
knex({
|
|
5
|
+
client: 'postgresql',
|
|
6
|
+
connection: {
|
|
7
|
+
host: 'localhost',
|
|
8
|
+
database,
|
|
9
|
+
user: 'postgres',
|
|
10
|
+
password: 'password',
|
|
11
|
+
},
|
|
12
|
+
migrations: {
|
|
13
|
+
tableName: 'knex_migrations',
|
|
14
|
+
},
|
|
15
|
+
pool: {
|
|
16
|
+
min: 0,
|
|
17
|
+
max: 30,
|
|
18
|
+
},
|
|
19
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Knex } from 'knex';
|
|
2
|
+
|
|
3
|
+
export const setupSchema = async (knex: Knex) => {
|
|
4
|
+
await knex.raw(`CREATE TYPE "someEnum" AS ENUM ('A','B','C')`);
|
|
5
|
+
|
|
6
|
+
await knex.raw(`CREATE TYPE "role" AS ENUM ('ADMIN','USER')`);
|
|
7
|
+
|
|
8
|
+
await knex.schema.createTable('User', (table) => {
|
|
9
|
+
table.uuid('id').notNullable().primary();
|
|
10
|
+
table.string('username', undefined).nullable();
|
|
11
|
+
table
|
|
12
|
+
.enum('role', null as any, {
|
|
13
|
+
useNative: true,
|
|
14
|
+
existingType: true,
|
|
15
|
+
enumName: 'role',
|
|
16
|
+
})
|
|
17
|
+
.nullable();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
await knex.schema.createTable('AnotherObject', (table) => {
|
|
21
|
+
table.uuid('id').notNullable().primary();
|
|
22
|
+
table.uuid('myselfId').notNullable();
|
|
23
|
+
table.foreign('myselfId').references('id').inTable('AnotherObject');
|
|
24
|
+
table.boolean('deleted').notNullable().defaultTo(false);
|
|
25
|
+
table.timestamp('deletedAt').nullable();
|
|
26
|
+
table.uuid('deletedById').nullable();
|
|
27
|
+
table.foreign('deletedById').references('id').inTable('User');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
await knex.schema.createTable('AnotherObjectRevision', (table) => {
|
|
31
|
+
table.uuid('id').notNullable().primary();
|
|
32
|
+
table.boolean('deleted').notNullable();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
await knex.schema.createTable('SomeObject', (table) => {
|
|
36
|
+
table.uuid('id').notNullable().primary();
|
|
37
|
+
table.string('field', undefined).nullable();
|
|
38
|
+
table.uuid('anotherId').notNullable();
|
|
39
|
+
table.foreign('anotherId').references('id').inTable('AnotherObject');
|
|
40
|
+
table.decimal('list', 1, 1).notNullable();
|
|
41
|
+
table.integer('xyz').notNullable();
|
|
42
|
+
table.timestamp('createdAt').notNullable();
|
|
43
|
+
table.uuid('createdById').notNullable();
|
|
44
|
+
table.foreign('createdById').references('id').inTable('User');
|
|
45
|
+
table.timestamp('updatedAt').notNullable();
|
|
46
|
+
table.uuid('updatedById').notNullable();
|
|
47
|
+
table.foreign('updatedById').references('id').inTable('User');
|
|
48
|
+
table.boolean('deleted').notNullable().defaultTo(false);
|
|
49
|
+
table.timestamp('deletedAt').nullable();
|
|
50
|
+
table.uuid('deletedById').nullable();
|
|
51
|
+
table.foreign('deletedById').references('id').inTable('User');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
await knex.schema.createTable('SomeObjectRevision', (table) => {
|
|
55
|
+
table.uuid('id').notNullable().primary();
|
|
56
|
+
table.uuid('someObjectId').notNullable();
|
|
57
|
+
table.uuid('createdById').notNullable();
|
|
58
|
+
table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now(0));
|
|
59
|
+
table.boolean('deleted').notNullable();
|
|
60
|
+
table.integer('xyz').notNullable();
|
|
61
|
+
});
|
|
62
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Knex } from 'knex';
|
|
2
|
+
import { DateTime } from 'luxon';
|
|
3
|
+
import { summonByName } from '../../../src';
|
|
4
|
+
import { models } from '../models';
|
|
5
|
+
|
|
6
|
+
export const ADMIN_ID = '04e45b48-04cf-4b38-bb25-b9af5ae0b2c4';
|
|
7
|
+
|
|
8
|
+
export const SOME_ID = '604ab55d-ec3e-4857-9f27-219158f80e64';
|
|
9
|
+
export const ANOTHER_ID = '226a20e8-5c18-4423-99ca-eb0df6ff4fdd';
|
|
10
|
+
|
|
11
|
+
export const seed = {
|
|
12
|
+
User: [
|
|
13
|
+
{
|
|
14
|
+
id: ADMIN_ID,
|
|
15
|
+
username: 'admin',
|
|
16
|
+
role: 'ADMIN',
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
AnotherObject: [
|
|
20
|
+
{
|
|
21
|
+
id: ANOTHER_ID,
|
|
22
|
+
myselfId: ANOTHER_ID,
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
SomeObject: [
|
|
26
|
+
{
|
|
27
|
+
id: SOME_ID,
|
|
28
|
+
anotherId: ANOTHER_ID,
|
|
29
|
+
list: 0,
|
|
30
|
+
xyz: 1,
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const setupSeed = async (knex: Knex) => {
|
|
36
|
+
const now = DateTime.now();
|
|
37
|
+
for (const [table, entities] of Object.entries(seed)) {
|
|
38
|
+
const model = summonByName(models, table);
|
|
39
|
+
await knex.batchInsert(
|
|
40
|
+
table,
|
|
41
|
+
entities.map((entity, i) => ({
|
|
42
|
+
...entity,
|
|
43
|
+
...(model.creatable && {
|
|
44
|
+
createdAt: now.plus({ second: i }),
|
|
45
|
+
createdById: ADMIN_ID,
|
|
46
|
+
}),
|
|
47
|
+
...(model.updatable && {
|
|
48
|
+
updatedAt: now.plus({ second: i }),
|
|
49
|
+
updatedById: ADMIN_ID,
|
|
50
|
+
}),
|
|
51
|
+
}))
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { writeFileSync } from 'fs';
|
|
2
|
+
import { simpleGit } from 'simple-git';
|
|
3
|
+
import { MigrationGenerator } from '../../src/migrations/generate';
|
|
4
|
+
import { getKnex } from './database/knex';
|
|
5
|
+
import { rawModels } from './models';
|
|
6
|
+
|
|
7
|
+
const git = simpleGit();
|
|
8
|
+
|
|
9
|
+
const getDate = () => {
|
|
10
|
+
const date = new Date();
|
|
11
|
+
const year = date.getFullYear();
|
|
12
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
13
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
14
|
+
const hours = String(date.getHours()).padStart(2, '0');
|
|
15
|
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
16
|
+
const seconds = String(date.getSeconds()).padStart(2, '0');
|
|
17
|
+
|
|
18
|
+
return `${year}${month}${day}${hours}${minutes}${seconds}`;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const writeMigration = async () => {
|
|
22
|
+
const name = process.argv[2] || (await git.branch()).current.split('/').pop();
|
|
23
|
+
|
|
24
|
+
const knex = getKnex();
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const migrations = await new MigrationGenerator(knex, rawModels).generate();
|
|
28
|
+
|
|
29
|
+
writeFileSync(`tmp/${getDate()}_${name}.ts`, migrations);
|
|
30
|
+
} finally {
|
|
31
|
+
await knex.destroy();
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
void writeMigration();
|
|
@@ -8,6 +8,11 @@ export const rawModels: RawModels = [
|
|
|
8
8
|
type: 'enum',
|
|
9
9
|
values: ['A', 'B', 'C'],
|
|
10
10
|
},
|
|
11
|
+
{
|
|
12
|
+
name: 'Role',
|
|
13
|
+
type: 'enum',
|
|
14
|
+
values: ['ADMIN', 'USER'],
|
|
15
|
+
},
|
|
11
16
|
|
|
12
17
|
{
|
|
13
18
|
name: 'SomeRawObject',
|
|
@@ -23,6 +28,25 @@ export const rawModels: RawModels = [
|
|
|
23
28
|
name: 'username',
|
|
24
29
|
type: 'String',
|
|
25
30
|
},
|
|
31
|
+
{
|
|
32
|
+
name: 'role',
|
|
33
|
+
type: 'Role',
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
type: 'object',
|
|
39
|
+
name: 'AnotherObject',
|
|
40
|
+
listQueriable: true,
|
|
41
|
+
deletable: true,
|
|
42
|
+
fields: [
|
|
43
|
+
{
|
|
44
|
+
type: 'AnotherObject',
|
|
45
|
+
name: 'myself',
|
|
46
|
+
toOne: true,
|
|
47
|
+
reverse: 'self',
|
|
48
|
+
relation: true
|
|
49
|
+
}
|
|
26
50
|
],
|
|
27
51
|
},
|
|
28
52
|
{
|
|
@@ -51,6 +75,8 @@ export const rawModels: RawModels = [
|
|
|
51
75
|
{
|
|
52
76
|
name: 'list',
|
|
53
77
|
type: 'Float',
|
|
78
|
+
scale: 1,
|
|
79
|
+
precision: 1,
|
|
54
80
|
nonNull: true,
|
|
55
81
|
list: true,
|
|
56
82
|
args: [{ name: 'magic', type: 'Boolean' }],
|
|
@@ -66,11 +92,6 @@ export const rawModels: RawModels = [
|
|
|
66
92
|
},
|
|
67
93
|
],
|
|
68
94
|
},
|
|
69
|
-
{
|
|
70
|
-
type: 'object',
|
|
71
|
-
name: 'AnotherObject',
|
|
72
|
-
fields: [],
|
|
73
|
-
},
|
|
74
95
|
];
|
|
75
96
|
|
|
76
97
|
export const models = getModels(rawModels);
|