diff --git a/package.json b/package.json index 43ac26e4..633f003b 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,15 @@ { "dependencies": { "@entria/graphql-mongo-helpers": "^1.1.2", + "@entria/graphql-mongoose-loader": "^4.3.2", "@koa/router": "10.0.0", "@types/jest": "^27.5.1", + "babel-node": "^0.0.1-security", "dataloader": "^1.4.0", - "get-graphql-schema": "^2.1.2", "esbuild": "^0.12.5", + "get-graphql-schema": "^2.1.2", "graphql": "^15.5.0", - "graphql-relay": "^0.10.0", + "graphql-relay": "^0.9.0", "koa": "^2.13.1", "koa-graphql": "^0.8.0", "mongoose": "^5.12.12", @@ -56,7 +58,7 @@ "scripts": { "start": "webpack --watch --progress --config webpack.js", "jest": "jest", - "build": "esbuild app.jsx --bundle --outfile = out.js", - "schema": "babel-node --extensions \".es6,.js,.es,.jsx,.mjs,.ts\" ./scripts/updateSchema.ts" + "build": "babel-node --extensions \".es6,.js,.es,.jsx,.mjs,.ts,.tsx\"", + "schema": "yarn build webpackx.ts ./scripts/updateSchema.ts" } } diff --git a/schema.graphql b/schema.graphql new file mode 100644 index 00000000..639a000c --- /dev/null +++ b/schema.graphql @@ -0,0 +1,102 @@ +"""The root of all... queries""" +type Query { + """Fetches an object given its ID""" + node( + """The ID of an object""" + id: ID! + ): Node + + """Fetches objects given their IDs""" + nodes( + """The IDs of objects""" + ids: [ID!]! + ): [Node]! + events(after: String, first: Int, before: String, last: Int): EventConnection! +} + +"""An object with an ID""" +interface Node { + """The id of the object.""" + id: ID! +} + +"""A connection to a list of items.""" +type EventConnection { + """Number of items in this connection""" + count: Int + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """Offset from start""" + startCursorOffset: Int! + + """Offset till end""" + endCursorOffset: Int! + + """Information to aid in pagination.""" + pageInfo: PageInfoExtended! + + """A list of edges.""" + edges: [EventEdge]! +} + +"""Information about pagination in a connection.""" +type PageInfoExtended { + """When paginating forwards, are there more items?""" + hasNextPage: Boolean! + + """When paginating backwards, are there more items?""" + hasPreviousPage: Boolean! + + """When paginating backwards, the cursor to continue.""" + startCursor: String + + """When paginating forwards, the cursor to continue.""" + endCursor: String +} + +"""An edge in a connection.""" +type EventEdge { + """The item at the end of the edge""" + node: Event + + """A cursor for use in pagination""" + cursor: String! +} + +"""event data""" +type Event implements Node { + """The ID of an object""" + id: ID! + name: String + start: String + end: String + allDay: Boolean +} + +"""Root of ... mutations""" +type Mutation { + CreateEvent(input: Create eventInput!): Create eventPayload +} + +type Create eventPayload { + event: Event + + """Default error field resolver.""" + error: String + + """Default success field resolver.""" + success: String + clientMutationId: String +} + +input Create eventInput { + event: ID! + clientMutationId: String +} diff --git a/src/graphql/connectionDefinitions.ts b/src/graphql/connectionDefinitions.ts new file mode 100644 index 00000000..38216635 --- /dev/null +++ b/src/graphql/connectionDefinitions.ts @@ -0,0 +1,143 @@ +import { + GraphQLBoolean, + GraphQLFieldConfigArgumentMap, + GraphQLFieldConfigMap, + GraphQLFieldResolver, + GraphQLInt, + GraphQLList, + GraphQLNonNull, + GraphQLObjectType, + GraphQLString, + Thunk, +} from "graphql"; + +export const forwardConnectionArgs: GraphQLFieldConfigArgumentMap = { + after: { + type: GraphQLString, + }, + first: { + type: GraphQLInt, + }, +}; + +export const backwardConnectionArgs: GraphQLFieldConfigArgumentMap = { + before: { + type: GraphQLString, + }, + last: { + type: GraphQLInt, + }, +}; + +export const connectionArgs: GraphQLFieldConfigArgumentMap = { + ...forwardConnectionArgs, + ...backwardConnectionArgs, +}; + +type ConnectionConfig = { + name?: string | null; + nodeType: GraphQLObjectType; + resolveNode?: GraphQLFieldResolver | null; + resolveCursor?: GraphQLFieldResolver | null; + edgeFields?: Thunk> | null; + connectionFields?: Thunk> | null; +}; + +export type GraphQLConnectionDefinitions = { + edgeType: GraphQLObjectType; + connectionType: GraphQLObjectType; +}; + +const pageInfoType = new GraphQLObjectType({ + name: "PageInfoExtended", + description: "Information about pagination in a connection.", + fields: () => ({ + hasNextPage: { + type: GraphQLNonNull(GraphQLBoolean), + description: "When paginating forwards, are there more items?", + }, + hasPreviousPage: { + type: GraphQLNonNull(GraphQLBoolean), + description: "When paginating backwards, are there more items?", + }, + startCursor: { + type: GraphQLString, + description: "When paginating backwards, the cursor to continue.", + }, + endCursor: { + type: GraphQLString, + description: "When paginating forwards, the cursor to continue.", + }, + }), +}); + +function resolveMaybeThunk(thingOrThunk: Thunk): T { + return typeof thingOrThunk === "function" + ? (thingOrThunk as () => T)() + : thingOrThunk; +} + +export function connectionDefinitions( + config: ConnectionConfig +): GraphQLConnectionDefinitions { + const { nodeType, resolveCursor, resolveNode } = config; + const name = config.name || nodeType.name; + const edgeFields = config.edgeFields || {}; + const connectionFields = config.connectionFields || {}; + + const edgeType = new GraphQLObjectType({ + name: `${name}Edge`, + description: "An edge in a connection.", + fields: () => ({ + node: { + type: nodeType, + resolve: resolveNode, + description: "The item at the end of the edge", + }, + cursor: { + type: GraphQLNonNull(GraphQLString), + resolve: resolveCursor, + description: "A cursor for use in pagination", + }, + ...(resolveMaybeThunk(edgeFields) as any), + }), + }); + + const connectionType = new GraphQLObjectType({ + name: `${name}Connection`, + description: "A connection to a list of items.", + fields: () => ({ + count: { + type: GraphQLInt, + description: "Number of items in this connection", + }, + totalCount: { + type: GraphQLInt, + resolve: (connection) => connection.count, + description: `A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example.`, + }, + startCursorOffset: { + type: GraphQLNonNull(GraphQLInt), + description: "Offset from start", + }, + endCursorOffset: { + type: GraphQLNonNull(GraphQLInt), + description: "Offset till end", + }, + pageInfo: { + type: GraphQLNonNull(pageInfoType), + description: "Information to aid in pagination.", + }, + edges: { + type: GraphQLNonNull(GraphQLList(edgeType)), + description: "A list of edges.", + }, + ...(resolveMaybeThunk(connectionFields) as any), + }), + }); + + return { edgeType, connectionType }; +} diff --git a/src/graphql/index.ts b/src/graphql/index.ts new file mode 100644 index 00000000..9e862c34 --- /dev/null +++ b/src/graphql/index.ts @@ -0,0 +1,2 @@ +export { registerLoader, getDataloaders } from "./loaderRegister"; +export { connectionArgs, connectionDefinitions } from "./connectionDefinitions"; \ No newline at end of file diff --git a/src/modules/event/EventType.ts b/src/modules/event/EventType.ts index 8aeacfe6..2d9606d0 100644 --- a/src/modules/event/EventType.ts +++ b/src/modules/event/EventType.ts @@ -2,7 +2,7 @@ import { GraphQLBoolean, GraphQLObjectType, GraphQLString } from "graphql"; import { globalIdField } from "graphql-relay"; -import { connectionDefinitions } from "@entria/graphql-mongo-helpers"; +import { connectionDefinitions } from "../../graphql"; import { load } from "./EventLoader"; diff --git a/src/schema/QueryType.ts b/src/schema/QueryType.ts index e8f9aafe..1bd8ee03 100644 --- a/src/schema/QueryType.ts +++ b/src/schema/QueryType.ts @@ -1,6 +1,6 @@ import { GraphQLObjectType, GraphQLNonNull } from 'graphql'; -import { connectionArgs } from "@entria/graphql-mongo-helpers"; +import { connectionArgs } from "../graphql/connectionDefinitions"; import { nodesField, nodeField } from '../modules/node/typeRegister'; import * as EventLoader from '../modules/event/EventLoader'; diff --git a/webpack/webpack.config.js b/webpack/webpack.config.js new file mode 100644 index 00000000..f015a541 --- /dev/null +++ b/webpack/webpack.config.js @@ -0,0 +1,64 @@ +const path = require("path"); + +const nodeExternals = require("webpack-node-externals"); + +const cwd = process.cwd(); + +export const outputPath = path.join(cwd, ".webpack"); +export const outputFilename = "bundle.js"; + +export default { + context: cwd, + mode: "development", + devtool: false, + resolve: { + extensions: [".ts", ".tsx", ".js", ".json", ".mjs"], + // alias: { + // mongoose: 'mongoosev5', + // mongodb: 'mongodbv3', + // }, + }, + output: { + libraryTarget: "commonjs2", + path: outputPath, + filename: outputFilename, + }, + target: "node", + externals: [ + nodeExternals({ + allowlist: [/@feedback/, /@openpix/], + }), + nodeExternals({ + modulesDir: path.resolve(__dirname, "../node_modules"), + allowlist: [/@feedback/, /@openpix/], + }), + ], + module: { + rules: [ + { + test: /\.mjs$/, + type: "javascript/auto", + }, + { + test: /\.(js|jsx|ts|tsx)?$/, + use: { + loader: "babel-loader?cacheDirectory", + }, + exclude: [ + /node_modules/, + path.resolve(__dirname, ".serverless"), + path.resolve(__dirname, ".webpack"), + ], + }, + { + test: /\.(pem|p12)?$/, + type: "asset/source", + }, + ], + }, + plugins: [], + node: { + __dirname: false, + __filename: false, + }, +}; diff --git a/webpackx.ts b/webpackx.ts new file mode 100644 index 00000000..b9a87c8b --- /dev/null +++ b/webpackx.ts @@ -0,0 +1,71 @@ +import path from "path"; +import { spawn } from "child_process"; + +import webpack from "webpack"; + +import config, { outputPath, outputFilename } from './webpack/webpack.config'; + +const compilerRunPromise = (compiler) => + new Promise((resolve, reject) => { + compiler.run((err, stats) => { + if (err) { + return reject(err); + } + + if (stats && stats.hasErrors()) { + reject(err || stats.toString()); + } + + resolve(stats); + }); + }); + +export function onExit(childProcess: ChildProcess): Promise { + return new Promise((resolve, reject) => { + childProcess.once("exit", (code: number) => { + if (code === 0) { + resolve(undefined); + } else { + reject(new Error(`Exit with error code: ${code}`)); + } + }); + childProcess.once("error", (err: Error) => { + reject(err); + }); + }); +} + +const runProgram = async () => { + const outputFile = path.join(outputPath, outputFilename); + const execArgs = process.argv.slice(3); + + const childProcess = spawn(process.execPath, [outputFile, ...execArgs], { + stdio: [process.stdin, process.stdout, process.stderr], + }); + + await onExit(childProcess); +}; + +(async () => { + try { + const wpConfig = { + ...config, + entry: path.join(__dirname, process.argv[2]), + }; + + const compiler = webpack(wpConfig); + + // eslint-disable-next-line + const stats = await compilerRunPromise(compiler); + + // eslint-disable-next-line + // console.log(stats.toString()); + + await runProgram(); + } catch (err) { + // eslint-disable-next-line + console.log("err: ", err); + process.exit(1); + } + process.exit(0); +})(); diff --git a/yarn.lock b/yarn.lock index a31577a3..ec27a1a9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2374,6 +2374,11 @@ babel-loader@8.2.2: make-dir "^3.1.0" schema-utils "^2.6.5" +babel-node@^0.0.1-security: + version "0.0.1-security" + resolved "https://registry.yarnpkg.com/babel-node/-/babel-node-0.0.1-security.tgz#cbff6d9e7a2acb4065c1839c234d5c57091d7217" + integrity sha512-zF3D9H2FA2xrP+B/X462G+38aHpmR+33jBF7NowfPuV4CiPEzAR1Typ1RC+qPfe0vCeWeV0FLG2pfVGb0vIfeg== + babel-plugin-dynamic-import-node@^2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3" @@ -2709,15 +2714,10 @@ camelcase@^6.0.0, camelcase@^6.2.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== -caniuse-lite@^1.0.30001274: - version "1.0.30001275" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001275.tgz#26f5076629fe4e52bbd245f9046ad7b90aafdf57" - integrity sha512-ihJVvj8RX0kn9GgP43HKhb5q9s2XQn4nEQhdldEJvZhCsuiB2XOq6fAMYQZaN6FPWfsr2qU0cdL0CSbETwbJAg== - -caniuse-lite@^1.0.30001332: - version "1.0.30001341" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001341.tgz#59590c8ffa8b5939cf4161f00827b8873ad72498" - integrity sha512-2SodVrFFtvGENGCv0ChVJIDQ0KPaS1cg7/qtfMaICgeMolDdo/Z2OD32F0Aq9yl6F4YFwGPBS5AaPqNYiW4PoA== +caniuse-lite@^1.0.30001274, caniuse-lite@^1.0.30001332: + version "1.0.30001342" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001342.tgz" + integrity sha512-bn6sOCu7L7jcbBbyNhLg0qzXdJ/PMbybZTH/BA6Roet9wxYRm6Tr9D0s0uhLkOZ6MSG+QU6txUgdpr3MXIVqjA== chalk@^2.0.0, chalk@^2.4.1: version "2.4.2" @@ -3974,10 +3974,10 @@ graceful-fs@^4.2.9: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== -graphql-relay@^0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/graphql-relay/-/graphql-relay-0.10.0.tgz#3b661432edf1cb414cd4a132cf595350e524db2b" - integrity sha512-44yBuw2/DLNEiMypbNZBt1yMDbBmyVPVesPywnteGGALiBmdyy1JP8jSg8ClLePg8ZZxk0O4BLhd1a6U/1jDOQ== +graphql-relay@^0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/graphql-relay/-/graphql-relay-0.9.0.tgz#d96f19d38b742a390acf10056ddd136034b3e1b4" + integrity sha512-yNJLCqcjz0XpzpmmckRJCSK8a2ZLwTurwrQ09UyGftONh52PbrGpK1UO4yspvj0c7pC+jkN4ZUqVXG3LRrWkXQ== graphql@^14.0.2: version "14.7.0"