Skip to content
Merged

Next #428

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,5 @@ yarn-error.log*
*.tgz

/tmp

.env.local
3 changes: 2 additions & 1 deletion .gqmrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
"graphqlQueriesPath": "tests",
"gqmModule": "../../../src",
"knexfilePath": "knexfile.ts",
"dateLibrary": "luxon"
"dateLibrary": "luxon",
"functionsPath": "tests/functions.ts"
}
5 changes: 2 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ services:
postgres:
image: postgres:18-alpine@sha256:aa6eb304ddb6dd26df23d05db4e5cb05af8951cda3e0dc57731b771e0ef4ab29
shm_size: 1gb
env_file:
- .env
environment:
POSTGRES_DB: postgres
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_HOST_AUTH_METHOD: trust
ports:
- '5432:5432'
10 changes: 5 additions & 5 deletions src/db/generate.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import CodeBlockWriter from 'code-block-writer';
import { EntityField, get, getColumnName, isCustomField, isInTable, isRootModel, not } from '..';
import { and, EntityField, get, getColumnName, isDynamicField, isInTable, isRootModel, isStoredInDatabase, not } from '..';
import { Models } from '../models/models';
import { DATE_CLASS, DATE_CLASS_IMPORT, DateLibrary } from '../utils/dates';

Expand Down Expand Up @@ -60,7 +60,7 @@ export const generateDBModels = (models: Models, dateLibrary: DateLibrary) => {
writer
.write(`export type ${model.name} = `)
.inlineBlock(() => {
for (const field of fields.filter(not(isCustomField))) {
for (const field of fields.filter(isStoredInDatabase)) {
writer
.write(`'${getColumnName(field)}': ${getFieldType(field, dateLibrary)}${field.nonNull ? '' : ' | null'};`)
.newLine();
Expand All @@ -71,7 +71,7 @@ export const generateDBModels = (models: Models, dateLibrary: DateLibrary) => {
writer
.write(`export type ${model.name}Initializer = `)
.inlineBlock(() => {
for (const field of fields.filter(not(isCustomField)).filter(isInTable)) {
for (const field of fields.filter(and(isInTable, not(isDynamicField)))) {
writer
.write(
`'${getColumnName(field)}'${field.nonNull && field.defaultValue === undefined ? '' : '?'}: ${getFieldType(
Expand All @@ -88,7 +88,7 @@ export const generateDBModels = (models: Models, dateLibrary: DateLibrary) => {
writer
.write(`export type ${model.name}Mutator = `)
.inlineBlock(() => {
for (const field of fields.filter(not(isCustomField)).filter(isInTable)) {
for (const field of fields.filter(and(isInTable, not(isDynamicField)))) {
writer
.write(
`'${getColumnName(field)}'?: ${getFieldType(field, dateLibrary, true)}${field.list ? ' | string' : ''}${
Expand All @@ -104,7 +104,7 @@ export const generateDBModels = (models: Models, dateLibrary: DateLibrary) => {
writer
.write(`export type ${model.name}Seed = `)
.inlineBlock(() => {
for (const field of fields.filter(not(isCustomField))) {
for (const field of fields.filter(not(isDynamicField))) {
if (model.parent && field.name === 'type') {
continue;
}
Expand Down
17 changes: 6 additions & 11 deletions src/migrations/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
get,
isCreatableModel,
isInherited,
isStoredInDatabase,
isUpdatableField,
isUpdatableModel,
modelNeedsTable,
Expand Down Expand Up @@ -298,9 +299,7 @@ export class MigrationGenerator {
writer.writeLine(`deleteRootId: row.deleteRootId,`);
}

for (const { name, kind } of model.fields
.filter(isUpdatableField)
.filter((f) => !(f.generateAs?.type === 'expression'))) {
for (const { name, kind } of model.fields.filter(and(isUpdatableField, isStoredInDatabase))) {
const col = kind === 'relation' ? `${name}Id` : name;

writer.writeLine(`${col}: row.${col},`);
Expand Down Expand Up @@ -331,11 +330,9 @@ export class MigrationGenerator {
);

const missingRevisionFields = model.fields
.filter(isUpdatableField)
.filter(and(isUpdatableField, isStoredInDatabase))
.filter(
({ name, ...field }) =>
field.kind !== 'custom' &&
!(field.generateAs?.type === 'expression') &&
!this.getColumn(revisionTable, field.kind === 'relation' ? field.foreignKey || `${name}Id` : name),
);

Expand Down Expand Up @@ -534,7 +531,7 @@ export class MigrationGenerator {
});

if (isUpdatableModel(model)) {
const updatableFields = fields.filter(isUpdatableField).filter((f) => !(f.generateAs?.type === 'expression'));
const updatableFields = fields.filter(and(isUpdatableField, isStoredInDatabase));
if (!updatableFields.length) {
return;
}
Expand Down Expand Up @@ -588,7 +585,7 @@ export class MigrationGenerator {
});

if (isUpdatableModel(model)) {
const updatableFields = fields.filter(isUpdatableField).filter((f) => !(f.generateAs?.type === 'expression'));
const updatableFields = fields.filter(and(isUpdatableField, isStoredInDatabase));
if (!updatableFields.length) {
return;
}
Expand Down Expand Up @@ -632,9 +629,7 @@ export class MigrationGenerator {
}
}

for (const field of model.fields
.filter(and(isUpdatableField, not(isInherited)))
.filter((f) => !(f.generateAs?.type === 'expression'))) {
for (const field of model.fields.filter(and(isUpdatableField, not(isInherited), isStoredInDatabase))) {
this.column(field, { setUnique: false, setDefault: false });
}
});
Expand Down
3 changes: 2 additions & 1 deletion src/migrations/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// created from 'create-ts-index'

export * from './generate';
export * from './generate-functions';
export * from './generate';
export * from './types';
export * from './update-functions';
13 changes: 9 additions & 4 deletions src/models/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,9 @@ export const isInputModel = (model: Model): model is InputModel => model instanc

export const isInterfaceModel = (model: Model): model is InterfaceModel => model instanceof InterfaceModel;

export const isCreatableModel = (model: EntityModel) => model.creatable && model.fields.some(isCreatableField);
export const isCreatableModel = (model: EntityModel) => !!model.creatable && model.fields.some(isCreatableField);

export const isUpdatableModel = (model: EntityModel) => model.updatable && model.fields.some(isUpdatableField);
export const isUpdatableModel = (model: EntityModel) => !!model.updatable && model.fields.some(isUpdatableField);

export const isCreatableField = (field: EntityField) => !field.inherited && !!field.creatable;

Expand All @@ -88,13 +88,18 @@ export const isQueriableField = ({ queriable }: EntityField) => queriable !== fa

export const isCustomField = (field: EntityField): field is CustomField => field.kind === 'custom';

export const isDynamicField = (field: EntityField) => !!field.generateAs || isCustomField(field);

/** True if field exists as a column in the DB (excludes custom and expression-only fields). */
export const isStoredInDatabase = (field: EntityField) => !isCustomField(field) && field.generateAs?.type !== 'expression';

export const isVisible = ({ hidden }: EntityField) => hidden !== true;

export const isSimpleField = and(not(isRelation), not(isCustomField));

export const isUpdatable = ({ updatable }: EntityField) => !!updatable;
export const isUpdatable = ({ updatable }: EntityField | EntityModel) => !!updatable;

export const isCreatable = ({ creatable }: EntityField) => !!creatable;
export const isCreatable = ({ creatable }: EntityField | EntityModel) => !!creatable;

export const isQueriableBy = (role: string) => (field: EntityField) =>
field.queriable !== false &&
Expand Down
8 changes: 4 additions & 4 deletions src/permissions/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Knex } from 'knex';
import { FullContext } from '../context';
import { NotFoundError, PermissionError } from '../errors';
import { EntityModel } from '../models/models';
import { get, isRelation } from '../models/utils';
import { get, isRelation, isStoredInDatabase } from '../models/utils';
import { AliasGenerator, getColumnName, hash, ors } from '../resolvers/utils';
import { PermissionAction, PermissionLink, PermissionStack } from './generate';

Expand Down Expand Up @@ -155,9 +155,9 @@ export const checkCanWrite = async (
const query = ctx.knex.first();
let linked = false;

for (const field of model.fields.filter(
(field) => field.generated || (action === 'CREATE' ? field.creatable : field.updatable),
)) {
for (const field of model.fields
.filter(isStoredInDatabase)
.filter((field) => field.generated || (action === 'CREATE' ? field.creatable : field.updatable))) {
const fieldPermissions = field[action === 'CREATE' ? 'creatable' : 'updatable'];
const role = getRole(ctx);
if (
Expand Down
22 changes: 16 additions & 6 deletions src/resolvers/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Context } from '../context';
import { ForbiddenError, GraphQLError } from '../errors';
import { EntityField, EntityModel } from '../models/models';
import { Entity, MutationContext, Trigger } from '../models/mutation-hook';
import { get, isPrimitive, it, typeToField } from '../models/utils';
import { and, get, isDynamicField, isPrimitive, isUpdatableField, it, not, typeToField } from '../models/utils';
import { applyPermissions, checkCanWrite, getEntityToMutate } from '../permissions/check';
import { anyDateToLuxon } from '../utils';
import { resolve } from './resolver';
Expand Down Expand Up @@ -87,7 +87,7 @@ export const createEntity = async (
if (model.parent) {
const rootInput = {};
const childInput = { id };
for (const field of model.fields) {
for (const field of model.fields.filter(not(isDynamicField))) {
const columnName = field.kind === 'relation' ? `${field.name}Id` : field.name;
if (columnName in normalizedInput) {
if (field.inherited) {
Expand All @@ -100,7 +100,12 @@ export const createEntity = async (
await ctx.knex(model.parent).insert(rootInput);
await ctx.knex(model.name).insert(childInput);
} else {
await ctx.knex(model.name).insert(normalizedInput);
const insertData = { ...normalizedInput };
for (const field of model.fields.filter(isDynamicField)) {
const columnName = field.kind === 'relation' ? `${field.name}Id` : field.name;
delete insertData[columnName];
}
await ctx.knex(model.name).insert(insertData);
}
await createRevision(model, normalizedInput, ctx);
await ctx.mutationHook?.({
Expand Down Expand Up @@ -555,7 +560,7 @@ export const createRevision = async (model: EntityModel, data: Entity, ctx: Muta
}
const childRevisionData = { id: revisionId };

for (const field of model.fields.filter(({ updatable }) => updatable)) {
for (const field of model.fields.filter(and(isUpdatableField, not(isDynamicField)))) {
const col = field.kind === 'relation' ? `${field.name}Id` : field.name;
let value;
if (field.nonNull && (!(col in data) || col === undefined || col === null)) {
Expand Down Expand Up @@ -631,7 +636,7 @@ const doUpdate = async (model: EntityModel, currentEntity: Entity, update: Entit
if (model.parent) {
const rootInput = {};
const childInput = {};
for (const field of model.fields) {
for (const field of model.fields.filter(not(isDynamicField))) {
const columnName = field.kind === 'relation' ? `${field.name}Id` : field.name;
if (columnName in update) {
if (field.inherited) {
Expand All @@ -648,7 +653,12 @@ const doUpdate = async (model: EntityModel, currentEntity: Entity, update: Entit
await ctx.knex(model.name).where({ id: currentEntity.id }).update(childInput);
}
} else {
await ctx.knex(model.name).where({ id: currentEntity.id }).update(update);
const updateData = { ...update };
for (const field of model.fields.filter(isDynamicField)) {
const columnName = field.kind === 'relation' ? `${field.name}Id` : field.name;
delete updateData[columnName];
}
await ctx.knex(model.name).where({ id: currentEntity.id }).update(updateData);
}
await createRevision(model, { ...currentEntity, ...update }, ctx);
};
20 changes: 7 additions & 13 deletions src/resolvers/resolvers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Models } from '../models/models';
import { isRootModel, merge, not, typeToField } from '../models/utils';
import { and, isCreatable, isRootModel, isUpdatable, merge, not, typeToField } from '../models/utils';
import { mutationResolver } from './mutations';
import { queryResolver } from './resolver';

Expand Down Expand Up @@ -27,18 +27,12 @@ export const getResolvers = (models: Models) => {
]),
};
const mutations = [
...models.entities
.filter(not(isRootModel))
.filter(({ creatable }) => creatable)
.map((model) => ({
[`create${model.name}`]: mutationResolver,
})),
...models.entities
.filter(not(isRootModel))
.filter(({ updatable }) => updatable)
.map((model) => ({
[`update${model.name}`]: mutationResolver,
})),
...models.entities.filter(and(not(isRootModel), isCreatable)).map((model) => ({
[`create${model.name}`]: mutationResolver,
})),
...models.entities.filter(and(not(isRootModel), isUpdatable)).map((model) => ({
[`update${model.name}`]: mutationResolver,
})),
...models.entities
.filter(not(isRootModel))
.filter(({ deletable }) => deletable)
Expand Down
52 changes: 28 additions & 24 deletions src/schema/generate.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { DefinitionNode, DocumentNode, GraphQLSchema, buildASTSchema, print } from 'graphql';
import { Models } from '../models/models';
import { isQueriableField, isRootModel, typeToField } from '../models/utils';
import {
and,
isCreatable,
isQueriableField,
isRootModel,
isStoredInDatabase,
isUpdatable,
typeToField,
} from '../models/utils';
import { Field, document, enm, iface, input, object, scalar, union } from './utils';

export const generateDefinitions = ({
Expand Down Expand Up @@ -133,18 +141,16 @@ export const generateDefinitions = ({
types.push(
input(
`Create${model.name}`,
model.fields
.filter(({ creatable }) => creatable)
.map((field) =>
field.kind === 'relation'
? { name: `${field.name}Id`, type: 'ID', nonNull: field.nonNull }
: {
name: field.name,
type: field.kind === 'json' ? `Create${field.type}` : field.type,
list: field.list,
nonNull: field.nonNull && field.defaultValue === undefined,
},
),
model.fields.filter(and(isCreatable, isStoredInDatabase)).map((field) =>
field.kind === 'relation'
? { name: `${field.name}Id`, type: 'ID', nonNull: field.nonNull }
: {
name: field.name,
type: field.kind === 'json' ? `Create${field.type}` : field.type,
list: field.list,
nonNull: field.nonNull && field.defaultValue === undefined,
},
),
),
);
}
Expand All @@ -153,17 +159,15 @@ export const generateDefinitions = ({
types.push(
input(
`Update${model.name}`,
model.fields
.filter(({ updatable }) => updatable)
.map((field) =>
field.kind === 'relation'
? { name: `${field.name}Id`, type: 'ID' }
: {
name: field.name,
type: field.kind === 'json' ? `Update${field.type}` : field.type,
list: field.list,
},
),
model.fields.filter(and(isUpdatable, isStoredInDatabase)).map((field) =>
field.kind === 'relation'
? { name: `${field.name}Id`, type: 'ID' }
: {
name: field.name,
type: field.kind === 'json' ? `Update${field.type}` : field.type,
list: field.list,
},
),
),
);
}
Expand Down
Loading