diff --git a/packages/api/src/data/definitions.ts b/packages/api/src/data/definitions.ts index a4d703f..eec49fd 100644 --- a/packages/api/src/data/definitions.ts +++ b/packages/api/src/data/definitions.ts @@ -1,7 +1,61 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ /* eslint-disable @typescript-eslint/no-explicit-any */ import { Expr, query as q } from "faunadb"; -import { Table, FaunaSchema } from "./types"; +import { Table, FaunaSchema, RelationshipField } from "./types"; + +const generateRelationQueries = (field: RelationshipField) => { + const existingRelation = (id: string) => + q.Match(q.Index("relationsUnique"), [ + field.relationshipRef, + field.relationKey === "A" + ? q.Var("docRef") + : q.Ref(q.Collection(field.to.id), id), + field.relationKey === "B" + ? q.Var("docRef") + : q.Ref(q.Collection(field.to.id), id), + ]); + + return { + connect: (ids: Array) => { + if (!ids) return []; + return ids.map((id: string) => + q.If( + q.IsEmpty(existingRelation(id)), + q.Create(q.Collection("relations"), { + data: { + // @ts-ignore + relationshipRef: field.relationshipRef, + // @ts-ignore + [field.relationKey]: q.Var("docRef"), + // @ts-ignore + [field.relationKey === "A" ? "B" : "A"]: q.Ref( + // @ts-ignore + q.Collection(field.to.id), + // @ts-ignore + id + ), + }, + }), + q.Abort(`Object with id ${field.to.id} is already connected.`) + ) + ); + }, + disconnect: (ids: Array) => { + if (!ids) return []; + return ids.map((id: string) => + q.If( + q.IsEmpty(existingRelation(id)), + q.Abort(`Object with id ${field.to.id} does not exist.`), + + q.Map( + q.Paginate(existingRelation(id)), + q.Lambda("X", q.Delete(q.Var("X"))) + ) + ) + ); + }, + }; +}; export const definitions = ( table: Pick @@ -16,7 +70,7 @@ export const definitions = ( } => ({ queries: { findMany: { - name: (): string => table.apiName + "GetMany", + name: (): string => "query" + table.apiName, // @ts-ignore query: (args): Expr => { const options: { size: number; after?: Expr; before?: Expr } = { @@ -34,6 +88,13 @@ export const definitions = ( ); }, }, + findOne: { + name: (): string => "get" + table.apiName, + // @ts-ignore + query: (args: { id: string }): Expr => { + return q.Get(q.Ref(q.Collection(table.id), args.id)); + }, + }, createOne: { name: () => table.apiName + "CreateOne", // @ts-ignore @@ -42,52 +103,92 @@ export const definitions = ( args, faunaSchema: FaunaSchema ) => { - { - const data: Record = {}; - let relationQueries; - // const args = getArgumentValues(field, node); - for (const [key, value] of Object.entries(args.input)) { - const faunaField = faunaSchema[table.apiName].fields[key]; - if (faunaField.type === "Relation") { - relationQueries = q.Do( - // @ts-ignore - value.connect.map((idToConnect: string) => - q.Create(q.Collection("relations"), { - data: { - // @ts-ignore - relationshipRef: faunaField.relationshipRef, - // @ts-ignore - [faunaField.relationKey]: q.Var("docRef"), - // @ts-ignore - [faunaField.relationKey === "A" ? "B" : "A"]: q.Ref( - // @ts-ignore - q.Collection(faunaField.to.id), - // @ts-ignore - idToConnect - ), - }, - }) - ) - ); - } else { - data[faunaField.id] = value; + const data: Record = {}; + let relationQueries: Array = []; + // const args = getArgumentValues(field, node); + for (const [key, value] of Object.entries(args.input)) { + const faunaField = faunaSchema[table.apiName].fields[key]; + if (faunaField.type === "Relation") { + relationQueries = [ + ...relationQueries, + // @ts-ignore + ...generateRelationQueries(faunaField).connect(value.connect), + ]; + } else { + data[faunaField.id] = value; + } + } + return q.Select( + ["doc"], + q.Let( + { + docRef: q.Select( + ["ref"], + q.Create(q.Collection(faunaSchema[table.apiName].id), { + data, + }) + ), + }, + { + doc: q.Get(q.Var("docRef")), + relationQueries: q.Do(relationQueries), } + ) + ); + }, + }, + updateOne: { + name: () => table.apiName + "UpdateOne", + // @ts-ignore + query: ( + // @ts-ignore + args: GiraphQLFieldKindToConfig, + faunaSchema: FaunaSchema + ) => { + const data: Record = {}; + let relationQueries: Array = []; + // const args = getArgumentValues(field, node); + for (const [key, value] of Object.entries(args.input)) { + const faunaField = faunaSchema[table.apiName].fields[key]; + if (faunaField.type === "Relation") { + relationQueries = [ + ...relationQueries, + // @ts-ignore + ...generateRelationQueries(faunaField).connect(value.connect), + ...generateRelationQueries(faunaField).disconnect( + // @ts-ignore + value.disconnect + ), + ]; + } else { + data[faunaField.id] = value; } - return q.Select( - ["doc"], - q.Let( - { - docRef: q.Select( + } + return q.Select( + ["doc"], + q.Let( + { + toUpdateRef: q.Ref( + q.Collection(faunaSchema[table.apiName].id), + args.id + ), + docRef: q.If( + q.Exists(q.Var("toUpdateRef")), + q.Select( ["ref"], - q.Create(q.Collection(faunaSchema[table.apiName].id), { + q.Update(q.Var("toUpdateRef"), { data, }) ), - }, - { doc: q.Get(q.Var("docRef")), relationQueries } - ) - ); - } + q.Abort("Object does not exist.") + ), + }, + { + doc: q.Get(q.Var("docRef")), + relationQueries: q.Do(relationQueries), + } + ) + ); }, }, }, diff --git a/packages/api/src/data/fauna/scaffold.ts b/packages/api/src/data/fauna/scaffold.ts index fadba95..698dde2 100644 --- a/packages/api/src/data/fauna/scaffold.ts +++ b/packages/api/src/data/fauna/scaffold.ts @@ -90,4 +90,24 @@ export default async (client: Client): Promise => { ], }) ); + + await client.query( + q.CreateIndex({ + name: "relationsUnique", + unique: true, + serialized: true, + source: q.Collection("relations"), + terms: [ + { + field: ["data", "relationshipRef"], + }, + { + field: ["data", "A"], + }, + { + field: ["data", "B"], + }, + ], + }) + ); }; diff --git a/packages/api/src/data/generateFaunaQuery.ts b/packages/api/src/data/generateFaunaQuery.ts index 9f96753..6a5222b 100644 --- a/packages/api/src/data/generateFaunaQuery.ts +++ b/packages/api/src/data/generateFaunaQuery.ts @@ -92,7 +92,8 @@ const generateSelector = ( name, q.Select( ["data", faunaSchema[parentType.name].fields[name].id], - CURRENT_DOC_VAR + CURRENT_DOC_VAR, + null ), ]; }; diff --git a/packages/api/src/data/generateGraphQLSchema.ts b/packages/api/src/data/generateGraphQLSchema.ts index bc754db..413e6e6 100644 --- a/packages/api/src/data/generateGraphQLSchema.ts +++ b/packages/api/src/data/generateGraphQLSchema.ts @@ -79,19 +79,31 @@ export default (projectData: any, client: Client): GraphQLSchema => { const builder = new SchemaBuilder<{ DefaultFieldNullability: true; + Scalars: { + Number: { + Input: number; + Output: number; + }; + }; }>({ defaultFieldNullability: true, }); + builder.scalarType("Number", { + serialize: (n) => n, + parseValue: (n) => { + if (typeof n === "number") { + return n; + } + + throw new Error("Value must be a number"); + }, + }); builder.queryType({}); builder.mutationType({}); // let relationshipFields: { [key: string]: any } = {}; - let inputFields: { - [tableApiName: string]: { - [fieldApiName: string]: GiraphQLSchemaTypes.InputFieldOptions; - }; - } = {}; + for (let table of Object.values(faunaSchema)) { // @ts-ignore builder.objectType(table.apiName, { @@ -99,37 +111,44 @@ export default (projectData: any, client: Client): GraphQLSchema => { id: t.exposeID("id", {}), }), }); - inputFields[table.apiName] = {}; for (let field of Object.values(table.fields)) { if (field.type === "Relation") { - builder.inputType(tableIdToApiName[field.to.id] + "RelationInput", { - fields: (t) => ({ - connect: t.field({ type: ["ID"], required: true }), - }), - }); - } - - // @ts-ignore - builder.objectField(table.apiName, field.apiName, (t) => + builder.inputType( + tableIdToApiName[field.to.id] + "CreateRelationInput", + { + fields: (t) => ({ + connect: t.field({ type: ["ID"], required: true }), + }), + } + ); + builder.inputType( + tableIdToApiName[field.to.id] + "UpdateRelationInput", + { + fields: (t) => ({ + connect: t.field({ type: ["ID"], required: false }), + disconnect: t.field({ type: ["ID"], required: false }), + }), + } + ); // @ts-ignore - t.expose(field.apiName, { + builder.objectField(table.apiName, field.apiName, (t) => // @ts-ignore - type: - field.type === "Relation" - ? [tableIdToApiName[field.to.id]] - : field.type, - }) - ); - - inputFields[table.apiName][field.apiName] = { + t.expose(field.apiName, { + // @ts-ignore + type: [tableIdToApiName[field.to.id]], + }) + ); + } else { // @ts-ignore - type: - field.type === "Relation" - ? tableIdToApiName[field.to.id] + "RelationInput" - : field.type, - required: false, - }; + builder.objectField(table.apiName, field.apiName, (t) => + // @ts-ignore + t.expose(field.apiName, { + // @ts-ignore + type: field.type, + }) + ); + } } builder.queryField(definitions(table).queries.findMany.name(), (t) => @@ -149,6 +168,21 @@ export default (projectData: any, client: Client): GraphQLSchema => { ), }) ); + builder.queryField(definitions(table).queries.findOne.name(), (t) => + t.field({ + // @ts-ignore + type: table.apiName, + args: { + id: t.arg({ type: "String", required: true }), + }, + resolve: (...args) => + resolve( + ...args, + faunaSchema, + definitions(table).queries.findOne.query(args[1]) + ), + }) + ); builder.mutationField(definitions(table).queries.createOne.name(), (t) => t.field({ @@ -156,7 +190,7 @@ export default (projectData: any, client: Client): GraphQLSchema => { type: table.apiName, args: { // @ts-ignore - input: t.arg({ type: table.apiName + "Input", required: true }), + input: t.arg({ type: table.apiName + "CreateInput", required: true }), }, resolve: (...args) => resolve( @@ -166,21 +200,59 @@ export default (projectData: any, client: Client): GraphQLSchema => { ), }) ); - } + builder.mutationField(definitions(table).queries.updateOne.name(), (t) => + t.field({ + // @ts-ignore + type: table.apiName, + args: { + // @ts-ignore + input: t.arg({ type: table.apiName + "UpdateInput", required: true }), + id: t.arg({ type: "ID", required: true }), + }, + resolve: (...args) => + resolve( + ...args, + faunaSchema, + definitions(table).queries.updateOne.query(args[1], faunaSchema) + ), + }) + ); - for (const [tableApiName, tableInputFields] of Object.entries(inputFields)) { - if (!builder.configStore.typeConfigs.has(tableApiName + "Input")) { - builder.inputType(tableApiName + "Input", { - fields: (t) => - Object.keys(tableInputFields).reduce( - (attrs, key) => ({ - ...attrs, - [key]: t.field(tableInputFields[key]), + builder.inputType(table.apiName + "CreateInput", { + fields: (t) => + Object.values(table.fields).reduce( + (attrs, field) => ({ + ...attrs, + [field.apiName]: t.field({ + //@ts-ignore + type: + field.type === "Relation" + ? tableIdToApiName[field.to.id] + "CreateRelationInput" + : field.type, + required: false, }), - {} - ), - }); - } + }), + {} + ), + }); + builder.inputType(table.apiName + "UpdateInput", { + fields: (t) => + Object.values(table.fields).reduce( + (attrs, field) => ({ + ...attrs, + + [field.apiName]: t.field({ + //@ts-ignore + type: + field.type === "Relation" + ? tableIdToApiName[field.to.id] + "UpdateRelationInput" + : field.type, + required: false, + }), + }), + {} + ), + }); } return builder.toSchema({}); diff --git a/packages/api/src/data/types/index.ts b/packages/api/src/data/types/index.ts index a3866ba..f128cd5 100644 --- a/packages/api/src/data/types/index.ts +++ b/packages/api/src/data/types/index.ts @@ -35,7 +35,7 @@ export type FieldInput = ScalarFieldInput | RelationshipFieldInput; export interface ScalarFieldInput { name: string; apiName: string; - type: "String" | "Boolean"; + type: "String" | "Boolean" | "Number"; tableRef: Expr; } @@ -68,7 +68,7 @@ export interface ScalarField { id: string; name: string; apiName: string; - type: "String" | "Boolean"; + type: "String" | "Boolean" | "Number"; tableRef: Ref; }