diff --git a/.gitignore b/.gitignore index 3943ceb3..64418763 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,7 @@ warning.log .env /backups .DS_STORE -lib \ No newline at end of file +lib + +.scannerwork +graphqlSchema.graphql diff --git a/.npmignore b/.npmignore index 14545d6f..6b81d89d 100644 --- a/.npmignore +++ b/.npmignore @@ -17,4 +17,6 @@ warning.log uploads backups docs -tests \ No newline at end of file +tests + +test_database.sql \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc similarity index 100% rename from .prettierrc.json rename to .prettierrc diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..b6b0a3c6 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "cSpell.words": [ + "Wertik" + ] +} \ No newline at end of file diff --git a/changelog.md b/changelog.md index 1583e28d..0c279608 100644 --- a/changelog.md +++ b/changelog.md @@ -1,14 +1,49 @@ ## Started changelog after version 3.2.9 +### 3.4.0 + +- Added eager loading for list and view queries. +- Improved lazy loading behavior in GraphQL to avoid unnecessary queries. +- Added support for loading only requested fields in Sequelize. +- Added recursion support for both list and view queries. +- Added support for filtering associated data in recursive queries. +- Replaced moment.js with dayjs. +- Scanned and verified fixes with SonarQube. +- Removed lodash and installed its child packages to reduce app size. +- Added methods: `startServer()`, `restartServer()`, `stopServer()`. +- Removed query `list`. For example for Module `Users`, `listUsers` will become `users`, `EcommerceUsers` will become `ecommerce_users`. +- From single query item `view` is removed, For example `viewUser` will become `user` and `EcommerceUser` will become `ecommerce_user` +- Changed GraphQL list field from `list` to `rows`. +- Removed `applyRelationshipsFromStoreToGraphql` method due to the shift towards eager loading. +- Appended `Module` after module names in GraphQL types, e.g., `User` is now `UserModule`. +- Added tests to ensure GraphQL operations function correctly. +- Introduced an option to output GraphQL type definitions in a file. +- Change configuration prop `skip` to `selfStart` to make it more intuitive. +- Rename `useMysqlDatabase` to `withMysqlDatabase` with its props. +- Rename `useRedis` to `withRedis` with its props. +- Rename `useGraphql` to `withGraphql` with its props. +- Rename `useMailer` to `withMailer` with its props. +- Rename `useSocketIO` to `withSocketIO` with its props. +- Rename `useWebSockets` to `withWebSockets` with its props. +- Rename `useIndependentWebSocketsServer` to `withIndependentWebSocketsServer` with its props. +- Rename `useLogger` to `withLogger` with its props. +- Rename `useWinstonTransport` to `withWinstonTransport` with its props. +- Rename `useQueue` to `withQueue` with its props. +- Verified tests with Jest for renaming changes. +- Rename `useSchema` to `extendSchema` with its props. +- Rename `useMutation` to `addMutation` with its props. +- Rename `useExpress` to `getExpress`. +- BREAKING CHANGE: Remove `useModule` and allowed using tables on database configuration. + ### 3.3.0 - When sending emails fails, also throws errors in console -- When taking backup and provides invalid table/module name, the backup process failes with an error. +- When taking backup and provides invalid table/module name, the backup process failed with an error. - Added `extendFields` to `useModule` to extend `sequelize.model` table fields. ### 3.3.1 -- Removed support old legacy wertik framework. +- Removed old legacy wertik framework. - Now wertik can be import from `wertik-js/lib` instead of `wertik-js/lib/ndex` - Added more types support. -- Change the way mysql is initalized so that it can open doors for postgres and mongoose in future. \ No newline at end of file +- Change the way mysql is initialized so that it can open doors for postgres and mongoose in future. \ No newline at end of file diff --git a/docs/v3/graphql.md b/docs/v3/graphql.md index dcac33df..b70b4dd2 100644 --- a/docs/v3/graphql.md +++ b/docs/v3/graphql.md @@ -1,23 +1,23 @@ # GraphQL -For GraphQL, Wertik JS uses Apollo GraphQL under the hood. We choose Apollo GraphQL because it is well managed and bug-free. To set up Graphql Wertik JS provides a function called `useGraphql` to use Graphql in your app. +For GraphQL, Wertik JS uses Apollo GraphQL under the hood. We choose Apollo GraphQL because it is well managed and bug-free. To set up Graphql Wertik JS provides a function called `withGraphql` to use Graphql in your app. ```javascript -import wertik, { useGraphql } from "wertik-js/lib/"; +import wertik, { withGraphql } from "wertik-js/lib/"; wertik({ port: 1200, - graphql: useGraphql(useGraphqlProps), + graphql: withGraphql(WithGraphqlProps), }); ``` This will initialize GraphQL on URL: http://localhost:1200/graphql. If you visit this link you will Apollo GraphQL playground. -#### useGraphqlProps +#### WithGraphqlProps -useGraphqlProps is an argument which is optional when using `useGraphql` method. +WithGraphqlProps is an argument which is optional when using `withGraphql` method. ```typescript -export interface useGraphqlProps { +export interface WithGraphqlProps { options?: { [key: string]: any; }; @@ -32,5 +32,5 @@ export interface useGraphqlProps { - options includes ApolloServer options. - applyMiddlewareOptions includes options while integrating Apollo Server with express server with same port. -- resolvers for defined schema in useGraphqlProps.typeDefs. +- resolvers for defined schema in WithGraphqlProps.typeDefs. - typeDefs is your graphql schema. diff --git a/docs/v3/modules.md b/docs/v3/modules.md index 9b0835e9..a9538632 100644 --- a/docs/v3/modules.md +++ b/docs/v3/modules.md @@ -4,16 +4,16 @@ Wertik-js allows extending your app with more features using the `modules` term. ```js import wertik, { - useMysqlDatabase, - useMailer, - useModule, - useGraphql, + withMysqlDatabase, + withMailer, + withModule, + withGraphql, } from "wertik-js/lib/"; weritk({ port: 1200, database: { - default: useMysqlDatabase({ + default: withMysqlDatabase({ name: "default", password: "pass", host: "localhost", @@ -21,16 +21,16 @@ weritk({ username: "root", }), }, - graphql: useGraphql(), + graphql: withGraphql,(), mailer: { - default: useMailer(), + default: withMailer(), }, modules: { - users: useModule({ + users: withModule({ table: "users", database: "default", name: "users", - useMysqlDatabase: true, + useDatabase: true, }), }, }); @@ -48,10 +48,10 @@ When you provide `useMysqlDatabase: true`, `table` and `database`, Wertik JS aut You have to initialize its module in this way: ```js -import wertik, { useModule, useMysqlDatabase, useGraphql } from "wertik-js/lib/"; +import wertik, { useModule, useMysqlDatabase, withGraphql, } from "wertik-js/lib/"; wertik({ port: 1200, - graphql: useGraphql(), + graphql: withGraphql,(), database: { default: useMysqlDatabase({ name: "dbname", @@ -98,13 +98,13 @@ input updateGamesInput { For filtering data from `games` table, Wertik JS will also create an input for filtering: ```graphql -input GamesFilterInput { - name: StringFilterInput - publisher: StringFilterInput +input Gamesfilter_input { + name: Stringfilter_input + publisher: Stringfilter_input } ``` -To explore more about `StringFilterInput` and other filter input please visit GraphQL Playground to get more familiar with it. +To explore more about `Stringfilter_input` and other filter input please visit GraphQL Playground to get more familiar with it. ## This will generate @@ -117,13 +117,13 @@ To explore more about `StringFilterInput` and other filter input please visit Gr ```graphql type Query { version: String - viewGames(where: GamesFilterInput): Games + viewGames(where: Gamesfilter_input): Games listGames( pagination: PaginationInput - where: GamesFilterInput + where: Gamesfilter_input sorting: [SortingInput] ): GamesList - countGames(where: GamesFilterInput): Int + countGames(where: Gamesfilter_input): Int } ``` @@ -140,10 +140,10 @@ type Mutation { version: String updateGames( input: updateGamesInput - where: GamesFilterInput! + where: Gamesfilter_input! ): GamesBulkMutationResponse createGames(input: [createGamesInput]): GamesBulkMutationResponse - deleteGames(where: GamesFilterInput!): SuccessResponse + deleteGames(where: Gamesfilter_input!): SuccessResponse createOrUpdateGames(id: Int, input: createGamesInput): Games } ``` @@ -157,7 +157,7 @@ When you provide `useMysqlDatabase: true` for a module called Games. Wertik JS w ```graphql listGames( pagination: PaginationInput - where: GamesFilterInput + where: Gamesfilter_input sorting: [SortingInput] ): GamesList ``` @@ -171,7 +171,7 @@ input PaginationInput { } ``` -And `GamesFilterInput` is same as Sequelize search object but main keywords such as like, `eq` or `like` starts with `_`, For example: +And `Gamesfilter_input` is same as Sequelize search object but main keywords such as like, `eq` or `like` starts with `_`, For example: ```graphql query GamesList { @@ -280,25 +280,25 @@ wertik({ name: "Games", table: "games", database: "jscontainer", - on({ useExpress, useQuery, useMutation, useSchema }) { + on({ useExpress, addQuery, addMutation, extendSchema }) { useExpress((express) => { express.get("/404", (req, res) => res.status(404).send("404")); }); - useQuery({ + addQuery({ name: "getGames", query: "getGames: [Games]", resolver() { return []; }, }); - useMutation({ + addMutation({ name: "updateAllGames", query: "updateAllGames: [Games]", resolver() { return []; }, }); - useSchema(` + extendSchema(` type MyType { id: Int name: String diff --git a/docs/v3/queue.md b/docs/v3/queue.md index eda013fe..74804abe 100644 --- a/docs/v3/queue.md +++ b/docs/v3/queue.md @@ -60,12 +60,12 @@ This `useQueue` method simply instantiates a new instance of bull and returns th - A queue can be instantiated with some useful options, for instance, we can specify the `location` and `password` of our Redis server, as well as some other useful settings. we can use them as `options : {redis : {port : 6379, host : "127.0.0.1", password : "somepass" }}`. -#### useQueueProps +#### UseQueueProps -The `useQueue` method always expects an instantiation name such as(`my-queue-name`) all the other arguments are optional. ```typescript -export interface useQueueProps { +export interface UseQueueProps { name?: string url?: string options?: QueueOptions diff --git a/docs/v3/redis.md b/docs/v3/redis.md index 11313eaf..c815a32e 100644 --- a/docs/v3/redis.md +++ b/docs/v3/redis.md @@ -1,11 +1,11 @@ # Redis(Beta) -Wertik JS allows a using redis, Wertik JS uses package named `redis(options: useRedisProps)`. Wertik JS gives a function called `useRedis` which allows creating a redis server. Let's create a redis client: +Wertik JS allows a using redis, Wertik JS uses package named `redis(options: UseRedisProps)`. Wertik JS gives a function called `useRedis` which allows creating a redis server. Let's create a redis client: -Where `useRedisProps` is: +Where `UseRedisProps` is: ```typescript -export interface useRedisProps { +export interface UseRedisProps { [key: string]: any name: string; } diff --git a/package.json b/package.json index 2d818619..a1ef2cd1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "wertik-js", - "version": "3.3.3", - "main": "lib/main.js", + "version": "3.4.0", + "main": "lib/index.js", "types": "lib/types.d.ts", "repository": "https://github.com/Uconnect-Technologies/wertik-js.git", "keywords": [ @@ -26,9 +26,11 @@ "license": "MIT", "scripts": { "dev": "tsc-watch --onSuccess \"node lib/devServer.js\"", - "build": "yarn prettier && yarn tsc && yarn test", + "build": "rm -rf lib && yarn prettier && yarn tsc && yarn test", "prettier": "prettier --write src package.json index.js", - "test": "cross-env TEST_MODE=true jest --runInBand --forceExit --detectOpenHandles" + "test": "cross-env TEST_MODE=true jest --runInBand --forceExit --detectOpenHandles", + "preinstall": "rm -rf node_modules", + "prepare": "yarn tsc" }, "pre-commit": [ "prettier" @@ -43,21 +45,27 @@ "chalk": "^3.0.0", "cors": "^2.8.5", "cross-env": "^7.0.3", + "dayjs": "^1.11.7", "dropbox": "^8.2.0", "express": "^4.17.1", "graphql": "^15.7.2", + "graphql-depth-limit": "^1.1.0", "graphql-fields": "^2.0.3", "graphql-type-json": "^0.3.2", "handlebars": "^4.5.3", - "lodash": "^4.17.15", + "lodash.get": "^4.4.2", + "lodash.has": "^4.5.2", + "lodash.isplainobject": "^4.0.6", + "lodash.omit": "^4.5.0", + "lodash.snakecase": "^4.1.1", "log-symbols": "^3.0.0", - "moment": "^2.23.0", "morgan": "^1.9.1", "multer": "^1.4.2", "mysql2": "^1.6.4", "mysqldump": "^3.2.0", "node-cron": "^2.0.3", "nodemailer": ">=6.4.16", + "pluralize": "^8.0.0", "redis": "^4.0.1", "sequelize": "^6.3.5", "socket.io": "^4.1.3", diff --git a/src/cronJobs/index.ts b/src/cronJobs/index.ts index ade4ef8c..5878ac71 100644 --- a/src/cronJobs/index.ts +++ b/src/cronJobs/index.ts @@ -1,9 +1,9 @@ -import { get } from "lodash" +import get from "lodash.get" import nodeCron from "node-cron" -import { useCronJobsProps } from "../types/cronJobs" +import { UseCronJobsProps } from "../types/cronJobs" import { iObject, WertikApp, WertikConfiguration } from "../types" -export const useCronJob = (cron: useCronJobsProps) => { +export const useCronJob = (cron: UseCronJobsProps) => { return ({ configuration, wertikApp, diff --git a/src/crud/index.ts b/src/crud/index.ts index 35af8a33..1a8decca 100644 --- a/src/crud/index.ts +++ b/src/crud/index.ts @@ -1,78 +1,69 @@ -import { get } from "lodash" -import convertFiltersIntoSequalizeObject from "../utils/convertFiltersIntoSequalizeObject" +import get from "lodash.get" +import { wLogWithDateWithInfo } from "../utils/log" +import { convertGraphqlRequestedFieldsIntoInclude } from "../database/eagerLoadingGraphqlQuery" +import { + generateRequestedFieldsFromGraphqlInfo, + convertWordIntoSingular, + convertWordIntoPlural, +} from "../modules/modulesHelpers" +import convertFiltersIntoSequelizeObject from "../utils/convertFiltersIntoSequelizeObject" +import graphqlFields from "graphql-fields" +import { paginate } from "./paginate" +import omit from "lodash.omit" +import { voidFunction } from "../utils/voidFunction" -export const paginate = async (arg, tableInstance) => { - const { page = 1, limit = 100, sorting = [] } = arg.pagination ?? {} - const offset = limit * (page - 1) - const where = await convertFiltersIntoSequalizeObject(arg.where) - const { count, rows } = await tableInstance.findAndCountAll({ - where, - offset, - limit, - order: sorting.map(({ column, type }) => [column, type]), - }) - const totalPages = Math.ceil(count / limit) - return { - list: rows, - paginationProperties: { - total: count, - nextPage: page + 1, - page, - previousPage: page === 1 ? 1 : page - 1, - pages: totalPages, - hasMore: page < totalPages, - limit, - }, - } -} +export default function (table, schemaInformation, store) { + let rowsFieldName = convertWordIntoPlural(table.name) + let singleRowFieldName = convertWordIntoSingular(table.name) -export default function (module, schemaInformation, store) { return { graphql: { generateQueriesCrudSchema() { return ` - - type ${module.name}List { - list: [${module.name}] + type ${table.name}List { + rows: [${table.name}] pagination: Pagination - sorting: Sorting - paginationProperties: PaginationProperties + paginationProperties: PaginationProperties @deprecated(reason: "Use pagination instead") } - type ${module.name}BulkMutationResponse { - returning: [${module.name}] + type ${table.name}_bulk_mutation_response { + returning: [${table.name}] affectedRows: Int } - type Count${module.name} { + type Count${table.name} { count: Int } extend type Query { - view${module.name}(where: ${module.name}FilterInput): ${module.name} - list${module.name}(pagination: PaginationInput, where: ${module.name}FilterInput, sorting: [SortingInput]): ${module.name}List - count${module.name}(where: ${module.name}FilterInput): Int + ${singleRowFieldName}(where: ${singleRowFieldName}_filter_input): ${table.name} + ${rowsFieldName}(pagination: PaginationInput, where: ${singleRowFieldName}_filter_input, order: ${convertWordIntoSingular(table.name)}_order_input): ${table.name}List + count${table.name}(where: ${singleRowFieldName}_filter_input): Int }` }, generateMutationsCrudSchema() { return ` extend type Mutation { - update${module.name}(input: update${module.name}Input,where: ${module.name}FilterInput!): ${module.name}BulkMutationResponse - create${module.name}(input: [create${module.name}Input]): ${module.name}BulkMutationResponse - delete${module.name}(where: ${module.name}FilterInput!): SuccessResponse - createOrUpdate${module.name}(id: Int, input: create${module.name}Input): ${module.name} + update_${rowsFieldName}(input: update_${table.name}_input,where: ${singleRowFieldName}_filter_input!): ${table.name}_bulk_mutation_response + insert_${rowsFieldName}(input: [insert_${rowsFieldName}_input]): ${table.name}_bulk_mutation_response + delete_${rowsFieldName}(where: ${singleRowFieldName}_filter_input!): SuccessResponse + insert_or_update_${rowsFieldName}(id: Int, input: insert_${rowsFieldName}_input): ${table.name}List } ` }, generateCrudResolvers() { return { Mutation: { - [`createOrUpdate${module.name}`]: get( - module, - "graphql.mutations.createOrUpdate", + [`insert_or_update_${rowsFieldName}`]: get( + table, + "graphql.mutations.InsertOrUpdate", async (_, args, context, info) => { + wLogWithDateWithInfo( + "[Wertik-GraphQL-Mutation]", + `${info.fieldName} - ${JSON.stringify(args)}` + ) const argsFromEvent = await get( - module, - "events.beforeCreateOrUpdate", - function () {} + table, + "events.beforeInsertOrUpdate", + voidFunction )(_, args, context, info) args = argsFromEvent ? argsFromEvent : args const id = args.id @@ -85,7 +76,7 @@ export default function (module, schemaInformation, store) { }) if (!___find) { - throw new Error(`${module.name} Not found`) + throw new Error(`${table.name} Not found`) } await schemaInformation.tableInstance.update(args.input, { @@ -102,17 +93,21 @@ export default function (module, schemaInformation, store) { } } ), - [`update${module.name}`]: get( - module, + [`update_${rowsFieldName}`]: get( + table, "graphql.mutations.update", async (_, args, context, info) => { + wLogWithDateWithInfo( + "[Wertik-GraphQL-Mutation]", + `${info.fieldName} - ${JSON.stringify(args)}` + ) const argsFromEvent = await get( - module, + table, "events.beforeUpdate", - function () {} + voidFunction )(_, args, context, info) args = argsFromEvent ? argsFromEvent : args - const where = await convertFiltersIntoSequalizeObject( + const where = await convertFiltersIntoSequelizeObject( args.where ) const response = await schemaInformation.tableInstance.update( @@ -130,33 +125,41 @@ export default function (module, schemaInformation, store) { } } ), - [`delete${module.name}`]: get( - module, + [`delete_${rowsFieldName}`]: get( + table, "graphql.mutations.delete", async (_, args, context, info) => { + wLogWithDateWithInfo( + "[Wertik-GraphQL-Mutation]", + `${info.fieldName} - ${JSON.stringify(args)}` + ) const argsFromEvent = await get( - module, + table, "events.beforeDelete", - function () {} + voidFunction )(_, args, context, info) args = argsFromEvent ? argsFromEvent : args - const where = await convertFiltersIntoSequalizeObject( + const where = await convertFiltersIntoSequelizeObject( args.where ) await schemaInformation.tableInstance.destroy({ where: where, }) - return { message: `${module.name} Deleted` } + return { message: `${table.name} Deleted` } } ), - [`create${module.name}`]: get( - module, + [`insert_${rowsFieldName}`]: get( + table, "graphql.mutations.create", async (_, args, context, info) => { + wLogWithDateWithInfo( + "[Wertik-GraphQL-Mutation]", + `${info.fieldName} - ${JSON.stringify(args)}` + ) const argsFromEvent = await get( - module, + table, "events.beforeCreate", - function () {} + voidFunction )(_, args, context, info) args = argsFromEvent ? argsFromEvent : args const response = [] @@ -173,53 +176,108 @@ export default function (module, schemaInformation, store) { ), }, Query: { - [`view${module.name}`]: get( - module, + [singleRowFieldName]: get( + table, "graphql.queries.view", async (_, args, context, info) => { + wLogWithDateWithInfo( + "[Wertik-GraphQL-Query]", + `${info.fieldName} - ${JSON.stringify(args)}` + ) const argsFromEvent = await get( - module, + table, "events.beforeView", - function () {} + voidFunction )(_, args, context, info) + const keys = [ + ...store.database.relationships.map((c) => c.graphqlKey), + ...store.graphql.graphqlKeys, + ] + args = argsFromEvent ? argsFromEvent : args - const where = await convertFiltersIntoSequalizeObject( + const where = await convertFiltersIntoSequelizeObject( args.where ) + + const convertFieldsIntoInclude = + convertGraphqlRequestedFieldsIntoInclude( + graphqlFields(info, {}, { processArguments: true }), + args, + table.name + ) + const find = await schemaInformation.tableInstance.findOne({ - where: where, + where: omit(where, keys), + attributes: generateRequestedFieldsFromGraphqlInfo( + schemaInformation.tableInstance.tableName, + graphqlFields(info) + ), + include: convertFieldsIntoInclude.include, + order: convertFieldsIntoInclude.order, }) + return find } ), - [`list${module.name}`]: get( - module, + [rowsFieldName]: get( + table, "graphql.queries.list", async (_, args, context, info) => { + wLogWithDateWithInfo( + "[Wertik-GraphQL-Query]", + `${rowsFieldName} - args ${JSON.stringify(args)}` + ) const argsFromEvent = await get( - module, + table, "events.beforeList", - function () {} + voidFunction )(_, args, context, info) args = argsFromEvent ? argsFromEvent : args - return await paginate(args, schemaInformation.tableInstance) + + const convertFieldsIntoInclude = + convertGraphqlRequestedFieldsIntoInclude( + graphqlFields(info, {}, { processArguments: true }), + args, + table.name + ) + + return await paginate( + args, + schemaInformation.tableInstance, + convertFieldsIntoInclude.include, + { + attributes: generateRequestedFieldsFromGraphqlInfo( + schemaInformation.tableInstance.tableName, + graphqlFields(info).rows + ), + }, + convertFieldsIntoInclude.order + ) } ), - [`count${module.name}`]: get( - module, + [`count${table.name}`]: get( + table, "graphql.queries.count", async (_, args, context, info) => { + wLogWithDateWithInfo( + "[Wertik-GraphQL-Query]", + `${info.fieldName} - ${JSON.stringify(args)}` + ) const argsFromEvent = await get( - module, + table, "events.beforeCount", - function () {} + voidFunction )(_, args, context, info) args = argsFromEvent ? argsFromEvent : args - const where = await convertFiltersIntoSequalizeObject( + const where = await convertFiltersIntoSequelizeObject( args.where ) + const keys = [ + ...store.database.relationships.map((c) => c.graphqlKey), + ...store.graphql.graphqlKeys, + ] const count = await schemaInformation.tableInstance.count({ - where: where, + where: omit(where, keys), }) return count } diff --git a/src/crud/paginate.ts b/src/crud/paginate.ts new file mode 100644 index 00000000..c622c431 --- /dev/null +++ b/src/crud/paginate.ts @@ -0,0 +1,45 @@ +import convertFiltersIntoSequelizeObject from "../utils/convertFiltersIntoSequelizeObject" +import omit from "lodash.omit" +import isPlainObject from "lodash.isplainobject" +import { wertikApp } from "../store" + +export const paginate = async ( + arg, + tableInstance, + includes: any[] = [], + queryOptions: { [key: string]: any } = {}, + order = [] +) => { + const { page = 1, limit = 100 } = arg.pagination ?? {} + const offset = limit * (page - 1) + const keys = [ + ...wertikApp.store.database.relationships.map((c) => c.graphqlKey), + ...wertikApp.store.graphql.graphqlKeys, + ] + let where = omit(convertFiltersIntoSequelizeObject(arg.where), keys) + + const { count, rows } = await tableInstance.findAndCountAll({ + where, + offset, + limit, + order, + include: includes, + ...queryOptions, + }) + + const totalPages = Math.ceil(count / limit) + const pagination = { + total: count, + nextPage: page + 1, + page, + previousPage: page === 1 ? 1 : page - 1, + pages: totalPages, + hasMore: page < totalPages, + limit, + } + return { + rows: rows, + paginationProperties: pagination, + pagination, + } +} diff --git a/src/database/database.ts b/src/database/database.ts deleted file mode 100644 index 1294b664..00000000 --- a/src/database/database.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { get } from "lodash" -import { paginate } from "../crud/index" -import { Store } from "../types" -import { WertikApp } from "../types" - -export const applyRelationshipsFromStoreToDatabase = async ( - store: Store, - app: WertikApp -) => { - store.database.relationships.forEach((element) => { - const currentTable = app.modules[element.currentModule].tableInstance - const referencedTable = app.modules[element.referencedModule].tableInstance - // element.type will be hasOne, hasMany, belongsTo or belongsToMany - currentTable[element.type](referencedTable, element.options || {}) - }) -} - -export const applyRelationshipsFromStoreToGraphql = async ( - store: Store, - _app: WertikApp -) => { - store.database.relationships.forEach((element) => { - const oldResolvers = get( - store, - `graphql.resolvers.${element.currentModule}`, - {} - ) - - store.graphql.resolvers[element.currentModule] = { - ...oldResolvers, - [element.graphqlKey]: async (parent, _args, context) => { - const tableInstance = - context.wertik.modules[element.referencedModule].tableInstance - let referencedModuleKey = - element.options.sourceKey || element.options.targetKey - let currentModuleKey = element.options.foreignKey || "id" - - if (!referencedModuleKey) { - referencedModuleKey = "id" - } - - if (["hasOne", "belongsTo"].includes(element.type)) { - return await tableInstance.findOne({ - where: { - [currentModuleKey]: parent[referencedModuleKey], - }, - }) - } else if (["hasMany", "belongsToMany"]) { - return await paginate( - { - where: { - [currentModuleKey]: parent[referencedModuleKey], - }, - }, - tableInstance - ) - } - }, - } - }) -} diff --git a/src/database/eagerLoadingGraphqlQuery.ts b/src/database/eagerLoadingGraphqlQuery.ts new file mode 100644 index 00000000..f1848e86 --- /dev/null +++ b/src/database/eagerLoadingGraphqlQuery.ts @@ -0,0 +1,130 @@ +import { wertikApp } from "../store" +import isPlainObject from "lodash.isplainobject" +import get from "lodash.get" +import has from "lodash.has" +import convertFiltersIntoSequelizeObject from "../utils/convertFiltersIntoSequelizeObject" +import { generateRequestedFieldsFromGraphqlInfo } from "../modules/modulesHelpers" +import { SqlTable } from "src/types/database" + +const clean = (cleanObject) => { + let recursion = (_obj) => { + Object.keys(_obj).forEach((key) => { + if (key === "rows") { + _obj = { ..._obj, ..._obj["rows"] } + delete _obj["rows"] + } + }) + + Object.keys(_obj).forEach((key) => { + if (isPlainObject(_obj[key]) && key !== "__arguments") { + _obj[key] = recursion(_obj[key]) + } + }) + + return _obj + } + + return recursion(cleanObject) +} + +export const convertGraphqlRequestedFieldsIntoInclude = ( + graphqlFields = {}, + args: any = {}, + tableName: string = "" +) => { + let order = [] + let depth = [] + graphqlFields = clean(graphqlFields) + let currentModel = wertikApp.models[tableName]; + let currentModuleRelationshipsKeys =[] + let allRelationshipKeys = [] + + for (const [modelName, model] of Object.entries(wertikApp.models)) { + for (const [key, relationship] of Object.entries(model.associations)) { + allRelationshipKeys.push(key) + currentModuleRelationshipsKeys.push(key) + } + } + + const requiredFilters = currentModuleRelationshipsKeys.filter((c) => + Object.keys(args.where ?? {}).includes(c) + ) + + Object.keys(args.order ?? {}).forEach((element) => { + order.push([element, args.order[element]]) + }) + + let recursion = (_obj) => { + let includes = [] + + Object.keys(_obj).forEach((key) => { + if (allRelationshipKeys.includes(key)) { + currentModel = currentModel.associations[key].target; + depth.push(key) + let _localDepth = [...JSON.parse(JSON.stringify(depth))] + const includeParams: { [key: string]: any } = { + required: false, + model: wertikApp.models[currentModel.tableName], + as: key, + attributes: generateRequestedFieldsFromGraphqlInfo(currentModel.tableName,_obj[key]), + include: + Object.keys(_obj[key]).length > 0 ? recursion(_obj[key]) : [], + } + + let __arguments = get(_obj, `[${key}].__arguments`, []) + let __whereInArguments = __arguments.find((c) => has(c, "where")) + let __orderInArguments = __arguments.find((c) => has(c, "order")) + let __limitInArguments = __arguments.find((c) => has(c, "limit")) + let __offsetInArguments = __arguments.find((c) => has(c, "offset")) + __limitInArguments = get(__limitInArguments, "limit.value", null) + __offsetInArguments = get(__offsetInArguments, "offset.value", null) + __orderInArguments = get(__orderInArguments, "order.value", null) + + if (isPlainObject(__orderInArguments)) { + Object.keys(__orderInArguments).forEach((element) => { + order.push([..._localDepth, element, __orderInArguments[element]]) + }) + } + + if (__whereInArguments) { + __whereInArguments = get(__whereInArguments, "where.value", {}) + __whereInArguments = + convertFiltersIntoSequelizeObject(__whereInArguments) + + includeParams.where = __whereInArguments + } + + if (__limitInArguments) includeParams.limit = __limitInArguments + if (__offsetInArguments) includeParams.offset = __offsetInArguments + includes.push(includeParams) + } + }) + return includes + } + + let include = recursion(graphqlFields) + /** + * Make sure the include is required if filters are requested in root level filters. + * If root level filters are not met then the response will be null. + * In below graphql query, it will return if user has id 2 and written a post which id is 132, if id is not found then whole response will be null. + query viewUser { + viewUser(where: { id: { _eq: 2 }, posts: { id: { _eq: 123 } } }) { + id + name + } + } + */ + include = include.map((c) => { + if (requiredFilters.includes(c.as)) { + c.required = true + c.where = convertFiltersIntoSequelizeObject(args.where[c.as]) + } + + return c + }) + + return { + include, + order, + } +} diff --git a/src/database/helpers.ts b/src/database/helpers.ts index 2c6777a5..d671c822 100644 --- a/src/database/helpers.ts +++ b/src/database/helpers.ts @@ -1,4 +1,4 @@ -import { MysqlColumnInfoDescribeTable } from "src/types/database" +import { MysqlColumnInfoDescribeTable, SqlTable } from "../types/database" import { capitalizeFirstLetter } from "../utils/capitalizeFirstLetter" import { numberTypes, @@ -7,48 +7,50 @@ import { enumTypes, jsonTypes, } from "./mysql/getTableInfo" +import { WertikApp } from "../types" +import get from "lodash.get" +import { wLogWithError, wLogWithInfo } from "../utils/log" export const convertDatabaseTypeIntoGraphqlType = ( columnInfo: MysqlColumnInfoDescribeTable, tableName: string ) => { - var isPrimary = columnInfo.Key === "PRI" - var limit = columnInfo.Type.match(/\d+/g) - var isRequiredIndicator = columnInfo.Null === "NO" ? "!" : "" - if (limit) limit[0] + let isPrimary = columnInfo.Key === "PRI" + // let limit = columnInfo.Type.match(/\d+/g) + let isRequiredIndicator = columnInfo.Null === "NO" ? "!" : "" if (columnInfo.Type.toLowerCase() === "tinyint(1)") { return { graphqlType: `Boolean`, - graphqlCreateInputType: `Boolean${isRequiredIndicator}`, + graphqlInsertInputType: `Boolean${isRequiredIndicator}`, graphqlUpdateInputType: `Boolean${isRequiredIndicator}`, databaseType: "INTEGER", } } else if (numberTypes.find((c) => columnInfo.Type.includes(c))) { return { graphqlType: `Int`, - graphqlCreateInputType: `Int`, + graphqlInsertInputType: `Int`, graphqlUpdateInputType: `Int${isPrimary ? "!" : ""}`, databaseType: "INTEGER", } } else if (jsonTypes.find((c) => columnInfo.Type.includes(c))) { return { graphqlType: `JSON`, - graphqlCreateInputType: `String${isRequiredIndicator}`, + graphqlInsertInputType: `String${isRequiredIndicator}`, graphqlUpdateInputType: `String${isRequiredIndicator}`, databaseType: "STRING", } } else if (stringTypes.find((c) => columnInfo.Type.includes(c))) { return { graphqlType: `String`, - graphqlCreateInputType: `String${isRequiredIndicator}`, + graphqlInsertInputType: `String${isRequiredIndicator}`, graphqlUpdateInputType: `String${isRequiredIndicator}`, databaseType: "STRING", } } else if (dateTypes.find((c) => columnInfo.Type.includes(c))) { return { graphqlType: `String`, - graphqlCreateInputType: `String${isRequiredIndicator}`, + graphqlInsertInputType: `String${isRequiredIndicator}`, graphqlUpdateInputType: `String${isRequiredIndicator}`, databaseType: "STRING", isDateColumn: true, @@ -58,7 +60,7 @@ export const convertDatabaseTypeIntoGraphqlType = ( graphqlType: `${capitalizeFirstLetter(tableName)}${capitalizeFirstLetter( columnInfo.Field )}Enum`, - graphqlCreateInputType: `${capitalizeFirstLetter( + graphqlInsertInputType: `${capitalizeFirstLetter( tableName )}${capitalizeFirstLetter(columnInfo.Field)}Enum${isRequiredIndicator}`, graphqlUpdateInputType: `${capitalizeFirstLetter( @@ -74,3 +76,42 @@ export const convertDatabaseTypeIntoGraphqlType = ( } } } + + + +export const applyRelationshipsFromStoreToDatabase = async ( + app: WertikApp +) => { + Object.keys(app.database).forEach(dbName => { + let db = app.database[dbName]; + const tables = db?.credentials?.tables || [] + tables.forEach(table => { + const currentModel = app.models[table.name]; + + for (const [relationshipType, relationships] of Object.entries(table.relationships|| {})) { + for (const [relatedTableName, relationshipOptions] of Object.entries(table.relationships[relationshipType] || {})) { + const relatedTable = app.models[relatedTableName]; + if (!relatedTable) { + wLogWithError(`[DB] Related table not found:`, `model '${relatedTableName}' not found for relationship '${relationshipType}' in table '${table.name}'`) + process.exit() + } + wLogWithInfo(`[DB] Applying relationship:`, `${table.name}.${relationshipType}(${relatedTable.tableName},${JSON.stringify(relationshipOptions)})`) + currentModel[relationshipType](relatedTable, relationshipOptions) + const isManyRelationship = ['hasMany','belongsToMany'].includes(relationshipType) + if (isManyRelationship) { + app.store.graphql.typeDefs = app.store.graphql.typeDefs.concat(` + extend type ${table.name} { + ${relationshipOptions.as}(offset: Int, limit: Int, where: ${table.name}_filter_input, order: ${table.name}_order_input): [${relatedTableName}] + } + `) + }else { + app.store.graphql.typeDefs = app.store.graphql.typeDefs.concat(` + extend type ${table.name} { + ${relationshipOptions.as}: ${relatedTableName} + }`) + } + } + } + }); + }); +} diff --git a/src/database/mysql/getTableInfo.ts b/src/database/mysql/getTableInfo.ts index 7eb3aa0d..e6638d2f 100644 --- a/src/database/mysql/getTableInfo.ts +++ b/src/database/mysql/getTableInfo.ts @@ -1,4 +1,4 @@ -import { useModuleProps } from "src/types/modules" +import { WithModuleProps } from "src/types/modules" import { convertDatabaseTypeIntoGraphqlType } from "../helpers" import { MysqlColumnInfoDescribeTable, TableInfo } from "./../../types/database" @@ -30,10 +30,10 @@ export const enumTypes = ["enum"] export const jsonTypes = ["json"] export const getMysqlTableInfo = async ( - module: useModuleProps, + tableName: string, sequelize: any ): Promise => { - let rows = await sequelize.query(`describe ${module.table};`) + let rows = await sequelize.query(`describe ${tableName};`) rows = rows[0] if (rows) { @@ -42,18 +42,19 @@ export const getMysqlTableInfo = async ( ).map((element) => { const graphqlType = convertDatabaseTypeIntoGraphqlType( element, - module.name + tableName ) - var isPrimary = element.Key === "PRI" + let isPrimary = element.Key === "PRI" + const isNull = element.Null === "YES" return { columnName: element.Field, default: element.Default, graphqlType: graphqlType.graphqlType, - graphqlCreateInputType: graphqlType.graphqlCreateInputType, + graphqlInsertInputType: graphqlType.graphqlInsertInputType, graphqlUpdateInputType: graphqlType.graphqlUpdateInputType, enumValues: graphqlType.enumValues, - isNull: element.Null === "no" ? false : true, + isNull: isNull, isEnum: graphqlType.isEnum, databaseType: graphqlType.databaseType, isPrimary: isPrimary, @@ -62,14 +63,14 @@ export const getMysqlTableInfo = async ( }) return { - name: module.table, + name: tableName, columns: fields, originalDescribeColumns: rows, } } return { - name: module.table, + name: tableName, columns: [], originalDescribeColumns: rows, } diff --git a/src/database/mysql/mysql.ts b/src/database/mysql/mysql.ts index 5df338f6..af045013 100644 --- a/src/database/mysql/mysql.ts +++ b/src/database/mysql/mysql.ts @@ -1,9 +1,25 @@ -import { Sequelize } from "sequelize" +import { Model, ModelAttributes, ModelCtor, Sequelize } from "sequelize" import { databaseDefaultOptions } from "../../utils/defaultOptions" -import { useMysqlDatabaseProps } from "../../types/database" -import { get } from "lodash" +import { WithMysqlDatabaseProps } from "../../types/database" +import get from "lodash.get" +import { + wLog, + wLogWithError, + wLogWithInfo, + wLogWithSuccess, +} from "../../utils/log" +import { getMysqlTableInfo } from "./getTableInfo" +import { + convertWordIntoSingular, + generateEnumTypeForGraphql, + generateGenerateGraphQLCrud, + getInsertSchema, + getOrderSchema, + getUpdateSchema, +} from "../../modules/modulesHelpers" +import { wertikApp } from "../../store" -export const getAllRelationships = (dbName: String) => { +export const getAllRelationships = (dbName: string) => { return ` SELECT * FROM information_schema.KEY_COLUMN_USAGE @@ -14,7 +30,7 @@ export const getAllRelationships = (dbName: String) => { ` } -export const useMysqlDatabase = function (obj: useMysqlDatabaseProps) { +export const withMysqlDatabase = function (obj: WithMysqlDatabaseProps) { return async () => { try { let sequelize = new Sequelize(obj.name, obj.username, obj.password, { @@ -24,18 +40,111 @@ export const useMysqlDatabase = function (obj: useMysqlDatabaseProps) { ...get(obj, "options", {}), ...(databaseDefaultOptions as any).sql.dbInitializeOptions, }) - await sequelize.authenticate() - console.log(`[DB] Succcessfully connected to database ${obj.name}`) + await sequelize.authenticate().catch((error) => { + wLogWithError("[DB] Connecting failed to database", obj.name) + wLogWithError("[DB] Error", error.message) + wLogWithInfo("[DB] Error Info") + wLog(error) + process.exit(1) + }) + let models: ModelCtor>[] = [] + obj.tables?.forEach(async (table) => { + + let graphqlSchema = [`type ${table.name} {`] + let listSchema = "" + let filterSchema = [ + `input ${convertWordIntoSingular(table.name)}_filter_input {`, + ] + let updateSchema = "" + let insertSchema = "" + let orderSchema = "" + + const tableInfo = await getMysqlTableInfo(table.name, sequelize) + + let fields: ModelAttributes, any> = {} + + tableInfo.columns.forEach((column) => { + // if (column.columnName === "id") return + + if (column.isEnum) { + wertikApp.store.graphql.typeDefs = wertikApp.store.graphql.typeDefs.concat( + generateEnumTypeForGraphql(column) + ) + } + + updateSchema = getUpdateSchema(table, tableInfo) + insertSchema = getInsertSchema(table, tableInfo) + orderSchema = getOrderSchema(table, tableInfo) + + graphqlSchema.push(`${column.columnName}: ${column.graphqlType}`) + + let filter_input = + column.databaseType.toLowerCase() === "enum" + ? `${column.columnName}: ${column.graphqlType}` + : `${ + column.columnName + }: ${column.graphqlType.toLowerCase()}_filter_input` + + filterSchema.push(filter_input) + + fields[column.columnName] = { + type: column.databaseType, + allowNull: column.isNull, + defaultValue: column.default, + primaryKey: column.isPrimary, + values: column.isEnum ? column.enumValues : null, + } + }) + graphqlSchema.push("}") + filterSchema.push("}") + + const tableInstance = sequelize.define( + table.name, + { + ...fields, + ...get(table, "extendFields", {}), + }, + { + ...get(table, "tableOptions", {}), + ...databaseDefaultOptions.sql.defaultTableOptions, + } + ) + + + wertikApp.models[table.name] = tableInstance + const schemaInformation = { + moduleName: table.name, + tableInstance: tableInstance, + schema: graphqlSchema.join(`\n`), + inputSchema: { + insert: insertSchema || "", + update: updateSchema || "", + list: listSchema, + filters: filterSchema.join("\n"), + order_schema: orderSchema || "", + }, + } + + generateGenerateGraphQLCrud(table, schemaInformation) + + models.push(tableInstance) + }) + + wLogWithSuccess( + `[Wertik-Mysql-Database]`, + `Successfully connected to database ${obj.name}` + ) ;(sequelize as any).relationships = await sequelize.query( getAllRelationships(obj.name) ) return { credentials: obj, instance: sequelize, + models: models, } } catch (e) { - console.log(`[DB] Connecting failed to database ${obj.name}`) - console.log(e.message) + wLog(`[DB] Connecting failed to database ${obj.name}`) + wLog(e.message) } } } @@ -43,4 +152,4 @@ export const useMysqlDatabase = function (obj: useMysqlDatabaseProps) { /** * @deprecated use useMysqlDatabase, useDatabase is deprecated and will be removed in 3.5.0 version. */ -export const useDatabase = useMysqlDatabase +export const useDatabase = withMysqlDatabase diff --git a/src/devServer.ts b/src/devServer.ts index 7c5566ce..2d35c464 100644 --- a/src/devServer.ts +++ b/src/devServer.ts @@ -1,36 +1,139 @@ -import wertik, { useMysqlDatabase, useGraphql, useModule } from "./index" +import wertik, { + withMysqlDatabase, + withApolloGraphql, + withModule, + withWebSockets, + withSocketIO, + withIndependentWebSocketsServer, + withLogger, + withWinstonTransport, + withMailer, + withRedis, +} from "./index" +import modules from "./devServerTestModules" wertik({ port: 1200, - graphql: useGraphql(), + graphql: withApolloGraphql({ + storeTypeDefFilePath: process.cwd() + "/graphqlSchema.graphql", + }), database: { - wapgee: useMysqlDatabase({ + wertik: withMysqlDatabase({ port: 3306, - name: "wapgee", - host: "localhost", + name: "wertik_test", + host: "127.0.0.1", password: "pass", username: "root", + tables: [ + { + name: "user", + relationships: { + hasMany: { + product: { + as: "products", + foreignKey: "user_id", + sourceKey: "id", + } + } + } + }, + { + name: "product", + relationships: { + hasOne: { + user: { + as: "user", + foreignKey: "id", + sourceKey: "user_id", + } + } + } + }, + ], }), - test: useMysqlDatabase({ + default: withMysqlDatabase({ username: "root", - port: 3306, password: "pass", + name: "wapgee_prod", host: "localhost", - name: "wertik", + port: 3306, + }), + }, + // modules: modules, + // Product: withModule({ + // name: "Product", + // useDatabase: true, + // database: "wertik", + // table: "product", + // on: function ({ belongsTo }) { + // belongsTo({ + // database: "wertik", + // graphqlKey: "user", + // module: "User", + // options: { + // as: "user", + // foreignKey: "user_id", + // targetKey: "id", + // }, + // }) + // }, + // }), + // User: withModule({ + // name: "User", + // useDatabase: true, + // database: "wertik", + // table: "user", + // on: function ({ hasMany }) { + // hasMany({ + // database: "wertik", + // graphqlKey: "products", + // module: "Product", + // options: { + // as: "products", + // foreignKey: "user_id", + // sourceKey: "id", + // }, + // }) + // }, + // }), + // Category: withModule({ + // name: "Category", + // useDatabase: true, + // database: "wertik", + // table: "category", + // }), + // }, + sockets: { + mySockets: withWebSockets({ + path: "/websockets", + }), + socketio: withSocketIO({ + path: "/mysocketioserver", + }), + mySockets2: withIndependentWebSocketsServer({ + port: 1500, }), }, - modules: { - User: useModule({ - name: "User", - useDatabase: true, - table: "users", - database: "wapgee", + logger: withLogger({ + transports: withWinstonTransport((winston) => { + return [ + new winston.transports.File({ + filename: "info.log", + level: "info", + }), + ] }), - test: useModule({ - name: "Shirts", - useDatabase: true, - database: "test", - table: "shirts", + }), + // mailer: { + // instances: { + // default: withMailer({ + // name: "Default", + // }), + // }, + // }, + redis: { + testRedis: withRedis({ + name: "testRedis", }), }, }) diff --git a/src/devServerTestModules.ts b/src/devServerTestModules.ts new file mode 100644 index 00000000..2dc818b1 --- /dev/null +++ b/src/devServerTestModules.ts @@ -0,0 +1,78 @@ +import wertik, { withModule } from "./index" + +export default { + User: withModule({ + name: "User", + useDatabase: true, + database: "default", + table: "users", + on: function ({ hasOne, hasMany, belongsTo, getExpress }) { + hasMany({ + database: "default", + module: "Post", + graphqlKey: "posts", + options: { + as: "posts", + foreignKey: "created_by", + sourceKey: "id", + }, + }) + }, + }), + Comment: withModule({ + name: "Comment", + useDatabase: true, + database: "default", + table: "comments", + on: function ({ hasOne }) { + hasOne({ + module: "Post", + graphqlKey: "post", + database: "default", + options: { + sourceKey: "post_id", + foreignKey: "id", + as: "post", + }, + }) + hasOne({ + module: "User", + graphqlKey: "created_by", + database: "default", + options: { + sourceKey: "created_by_id", + foreignKey: "id", + as: "created_by", + }, + }) + }, + }), + Post: withModule({ + name: "Post", + useDatabase: true, + database: "default", + table: "post", + on: function ({ hasOne, hasMany, belongsTo, getExpress }) { + hasOne({ + module: "User", + graphqlKey: "author", + database: "default", + options: { + as: "author", + sourceKey: "created_by", + foreignKey: "id", + }, + }) + hasOne({ + module: "User", + graphqlKey: "last_updated_by", + database: "default", + options: { + as: "last_updated_by", + sourceKey: "last_updated_by_id", + foreignKey: "id", + }, + }) + }, + }), +} diff --git a/src/graphql/generalSchema.ts b/src/graphql/generalSchema.ts index eee9866d..56c5fd32 100644 --- a/src/graphql/generalSchema.ts +++ b/src/graphql/generalSchema.ts @@ -6,7 +6,12 @@ export default ` scalar JSON scalar JSONObject - input StringFilterInput { + enum order_by { + ASC + DESC + } + + input string_filter_input { _eq: String _ne: String _like: String @@ -20,12 +25,12 @@ export default ` _notRegexp: String _iRegexp: String _notIRegexp: String - _or: StringFilterInput - _and: StringFilterInput + _or: string_filter_input + _and: string_filter_input _between: [String] _notBetween: [String] } - input IntFilterInput { + input int_filter_input { _eq: Int _gt: Int _gte: Int @@ -36,10 +41,10 @@ export default ` _notBetween: [Int] _in: [Int] _notIn: [Int] - _or: IntFilterInput - _and: IntFilterInput + _or: int_filter_input + _and: int_filter_input } - input DateFilterInput { + input date_filter_input { _eq: String _gt: String _gte: String @@ -49,7 +54,7 @@ export default ` _neq: String _nin: [String!] } - input BooleanFilterInput { + input boolean_filter_input { _eq: Boolean _ne: Boolean } @@ -65,13 +70,18 @@ export default ` page: Int limit: Int } - input FilterInput { + input filter_input { column: String! operator: String! value: String! } type Pagination { + total: Int + pages: Int page: Int + nextPage: Int + previousPage: Int + hasMore: Boolean limit: Int } type Filter { @@ -88,4 +98,5 @@ export default ` hasMore: Boolean limit: Int } + ` diff --git a/src/graphql/index.ts b/src/graphql/index.ts index 4d5513e1..9be0e5d1 100644 --- a/src/graphql/index.ts +++ b/src/graphql/index.ts @@ -1,36 +1,57 @@ -import { get, omit } from "lodash" +import fs from "fs" +import get from "lodash.get" +import omit from "lodash.omit" import { defaultApolloGraphqlOptions } from "../utils/defaultOptions" import { ApolloServer } from "apollo-server-express" -import { useGraphqlProps, GraphqlInitializeProps } from "../types/graphql" +import graphqlDepthLimit from "graphql-depth-limit" +import prettier from "prettier" -export const useGraphql = (props?: useGraphqlProps) => { +import { + WithApolloGraphqlProps, + GraphqlInitializeProps, +} from "../types/graphql" +import { wLogWithSuccess } from "../utils/log" + +export const withApolloGraphql = (props?: WithApolloGraphqlProps) => { return ({ wertikApp, expressApp, - store, configuration, }: GraphqlInitializeProps) => { + const depthLimit = get(props, "validation.depthLimit", 7) props = props ? props : {} - store.graphql.typeDefs = store.graphql.typeDefs.concat( + wertikApp.store.graphql.typeDefs = wertikApp.store.graphql.typeDefs.concat( get(configuration, "graphql.typeDefs", "") ) - store.graphql.resolvers.Query = { - ...store.graphql.resolvers.Query, + wertikApp.store.graphql.resolvers.Query = { + ...wertikApp.store.graphql.resolvers.Query, ...get(configuration, "graphql.resolvers.Query", {}), } - store.graphql.resolvers.Mutation = { - ...store.graphql.resolvers.Mutation, + wertikApp.store.graphql.resolvers.Mutation = { + ...wertikApp.store.graphql.resolvers.Mutation, ...get(configuration, "graphql.resolvers.Mutation", {}), } const options = { ...get(configuration, "graphql.options", {}) } + if (props && props.storeTypeDefFilePath) { + if (fs.existsSync(props.storeTypeDefFilePath)) + fs.unlinkSync(props.storeTypeDefFilePath) + + const formattedTypeDefs = prettier.format(wertikApp.store.graphql.typeDefs, { + filepath: props.storeTypeDefFilePath, + semi: false, + parser: "graphql", + }) + fs.writeFileSync(props.storeTypeDefFilePath, formattedTypeDefs) + } + const GraphqlApolloServer = new ApolloServer({ - typeDefs: store.graphql.typeDefs, + typeDefs: wertikApp.store.graphql.typeDefs, resolvers: { - ...store.graphql.resolvers, + ...wertikApp.store.graphql.resolvers, }, ...defaultApolloGraphqlOptions, ...omit(options, ["context"]), @@ -44,6 +65,7 @@ export const useGraphql = (props?: useGraphqlProps) => { ...contextFromOptions, } }, + validationRules: [graphqlDepthLimit(depthLimit)], }) GraphqlApolloServer.applyMiddleware({ @@ -51,10 +73,11 @@ export const useGraphql = (props?: useGraphqlProps) => { ...(props?.applyMiddlewareOptions ?? {}), }) - console.log( - `GraphQL server starting at http://localhost:${ - configuration.port ?? 1200 - }/${props?.applyMiddlewareOptions?.path ?? "graphql"}` + wLogWithSuccess( + "[Wertik-Graphql]", + `http://localhost:${configuration.port ?? 1200}/${ + props?.applyMiddlewareOptions?.path ?? "graphql" + }` ) return GraphqlApolloServer diff --git a/src/helpers/modules/backup.ts b/src/helpers/modules/backup.ts index f38867b8..25024585 100644 --- a/src/helpers/modules/backup.ts +++ b/src/helpers/modules/backup.ts @@ -1,7 +1,8 @@ -import moment from "moment" -import { useModule } from "../../modules/modules" +import dayjs from "./../../utils/dayjs" +import { withModule } from "../../modules/modules" import mysqldump from "mysqldump" import fs from "fs" +import { wLog } from "../../utils/log" const dumpDatabase = async ( dbName: string, @@ -13,7 +14,7 @@ const dumpDatabase = async ( name: string } ) => { - const filename = `backups/${moment().format( + const filename = `backups/${dayjs().format( "MMMM-DD-YYYY-h-mm-ss-a" )}-database-${dbName}.sql`.toLowerCase() @@ -44,7 +45,7 @@ const uploadDumpToDigitalOceanSpaces = async ( Bucket, ACL ) => { - const data = await fs.readFileSync(filename) + const data = fs.readFileSync(filename) const params = { Bucket: Bucket, @@ -58,7 +59,7 @@ const uploadDumpToDigitalOceanSpaces = async ( return response } const uploadDumpToDropbox = async (filename, dropboxInstance) => { - const data: Buffer = await fs.readFileSync(filename) + const data: Buffer = fs.readFileSync(filename) const response = await dropboxInstance.dropbox.filesUpload({ strict_conflict: false, path: `/${filename}`, @@ -72,21 +73,21 @@ export const WertikBackupModule = ( table: string, tableOptions: any = {} ) => - useModule({ + withModule({ name: "Backup", useDatabase: true, database: database, table: table, tableOptions: tableOptions, - on: function ({ useSchema, useMutation }) { - useSchema(` + on: function ({ extendSchema, addMutation }) { + extendSchema(` type BackupSuccessResponse { message: String filename: String - backup: Backup + backup: BackupModule } `) - useMutation({ + addMutation({ name: "backupLocal", query: "backupLocal(database: [String]!): [BackupSuccessResponse]", async resolver(_, args, context) { @@ -107,7 +108,7 @@ export const WertikBackupModule = ( return push }, }) - useMutation({ + addMutation({ name: "backupDigitalOceanSpaces", query: "backupDigitalOceanSpaces(ACL: String!, Bucket: String!, storage: String!, database: [String]!): [BackupSuccessResponse]", @@ -151,7 +152,7 @@ export const WertikBackupModule = ( } }, }) - useMutation({ + addMutation({ name: "backupDropbox", query: "backupDropbox(storage: String!, database: [String]): [BackupSuccessResponse]", @@ -188,7 +189,7 @@ export const WertikBackupModule = ( return push } catch (e) { - console.log(e) + wLog(e) throw new Error(e) } }, diff --git a/src/index.ts b/src/index.ts index 75040953..2b1f428c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,17 +1,15 @@ -import { get } from "lodash" +import get from "lodash.get" import express from "express" -import store from "./store" -import { - applyRelationshipsFromStoreToDatabase, - applyRelationshipsFromStoreToGraphql, -} from "./database/database" +import { wertikApp } from "./store" +import { applyRelationshipsFromStoreToDatabase } from "./database/helpers" import { emailSender } from "./mailer/index" import http from "http" -import { WertikConfiguration } from "./types" -import { WertikApp } from "./types" +import { WertikConfiguration, WertikApp } from "./types" import { initializeBullBoard } from "./queue/index" +import { wLogWithInfo, wLogWithSuccess } from "./utils/log" +import { validateModules } from "./modules/modules" -export * from "./database/database" +export * from "./database/mysql/mysql" export * from "./modules/modules" export * from "./graphql" export * from "./mailer" @@ -23,7 +21,6 @@ export * from "./queue" export * from "./redis" export * from "./logger" export * from "./database/mysql/mysql" -export * from "./database/database" const Wertik: (configuration?: WertikConfiguration) => Promise = ( configuration: WertikConfiguration @@ -32,27 +29,13 @@ const Wertik: (configuration?: WertikConfiguration) => Promise = ( return new Promise(async (resolve, reject) => { try { configuration.appEnv = configuration.appEnv ?? "local" - const wertikApp: WertikApp = { - appEnv: configuration.appEnv, - port: 1200, - modules: {}, - models: {}, - database: {}, - mailer: {}, - graphql: {}, - sockets: {}, - cronJobs: {}, - storage: {}, - queue: { - jobs: {}, - bullBoard: {}, - }, - redis: {}, - logger: null, - } const port = get(configuration, "port", 1200) - const skip = get(configuration, "skip", false) + const selfStart = get( + configuration, + "selfStart", + get(configuration, "skip", true) + ) const expressApp = get(configuration, "express", express()) const httpServer = http.createServer(expressApp) @@ -61,7 +44,7 @@ const Wertik: (configuration?: WertikConfiguration) => Promise = ( wertikApp.express = expressApp wertikApp.port = configuration.port - if (configuration.mailer && configuration.mailer.instances) { + if (configuration?.mailer?.instances) { for (const mailName of Object.keys( configuration.mailer.instances || {} )) { @@ -71,33 +54,27 @@ const Wertik: (configuration?: WertikConfiguration) => Promise = ( } } - if (configuration.storage) { + if (configuration?.storage) { for (const storageName of Object.keys(configuration.storage || {})) { - wertikApp.storage[storageName] = await configuration.storage[ - storageName - ]({ + wertikApp.storage[storageName] = configuration.storage[storageName]({ configuration: configuration, wertikApp: wertikApp, }) } } - if (configuration.cronJobs) { + if (configuration?.cronJobs) { for (const cronName of Object.keys(configuration.cronJobs || {})) { - wertikApp.cronJobs[cronName] = await configuration.cronJobs[cronName]( - { - configuration: configuration, - wertikApp: wertikApp, - } - ) + wertikApp.cronJobs[cronName] = configuration.cronJobs[cronName]({ + configuration: configuration, + wertikApp: wertikApp, + }) } } if (configuration.sockets) { for (const socketName of Object.keys(configuration.sockets || {})) { - wertikApp.sockets[socketName] = await configuration.sockets[ - socketName - ]({ + wertikApp.sockets[socketName] = configuration.sockets[socketName]({ configuration: configuration, wertikApp: wertikApp, }) @@ -107,6 +84,7 @@ const Wertik: (configuration?: WertikConfiguration) => Promise = ( if (configuration.database) { for (const databaseName of Object.keys(configuration.database || {})) { try { + //@ts-ignore wertikApp.database[databaseName] = await configuration.database[ databaseName ]() @@ -121,7 +99,6 @@ const Wertik: (configuration?: WertikConfiguration) => Promise = ( wertikApp.modules[moduleName] = await configuration.modules[ moduleName ]({ - store: store, configuration: configuration, app: wertikApp, }) @@ -130,9 +107,8 @@ const Wertik: (configuration?: WertikConfiguration) => Promise = ( if (configuration?.queue?.jobs) { for (const queueName of Object.keys(configuration?.queue?.jobs || {})) { - wertikApp.queue.jobs[queueName] = await configuration.queue.jobs[ - queueName - ]() + wertikApp.queue.jobs[queueName] = + configuration.queue.jobs[queueName]() } } @@ -145,7 +121,7 @@ const Wertik: (configuration?: WertikConfiguration) => Promise = ( if (configuration.redis) { for (const redisName of Object.keys(configuration.redis || {})) { - wertikApp.redis[redisName] = await configuration.redis[redisName]({ + wertikApp.redis[redisName] = configuration.redis[redisName]({ wertikApp, configuration, }) @@ -156,13 +132,12 @@ const Wertik: (configuration?: WertikConfiguration) => Promise = ( wertikApp.logger = configuration.logger } - applyRelationshipsFromStoreToDatabase(store, wertikApp) - applyRelationshipsFromStoreToGraphql(store, wertikApp) + applyRelationshipsFromStoreToDatabase(wertikApp) expressApp.get("/w/info", function (req, res) { res.json({ message: "You are running wertik-js v3", - version: require("./../../package.json").version, + version: require(`${process.cwd()}/package.json`).version, }) }) @@ -174,7 +149,6 @@ const Wertik: (configuration?: WertikConfiguration) => Promise = ( if (configuration.graphql) { wertikApp.graphql = configuration.graphql({ wertikApp: wertikApp, - store: store, configuration: configuration, expressApp: expressApp, }) @@ -185,17 +159,50 @@ const Wertik: (configuration?: WertikConfiguration) => Promise = ( next() }) + validateModules(wertikApp) + + let startServer = () => { + httpServer.listen(port, () => { + wLogWithSuccess(`[Wertik-App]`, `http://localhost:${port}`) + }) + } + + let stopServer = () => { + wLogWithInfo(`[Wertik-App]`, `Stopping server`) + httpServer.close(() => { + wLogWithSuccess(`[Wertik-App]`, `Server stopped`) + process.exit() + }) + } + + let restartServer = () => { + wLogWithInfo(`[Wertik-App]`, `Restarting server`) + httpServer.close(() => { + setTimeout(() => { + startServer() + }, 500) + }) + } + if (!new Object(process.env).hasOwnProperty("TEST_MODE")) { setTimeout(async () => { - if (skip === false) { - httpServer.listen(port, () => { - console.log(`Wertik JS app listening at http://localhost:${port}`) - }) + if (selfStart === true) { + startServer() } - resolve(wertikApp) + resolve({ + ...wertikApp, + restartServer, + stopServer, + startServer, + }) }, 500) } else { - resolve(wertikApp) + resolve({ + ...wertikApp, + restartServer, + stopServer, + startServer, + }) } } catch (e) { console.error(e) diff --git a/src/logger/consoleMessages.ts b/src/logger/consoleMessages.ts index 53e31ccc..78d0ba14 100644 --- a/src/logger/consoleMessages.ts +++ b/src/logger/consoleMessages.ts @@ -1,9 +1,9 @@ import logSymbols from "log-symbols" import chalk from "chalk" -const log = console.log +import { wLog } from "../utils/log" export const successMessage = function (message, secondMessage?: string) { - log( + wLog( logSymbols.success, ` [Wertik-js]: `, chalk.green(message), @@ -12,9 +12,13 @@ export const successMessage = function (message, secondMessage?: string) { } export const errorMessage = function (message) { - log(logSymbols.error, ` [Wertik-js]:`, chalk.red(message)) + wLog(logSymbols.error, ` [Wertik-js]:`, chalk.red(message)) } -export const warningMessage = function () {} +export const warningMessage = function (message) { + wLog(logSymbols.warning, ` [Wertik-js]:`, chalk.yellow(message)) +} -export const infoMessage = function () {} +export const infoMessage = function (message) { + wLog(logSymbols.info, ` [Wertik-js]:`, chalk.blue(message)) +} diff --git a/src/logger/index.ts b/src/logger/index.ts index 4b7786cc..ba96f88a 100644 --- a/src/logger/index.ts +++ b/src/logger/index.ts @@ -1,19 +1,22 @@ import winston, { LoggerOptions } from "winston" +import { wLogWithSuccess } from "../utils/log" /** * Creates a winston instance * @param props see interface LoggerOptions from winston * @returns winston instance */ -export const useLogger = (options?: LoggerOptions) => { - console.log(`[Logger]`, `Initialized winston logger`) +export const withLogger = (options?: LoggerOptions) => { + wLogWithSuccess(`[Wertik-WinstonLogger]`, `Initialized winston logger`) return winston.createLogger(options) } /** * Allows creating multiple logger instances - * @param fn callback function, useWinstonTransport expects a function and useWinstonTransport runs that function with winston passed so you can return transport instances + * @param fn callback function, withWinstonTransport expects a function and withWinstonTransport runs that function with winston passed so you can return transport instances * @returns should return array of winston transport object. */ -export const useWinstonTransport = (fn = (winstonInstance = winston) => []) => { +export const withWinstonTransport = ( + fn = (winstonInstance = winston) => [] +) => { return fn(winston) } diff --git a/src/mailer/index.ts b/src/mailer/index.ts index 49d5b1af..26ce0973 100644 --- a/src/mailer/index.ts +++ b/src/mailer/index.ts @@ -1,10 +1,10 @@ import nodemailer from "nodemailer" import handlebars from "handlebars" -import { useMailerProps, WertikApp, WertikConfiguration } from "../types" +import { WithMailerProps, WertikApp, WertikConfiguration } from "../types" import { SendEmailProps } from "../types/mailer" -import { get } from "lodash" +import { wLog, wLogWithSuccess } from "../utils/log" -export const useMailer = (props: useMailerProps) => { +export const withMailer = (props: WithMailerProps) => { return async () => { let testAccount = props.options ? null @@ -22,7 +22,7 @@ export const useMailer = (props: useMailerProps) => { }, } - console.log(`[Mailer]`, `Initialized mailer "${props.name}"`) + wLogWithSuccess(`[Wertik-NodeMailer]`, `Initialized mailer "${props.name}"`) return nodemailer.createTransport(emailConfiguration) } @@ -53,14 +53,11 @@ export const emailSender = ({ html: resultTemplate, subject: props.options.subject, }) - if (emailInstance && emailInstance.messageId) { - console.log("Message sent: %s", emailInstance.messageId) + if (emailInstance?.messageId) { + wLog("Message sent: %s", emailInstance.messageId) } - if (nodemailer && nodemailer.getTestMessageUrl) { - console.log( - "Preview URL: %s", - nodemailer.getTestMessageUrl(emailInstance) - ) + if (nodemailer?.getTestMessageUrl) { + wLog("Preview URL: %s", nodemailer.getTestMessageUrl(emailInstance)) } if (configuration.mailer.events.onEmailSent) { @@ -70,16 +67,13 @@ export const emailSender = ({ wertikApp, configuration, emailInstance, - previewURL: - nodemailer && nodemailer.getTestMessageUrl - ? nodemailer.getTestMessageUrl(emailInstance) - : "", + previewURL: nodemailer?.getTestMessageUrl(emailInstance), }) } return emailInstance } catch (e) { - console.log(e) + wLog(e) if (configuration.mailer.events.onEmailSentFailed) { configuration.mailer.events.onEmailSentFailed({ mailer: props.mailer, diff --git a/src/modules/modules.ts b/src/modules/modules.ts index 94831b6b..dcc19c60 100644 --- a/src/modules/modules.ts +++ b/src/modules/modules.ts @@ -1,51 +1,32 @@ -import { get } from "lodash" -import crud from "../crud" +import get from "lodash.get" import { databaseDefaultOptions } from "../utils/defaultOptions" -import { RelationParams, useModuleProps } from "../types/modules" +import { RelationParams, WithModuleProps } from "../types/modules" import { - getCreateSchema, + getInsertSchema, getUpdateSchema, generateEnumTypeForGraphql, + generateGenerateGraphQLCrud, + convertWordIntoPlural, + convertWordIntoSingular, + getOrderSchema, } from "./modulesHelpers" import { getMysqlTableInfo } from "../database/mysql/getTableInfo" import { Store, WertikApp, WertikConfiguration } from "./../types/index" -import { ModelCtor, Model, ModelAttributes } from "sequelize/types" - -const generateGenerateGraphQLCrud = (props, schemaInformation, store) => { - const { graphql } = crud(props, schemaInformation, store) - const resolvers = graphql.generateCrudResolvers() - - store.graphql.typeDefs = store.graphql.typeDefs.concat( - `\n ${schemaInformation.schema} - \n ${schemaInformation.inputSchema.filters} - \n ${schemaInformation.inputSchema.create} - \n ${schemaInformation.inputSchema.update} - ` - ) - - store.graphql.typeDefs = store.graphql.typeDefs.concat( - `\n ${graphql.generateQueriesCrudSchema()}` - ) - store.graphql.typeDefs = store.graphql.typeDefs.concat( - `\n ${graphql.generateMutationsCrudSchema()}` - ) - - store.graphql.resolvers.Query = { - ...store.graphql.resolvers.Query, - ...resolvers.Query, - } - - store.graphql.resolvers.Mutation = { - ...store.graphql.resolvers.Mutation, - ...resolvers.Mutation, - } -} +import { ModelStatic, Model, ModelAttributes } from "sequelize/types" +import { + wLogWithError, + wLogWithInfo, + wLogWithSuccess, + wLogWithWarn, +} from "../utils/log" +import camelize from "../utils/camelize" +import has from "lodash.has" /** * Wertik js module - * @param props see interface useModuleProps + * @param moduleProps */ -export const useModule = (moduleProps: useModuleProps) => { +export const withModule = (moduleProps: WithModuleProps) => { return async ({ store, configuration, @@ -55,8 +36,17 @@ export const useModule = (moduleProps: useModuleProps) => { configuration: WertikConfiguration app: WertikApp }) => { - let tableInstance: ModelCtor> - let graphqlSchema = [] + store.modules.push(moduleProps) + let currentModuleRelationships = [] + let tableInstance: ModelStatic> + let graphqlSchema = [`type ${moduleProps.name} {`] + let listSchema = "" + let filterSchema = [ + `input ${convertWordIntoSingular( + moduleProps.name + )}_filter_input {`, + ] + let orderSchema = "" const useDatabase = get(moduleProps, "useDatabase", false) @@ -67,13 +57,13 @@ export const useModule = (moduleProps: useModuleProps) => { ) } - const useSchema = (string: string) => { + const extendSchema = (string: string) => { store.graphql.typeDefs = store.graphql.typeDefs.concat(` ${string} `) } - const useQuery = ({ query, resolver, name }) => { + const addQuery = ({ query, resolver, name }) => { store.graphql.typeDefs = store.graphql.typeDefs.concat(` extend type Query { ${query} @@ -82,7 +72,7 @@ export const useModule = (moduleProps: useModuleProps) => { store.graphql.resolvers.Query[name] = resolver } - const useMutation = ({ query, resolver, name }) => { + const addMutation = ({ query, resolver, name }) => { store.graphql.typeDefs = store.graphql.typeDefs.concat(` extend type Mutation { ${query} @@ -91,24 +81,127 @@ export const useModule = (moduleProps: useModuleProps) => { store.graphql.resolvers.Mutation[name] = resolver } - const useExpress = (fn = (express) => {}) => { + const getExpress = (fn = (express) => {}) => { setTimeout(() => { fn(app.express) }, 2500) } - let listSchema = "" - let filterSchema = [] + const hasOne = (params: RelationParams) => { + graphqlSchema.push(`${params.graphqlKey}: ${params.module}Module`) + let relationshipInfo = { + currentModule: moduleProps.name, + currentModuleDatabase: moduleProps.database, + graphqlKey: params.graphqlKey, + referencedModule: params.module, + referencedModuleDatabase: params.database, + options: params.options, + type: "hasOne", + } + store.database.relationships.push(relationshipInfo) + currentModuleRelationships.push(relationshipInfo) + store.graphql.graphqlKeys.push(camelize(params.module)) + filterSchema.push( + `${camelize(params.graphqlKey)}: ${convertWordIntoSingular( + params.module + )}_filter_input` + ) + } + const belongsTo = (params: RelationParams) => { + graphqlSchema.push(`${params.graphqlKey}: ${params.module}Module`) + let relationshipInfo = { + currentModule: moduleProps.name, + currentModuleDatabase: moduleProps.database, + graphqlKey: params.graphqlKey, + referencedModule: params.module, + referencedModuleDatabase: params.database, + options: params.options, + type: "belongsTo", + } + store.database.relationships.push(relationshipInfo) + currentModuleRelationships.push(relationshipInfo) + store.graphql.graphqlKeys.push(camelize(params.module)) + filterSchema.push( + `${camelize(params.graphqlKey)}: ${convertWordIntoSingular( + params.module + )}_filter_input` + ) + } + const belongsToMany = (params: RelationParams) => { + let field_name = convertWordIntoSingular(params.module) + graphqlSchema.push( + `${params.graphqlKey}(offset: Int, limit: Int, where: ${field_name}_filter_input, order: ${field_name}_order_input): [${params.module}Module]` + ) + let relationshipInfo = { + currentModule: moduleProps.name, + currentModuleDatabase: moduleProps.database, + graphqlKey: params.graphqlKey, + referencedModule: params.module, + referencedModuleDatabase: params.database, + options: params.options, + type: "belongsToMany", + } + store.database.relationships.push(relationshipInfo) + currentModuleRelationships.push(relationshipInfo) + store.graphql.graphqlKeys.push(camelize(params.module)) + filterSchema.push( + `${camelize(params.graphqlKey)}: ${convertWordIntoSingular( + params.module + )}_filter_input` + ) + } + const hasMany = (params: RelationParams) => { + let field_name = convertWordIntoSingular(params.module) + graphqlSchema.push( + `${params.graphqlKey}(offset: Int, limit: Int, where: ${field_name}_filter_input, order: ${field_name}_order_input): [${params.module}Module]` + ) + let relationshipInfo = { + currentModule: moduleProps.name, + currentModuleDatabase: moduleProps.database, + graphqlKey: params.graphqlKey, + referencedModule: params.module, + referencedModuleDatabase: params.database, + options: params.options, + type: "hasMany", + } + currentModuleRelationships.push(relationshipInfo) + store.database.relationships.push(relationshipInfo) + store.graphql.graphqlKeys.push(camelize(params.module)) + filterSchema.push( + `${camelize(params.graphqlKey)}: ${convertWordIntoSingular( + params.module + )}_filter_input` + ) + } + get(moduleProps, "on", () => {})({ + addQuery, + addMutation, + getExpress, + hasOne, + belongsTo, + belongsToMany, + hasMany, + extendSchema, + }) + + let insertSchema = [] + let updateSchema = [] + if (useDatabase) { - var createSchema = [] - var updateSchema = [] + if (!has(app.database, moduleProps.database)) { + wLogWithError( + `Unknown database: ${moduleProps.database}`, + `Unknown database mentioned in module ${moduleProps.name}` + ) + process.exit() + } + const connection = app.database[moduleProps.database] // info const tableInfo = await getMysqlTableInfo( - moduleProps, + moduleProps.table, connection.instance ) - // console.log(tableInfo) let fields: ModelAttributes, any> = {} @@ -138,9 +231,6 @@ export const useModule = (moduleProps: useModuleProps) => { if (moduleProps?.graphql?.schema) { graphqlSchema = moduleProps.graphql.schema.replace("}", "").split("\n") } else { - // graphql schema - graphqlSchema = [`type ${moduleProps.name} {`] - tableInfo.columns.forEach((columnInfo) => { if (columnInfo.isEnum) { store.graphql.typeDefs = store.graphql.typeDefs.concat( @@ -155,111 +245,68 @@ export const useModule = (moduleProps: useModuleProps) => { updateSchema = getUpdateSchema(moduleProps, tableInfo) - createSchema = getCreateSchema(moduleProps, tableInfo) + insertSchema = getInsertSchema(moduleProps, tableInfo) - filterSchema = [`input ${moduleProps.name}FilterInput {`] + orderSchema = getOrderSchema(moduleProps, tableInfo) tableInfo.columns.forEach((column) => { - let filterInput = + let filter_input = column.databaseType.toLowerCase() === "enum" ? `${column.columnName}: ${column.graphqlType}` - : `${column.columnName}: ${column.graphqlType}FilterInput` + : `${ + column.columnName + }: ${column.graphqlType.toLowerCase()}_filter_input` - filterSchema.push(filterInput) + filterSchema.push(filter_input) }) listSchema = ` query List${moduleProps.name} { - list: [${moduleProps.name}] + rows: [${moduleProps.name}] paginationProperties: PaginationProperties filters: ${moduleProps.name}Filters } ` - } - const hasOne = (params: RelationParams) => { - graphqlSchema.push(`${params.graphqlKey}: ${params.module}`) - store.database.relationships.push({ - currentModule: moduleProps.name, - currentModuleDatabase: moduleProps.database, - graphqlKey: params.graphqlKey, - referencedModule: params.module, - referencedModuleDatabase: params.database, - options: params.options, - type: "hasOne", - }) - } - const belongsTo = (params: RelationParams) => { - graphqlSchema.push(`${params.graphqlKey}: ${params.module}`) - store.database.relationships.push({ - currentModule: moduleProps.name, - currentModuleDatabase: moduleProps.database, - graphqlKey: params.graphqlKey, - referencedModule: params.module, - referencedModuleDatabase: params.database, - options: params.options, - type: "belongsTo", - }) - } - const belongsToMany = (params: RelationParams) => { - graphqlSchema.push(`${params.graphqlKey}: ${params.module}List`) - store.database.relationships.push({ - currentModule: moduleProps.name, - currentModuleDatabase: moduleProps.database, - graphqlKey: params.graphqlKey, - referencedModule: params.module, - referencedModuleDatabase: params.database, - options: params.options, - type: "belongsToMany", - }) - } - const hasMany = (params: RelationParams) => { - graphqlSchema.push(`${params.graphqlKey}: ${params.module}List`) - store.database.relationships.push({ - currentModule: moduleProps.name, - currentModuleDatabase: moduleProps.database, - graphqlKey: params.graphqlKey, - referencedModule: params.module, - referencedModuleDatabase: params.database, - options: params.options, - type: "hasMany", - }) - } - - get(moduleProps, "on", () => {})({ - useQuery, - useMutation, - useExpress, - hasOne, - belongsTo, - belongsToMany, - hasMany, - useSchema, - }) - - if (useDatabase) { graphqlSchema.push("}") filterSchema.push("}") } const schemaInformation = { + moduleName: moduleProps.name, tableInstance: tableInstance, schema: graphqlSchema.join(`\n`), + props: moduleProps, inputSchema: { - create: createSchema || "", + insert: insertSchema || "", update: updateSchema || "", list: listSchema, filters: filterSchema.join("\n"), + order_schema: orderSchema || "", }, } if (useDatabase) { - generateGenerateGraphQLCrud(moduleProps, schemaInformation, store) + generateGenerateGraphQLCrud(moduleProps, schemaInformation) app.models[moduleProps.name] = tableInstance } - console.log(`[Module]`, `Initialized module "${moduleProps.name}"`) + wLogWithInfo(`[Wertik-Module]`, `Initialized module "${moduleProps.name}"`) return schemaInformation } } + +export function validateModules(wertikApp: WertikApp) { + wLogWithInfo("Validating:", "Modules") + Object.keys(wertikApp.modules).forEach((name) => { + let module = wertikApp.modules[name] + if (name !== module.props.name) { + wLogWithError( + "[MODULE NAME CONFLICT]", + "Please use same name for both key and module name" + ) + process.exit() + } + }) +} diff --git a/src/modules/modulesHelpers.ts b/src/modules/modulesHelpers.ts index 324475f9..44f6ee68 100644 --- a/src/modules/modulesHelpers.ts +++ b/src/modules/modulesHelpers.ts @@ -1,7 +1,13 @@ -import { get } from "lodash" -import { useModuleProps } from "../types/modules" -import { TableInfo } from "../types/database" +import get from "lodash.get" +import { WithModuleProps } from "../types/modules" +import { SqlTable, TableInfo } from "../types/database" import { capitalizeFirstLetter } from "../utils/capitalizeFirstLetter" +import crud from "../crud" +import { wertikApp } from "../store" +import pluralize from "pluralize" +import snackCase from "lodash.snakecase" +import isPlainObject from "lodash.isplainobject" +import { print } from "util" export const generateDataTypeFromDescribeTableColumnType = (Type: string) => { let length = Type.match(/[0-9]/g)?.join("") @@ -29,7 +35,7 @@ export const getGraphQLTypeNameFromSqlType = ( }, module ) => { - var type = column.Type + let type = column.Type if (typeof column.Type === "string") { type = type.toLowerCase() } else { @@ -59,12 +65,12 @@ export const getGraphQLTypeNameFromSqlType = ( } export const getUpdateSchema = ( - module: useModuleProps, + table: SqlTable, tableInfo: TableInfo ) => { - const optionsUpdateSchema = get(module, "graphql.updateSchema", "") + const optionsUpdateSchema = get(table, "graphql.updateSchema", "") if (optionsUpdateSchema) return optionsUpdateSchema - let updateSchema = [`input update${module.name}Input {`] + let updateSchema = [`input update_${table.name}_input {`] tableInfo.columns.forEach((column) => { if (column.columnName !== "id" && !column.isDateColumn) { updateSchema.push( @@ -77,23 +83,40 @@ export const getUpdateSchema = ( return updateSchema.join("\n") } -export const getCreateSchema = ( - module: useModuleProps, +export const getInsertSchema = ( + table: SqlTable, tableInfo: TableInfo ) => { - const optionsCreateSchema = get(module, "graphql.createSchema", "") - if (optionsCreateSchema) return optionsCreateSchema - let createSchema = [`input create${module.name}Input {`] + const optionsInsertSchema = get(table, "graphql.createSchema", "") + const rowsFieldName = convertWordIntoPlural(table.name) + if (optionsInsertSchema) return optionsInsertSchema + let insertSchema = [`input insert_${rowsFieldName}_input {`] tableInfo.columns.forEach((column) => { if (column.columnName !== "id" && !column.isDateColumn) { - createSchema.push( - `${column.columnName}: ${column.graphqlCreateInputType}` + insertSchema.push( + `${column.columnName}: ${column.graphqlInsertInputType}` ) } }) - createSchema.push("}") + insertSchema.push("}") - return createSchema.join("\n") + return insertSchema.join("\n") +} + +export const getOrderSchema = (table: SqlTable, tableInfo) => { + let orderSchema = [ + `input ${convertWordIntoSingular(table.name)}_order_input {`, + ] + let relationships = wertikApp.store.database.relationships.filter( + (c) => c.currentModule === table.name + ) + tableInfo.columns.forEach((column) => { + orderSchema.push(`${column.columnName}: order_by`) + }) + + orderSchema.push("}") + + return orderSchema.join("\n") } export const generateEnumTypeForGraphql = (column: TableInfo["columns"][0]) => { @@ -101,3 +124,73 @@ export const generateEnumTypeForGraphql = (column: TableInfo["columns"][0]) => { ${column.enumValues.join("\n")} }` } + +export const generateGenerateGraphQLCrud = ( + props, + schemaInformation +) => { + const { graphql } = crud(props, schemaInformation, wertikApp.store) + const resolvers = graphql.generateCrudResolvers() + + wertikApp.store.graphql.typeDefs = wertikApp.store.graphql.typeDefs.concat( + `\n ${schemaInformation.schema} + \n ${schemaInformation.inputSchema.filters} + \n ${schemaInformation.inputSchema.insert} + \n ${schemaInformation.inputSchema.update} + \n ${schemaInformation.inputSchema.order_schema} + ` + ) + + wertikApp.store.graphql.typeDefs = wertikApp.store.graphql.typeDefs.concat( + `\n ${graphql.generateQueriesCrudSchema()}` + ) + wertikApp.store.graphql.typeDefs = wertikApp.store.graphql.typeDefs.concat( + `\n ${graphql.generateMutationsCrudSchema()}` + ) + + wertikApp.store.graphql.resolvers.Query = { + ...wertikApp.store.graphql.resolvers.Query, + ...resolvers.Query, + } + + wertikApp.store.graphql.resolvers.Mutation = { + ...wertikApp.store.graphql.resolvers.Mutation, + ...resolvers.Mutation, + } +} + +/** + * Extract relational fields that were requested in a GraphQL query. + */ +export const getRelationalFieldsRequestedInQuery = ( + module, + requestedFields +) => { + const fields = Object.keys(requestedFields) + // Filter all relationships for provided modules, based on fields provided filter out those relationships. + const relationalFields = wertikApp.store.database.relationships + .filter((c) => c.currentModule === module.name) + .filter((relationship) => fields.includes(relationship.graphqlKey)) + return relationalFields +} + +export const generateRequestedFieldsFromGraphqlInfo = (tableName, info) => { + const keysToIgnore = [ + ...Object.keys(wertikApp.models[tableName].associations), + "__typename", + "__arguments", + ] + + return Object.keys(info).filter((c) => !keysToIgnore.includes(c)) +} + +export const convertWordIntoSingular = (moduleName) => { + return snackCase(pluralize.singular(moduleName)).toLowerCase() +} +export const convertWordIntoPlural = (moduleName) => { + let fieldName = snackCase(pluralize.plural(moduleName)).toLowerCase() + if (convertWordIntoSingular(moduleName) == fieldName) { + return fieldName + "s" + } + return fieldName +} diff --git a/src/queue/index.ts b/src/queue/index.ts index 27040778..ee9208b3 100644 --- a/src/queue/index.ts +++ b/src/queue/index.ts @@ -1,7 +1,8 @@ import Queue from "bull" import { isPackageInstalled } from "../utils/checkInstalledPackages" import { WertikApp, WertikConfiguration } from "../types" -import { useQueueProps } from "./../types/queue" +import { WithQueueProps } from "./../types/queue" +import { wLog } from "../utils/log" /** * @param name @@ -10,7 +11,7 @@ import { useQueueProps } from "./../types/queue" * @returns Queue */ -export const useQueue = (props: useQueueProps) => { +export const withQueue = (props: WithQueueProps) => { return () => new Queue(props.name, props.url, props.options) } @@ -18,7 +19,7 @@ export const initializeBullBoard = (props: { wertikApp: WertikApp configuration: WertikConfiguration }) => { - var isInstalledBullBoardExpress = isPackageInstalled("@bull-board/express") + let isInstalledBullBoardExpress = isPackageInstalled("@bull-board/express") if (!isInstalledBullBoardExpress) { throw new Error( "Please install package @bull-board/express to initialize Bull Board for Queue" @@ -51,7 +52,7 @@ export const initializeBullBoard = (props: { serverAdapter.setBasePath(queuePath) express.use(queuePath, serverAdapter.getRouter()) - console.log( + wLog( `Queue UI Monitoring Bull Board running at: http://localhost:${props.configuration.port}${queuePath}` ) diff --git a/src/redis/index.ts b/src/redis/index.ts index 1681a089..2be7dd11 100644 --- a/src/redis/index.ts +++ b/src/redis/index.ts @@ -1,7 +1,8 @@ import { createClient } from "redis" -import { useRedisProps, WertikApp, WertikConfiguration } from "../types" +import { wLog, wLogWithSuccess } from "../utils/log" +import { WithRedisProps, WertikApp, WertikConfiguration } from "../types" -export const useRedis = (props?: useRedisProps) => { +export const withRedis = (props?: WithRedisProps) => { return async ({ configuration, wertikApp, @@ -11,10 +12,8 @@ export const useRedis = (props?: useRedisProps) => { }) => { const client = createClient(props) await client.connect() - client.on("error", (err) => - console.log(`Redis Client ${props.name} Error `, err) - ) - console.log(`[Redis]`, `Initialized redis "${props.name}"`) + client.on("error", (err) => wLog(`Redis Client ${props.name} Error `, err)) + wLogWithSuccess(`[Wertik-Redis]`, `Initialized redis "${props.name}"`) return client } } diff --git a/src/sockets/index.ts b/src/sockets/index.ts index 4a6982bf..4adcceca 100644 --- a/src/sockets/index.ts +++ b/src/sockets/index.ts @@ -2,6 +2,7 @@ import { Server as SocketIOServer, ServerOptions as SocketIOServerOptions, } from "socket.io" +import { wLogWithSuccess } from "../utils/log" import { Server as WebSocketServer, ServerOptions as WebSocketServerOptions, @@ -13,7 +14,7 @@ import { WertikApp, WertikConfiguration } from "../types" * @param props see interface WebSocketServerOptions * @returns WebSocketServer instance */ -export const useWebSockets = (props: WebSocketServerOptions = {}) => { +export const withWebSockets = (props: WebSocketServerOptions = {}) => { return ({ configuration, wertikApp, @@ -24,8 +25,9 @@ export const useWebSockets = (props: WebSocketServerOptions = {}) => { if (!props.path) { throw new Error("Path must be passed for useWebSockets") } - console.log( - `Web Sockets server starting at ws://localhost:${configuration.port}${props.path}` + wLogWithSuccess( + `[Wertik-WebSockets]`, + `ws://localhost:${configuration.port}${props.path}` ) return new WebSocketServer({ server: wertikApp.httpServer, @@ -39,7 +41,7 @@ export const useWebSockets = (props: WebSocketServerOptions = {}) => { * @param props see interface WebSocketServerOptions * @returns WebSocketServer instance */ -export const useIndependentWebSocketsServer = ( +export const withIndependentWebSocketsServer = ( props: WebSocketServerOptions = {} ) => { return ({ @@ -49,10 +51,9 @@ export const useIndependentWebSocketsServer = ( configuration: WertikConfiguration wertikApp: WertikApp }) => { - console.log( - `Web Sockets server starting at ws://localhost:${props.port}/${ - props.path ?? "" - }` + wLogWithSuccess( + "[Wertik-WebSocket]", + `ws://localhost:${props.port}/${props.path ?? ""}` ) return new WebSocketServer({ ...props, @@ -65,7 +66,7 @@ export const useIndependentWebSocketsServer = ( * @param props see interface SocketIOServerOptions from socket.io * @returns SocketIOServer */ -export const useSocketIO = (props: any = {}) => { +export const withSocketIO = (props: any = {}) => { return ({ configuration, wertikApp, @@ -73,10 +74,9 @@ export const useSocketIO = (props: any = {}) => { configuration: WertikConfiguration wertikApp: WertikApp }) => { - console.log( - `Socket.IO server starting at http://localhost:${configuration.port}${ - props.path ?? "/socket.io" - }` + wLogWithSuccess( + `[Wertik-Socket.IO]`, + `http://localhost:${configuration.port}${props.path ?? "/socket.io"}` ) return new SocketIOServer(wertikApp.httpServer, props ?? {}) } diff --git a/src/storage/index.ts b/src/storage/index.ts index a72399c2..437dc102 100644 --- a/src/storage/index.ts +++ b/src/storage/index.ts @@ -1,10 +1,11 @@ import { WertikApp, WertikConfiguration } from "../types" -import { useStorageProps } from "../types/storage" +import { WithStorageProps } from "../types/storage" +import { wLog } from "../utils/log" const DIGITAL_OCEAN = "digitalocean" const DROPBOX = "dropbox" -export const useStorage = (storageItem: useStorageProps) => { +export const withStorage = (storageItem: WithStorageProps) => { return ({ configuration, wertikApp, @@ -30,9 +31,7 @@ export const useStorage = (storageItem: useStorageProps) => { endpoint: spacesEndpoint, }) - console.log( - `[Storage] Initialized Digital Ocean instance ${storageItem.name}` - ) + wLog(`[Storage] Initialized Digital Ocean instance ${storageItem.name}`) return { spacesEndpoint, @@ -45,7 +44,7 @@ export const useStorage = (storageItem: useStorageProps) => { accessToken: dropboxOptions.accessToken, }) - console.log(`[Storage] Initialized Dropbox instance ${storageItem.name}`) + wLog(`[Storage] Initialized Dropbox instance ${storageItem.name}`) return { dropbox: dropboxInstance, diff --git a/src/store.ts b/src/store.ts index ef89cd2b..c9a43bf7 100644 --- a/src/store.ts +++ b/src/store.ts @@ -1,36 +1,74 @@ import generalSchema from "./graphql/generalSchema" +import { WertikApp } from "./types" +import { WithModuleProps } from "./types/modules" -const store = { - graphql: { - typeDefs: ` - ${generalSchema} - type Response { - message: String - version: String - } - type Query { - version: String - } - type Mutation { - version: String - } - schema { - query: Query - mutation: Mutation - } - `, - resolvers: { - Query: { - version: () => require("../../package.json").version, - }, - Mutation: { - version: () => require("../../package.json").version, +type StoreDatabaseRelationship = { + currentModule: string + currentModuleDatabase: string + graphqlKey: string + referencedModule: string + referencedModuleDatabase: string + options: { + [key: string]: unknown + } +} + +/** + * @description This is the store of the app. It contains all the data that is required by the app to run. + */ +export const wertikApp: WertikApp = { + restartServer: () => {}, + stopServer: () => {}, + startServer: () => {}, + appEnv: "local", + port: 1200, + modules: {}, + models: {}, + database: {}, + mailer: {}, + graphql: null, + sockets: {}, + cronJobs: {}, + storage: {}, + queue: { + jobs: {}, + bullBoard: {}, + }, + redis: {}, + logger: null, + store: { + graphql: { + graphqlKeys: [], + typeDefs: ` + ${generalSchema} + type Response { + message: String + version: String + } + type Query { + version: String + } + type Mutation { + version: String + } + schema { + query: Query + mutation: Mutation + } + `, + resolvers: { + Query: { + version: () => require("../../package.json").version, + }, + Mutation: { + version: () => require("../../package.json").version, + }, }, }, - }, - database: { - relationships: [], - }, + database: { + relationships: [], + models: {}, + }, + modules: [], + } } - -export default store diff --git a/src/types/cronJobs.ts b/src/types/cronJobs.ts index 090d968f..1099be9a 100644 --- a/src/types/cronJobs.ts +++ b/src/types/cronJobs.ts @@ -1,9 +1,9 @@ import { WertikApp } from "." -export interface useCronJobsProps { +export interface UseCronJobsProps { expression: string name: string - beforeRun?: (app: WertikApp) => void | any - afterRun?: (app: WertikApp) => void | any - handler: (app: WertikApp) => void | any + beforeRun?: (app: WertikApp) => void + afterRun?: (app: WertikApp) => void + handler: (app: WertikApp) => void } diff --git a/src/types/database.ts b/src/types/database.ts index 365a6345..f6874304 100644 --- a/src/types/database.ts +++ b/src/types/database.ts @@ -1,6 +1,61 @@ import { iObject } from "." +import { AddMutationProps, AddQueryProps, RelationParams } from "./modules" -export interface useMysqlDatabaseProps { +export interface SqlTable { + name: string, + /** + * Sequelize Table Options + */ + tableOptions?: iObject + /** + * Provide set of fields to extend a table, mostly can be used to update createdAt and updatedAt columns. + */ + extendFields?: iObject + /** + * Graphql options for this module. + */ + graphql?: { + /** + * Wertik-js creates schema by default from the database table. Once you defined this Wertik-js will ignore taking schema from the database. + */ + schema?: string + /** + * Wertik-js creates an update schema from the database table. Once defined, Wertik JS will ignore creating an update schema from table information. + */ + updateSchema?: string + /** + * Wertik-js creates create a schema from the database table. Once defined this, Wertik JS will ignore creating create a schema from the table information. + */ + insertSchema: string + }, + relationships?: { + hasOne?: { + [tableName: string]: { + as: string, + [key: string]: any + } + }, + hasMany?: { + [tableName: string]: { + as: string, + [key: string]: any + } + }, + belongsTo?: { + [tableName: string]: { + as: string, + [key: string]: any + } + }, + belongsToMany?: { + [tableName: string]: { + as: string, + [key: string]: any + } + } + } +} +export interface WithMysqlDatabaseProps { /** * Database name */ @@ -24,7 +79,11 @@ export interface useMysqlDatabaseProps { /** * Sequelize Database options. */ - options?: iObject + options?: iObject, + /** + * Tables + */ + tables?: SqlTable[]; } export interface TableInfo { @@ -37,7 +96,7 @@ export interface TableInfo { default: string | number extra: any graphqlType: string - graphqlCreateInputType: string + graphqlInsertInputType: string graphqlUpdateInputType: string enumValues: string[] | null isEnum: boolean diff --git a/src/types/graphql.ts b/src/types/graphql.ts index 71ce4bbe..3c677d7d 100644 --- a/src/types/graphql.ts +++ b/src/types/graphql.ts @@ -5,7 +5,7 @@ export interface GetMiddlewareOptionsGraphql extends GetMiddlewareOptions { path: string } -export interface useGraphqlProps { +export interface WithApolloGraphqlProps { options?: { [key: string]: any } @@ -15,6 +15,10 @@ export interface useGraphqlProps { Query: {} } typeDefs?: string + storeTypeDefFilePath?: string + validation?: { + depthLimit?: number + } } export interface GraphqlInitializeProps { diff --git a/src/types/index.ts b/src/types/index.ts index e51bc069..47298db1 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,13 +1,15 @@ import { Sequelize } from "sequelize/types" -import { useMysqlDatabaseProps } from "./database" +import { WithMysqlDatabaseProps } from "./database" import { SendEmailProps } from "./mailer" -import { WertikModule } from "./modules" +import { WertikModule, WithModuleProps } from "./modules" +import { ApolloServer } from "apollo-server-express" export type iObject = { [key: string]: any } export interface Store { graphql: { - typeDefs: String + graphqlKeys: string[] + typeDefs: string resolvers: { Query: { [key: string]: Function @@ -20,6 +22,7 @@ export interface Store { database: { relationships: Array } + modules: WithModuleProps[] } export interface WertikConfiguration { @@ -42,23 +45,33 @@ export interface WertikConfiguration { httpServer?: iObject /** * [Optional] When passed as true, Wertik will not start server. + * + * @deprecated Use `selfStart` instead. + * @default true */ skip?: boolean + + /** + * When passed as true, Wertik will not start server. + * @default true + */ + selfStart?: boolean /** * Database connections */ database?: { [key: string]: () => Promise<{ - credentials: useMysqlDatabaseProps - instance: Sequelize + credentials: WithMysqlDatabaseProps + instance: Sequelize, + models: WertikModule["tableInstance"][] }> } /** * Modules + * @deprecated Use `tables` on database connections. */ modules?: { [key: string]: (options: { - store: Store configuration: WertikConfiguration app: WertikApp }) => Promise @@ -85,7 +98,7 @@ export interface WertikConfiguration { } events?: { /** - * Runs when email sents successfully. + * Runs when email sent successfully. */ onEmailSent?: (options: { options: iObject @@ -93,18 +106,18 @@ export interface WertikConfiguration { configuration: WertikConfiguration emailInstance: any previewURL: string | boolean - mailer: String - }) => void | any | null | undefined + mailer: string + }) => void /** * Runs when email fails to send. */ onEmailSentFailed?: (options: { - mailer: String + mailer: string wertikApp: WertikApp configuration: WertikConfiguration error: any options: iObject - }) => void | any | null | undefined + }) => void } } /** @@ -120,11 +133,10 @@ export interface WertikConfiguration { * Graphql */ graphql?: (options: { - store: Store configuration: WertikConfiguration wertikApp: WertikApp expressApp: any - }) => iObject + }) => ApolloServer /** * Cron Jobs */ @@ -169,10 +181,37 @@ export interface WertikConfiguration { export interface WertikApp { appEnv: "production" | "development" | "local" sendEmail?: (options: { mailer: string; options: SendEmailProps }) => iObject + restartServer: () => void + stopServer: () => void + startServer: () => void port: number modules: { [key: string]: WertikModule } + store: { + graphql: { + graphqlKeys: string[] + typeDefs: string + resolvers: { + Query: { + [key: string]: Function + } + Mutation: { + [key: string]: Function + } + [key: string]: { + [key: string]: Function | string | number | boolean | object | any + } + } + } + database: { + relationships: any[] + models: { + [key: string]: any + } + } + modules: WithModuleProps[] + } database: { [key: string]: { credentials: { @@ -180,16 +219,18 @@ export interface WertikApp { name: string password: string username: string - host: string + host: string, + tables: WithMysqlDatabaseProps['tables'] } - instance: Sequelize + instance: Sequelize, + models: WertikModule["tableInstance"][], } } models: { [key: string]: WertikModule["tableInstance"] } mailer: iObject - graphql: iObject + graphql: ApolloServer sockets: iObject cronJobs: iObject storage: iObject @@ -203,18 +244,18 @@ export interface WertikApp { /** * Provide same options that redis createClient method requires. */ -export interface useRedisProps { +export interface WithRedisProps { [key: string]: any name: string } -export interface useMailerProps { +export interface WithMailerProps { /** * Provide name for your mailer. */ name: string /** - * Provide options that you provide procide to nodemailer.createTransport function. + * Provide options that you provide provide to nodemailer.createTransport function. */ options?: { [key: string]: any diff --git a/src/types/mailer.ts b/src/types/mailer.ts index 337c435f..55c6d401 100644 --- a/src/types/mailer.ts +++ b/src/types/mailer.ts @@ -9,4 +9,4 @@ export interface SendEmailProps { [key: string]: any } -export interface userMailerProps {} +export interface UserMailerProps {} diff --git a/src/types/modules.ts b/src/types/modules.ts index 7040f55b..f80d1671 100644 --- a/src/types/modules.ts +++ b/src/types/modules.ts @@ -1,7 +1,7 @@ import { iObject } from "." -import { ModelCtor, Model } from "sequelize/types" +import { ModelStatic, Model } from "sequelize/types" -export interface useQueryProps { +export interface AddQueryProps { /** * Schema for this query, for example: getUsers: [Users] */ @@ -15,7 +15,7 @@ export interface useQueryProps { */ name: string } -export interface useMutationProps { +export interface AddMutationProps { /** * Schema for this query, for example: deleteUsers: Boolean */ @@ -29,7 +29,7 @@ export interface useMutationProps { */ name: string } -export type useExpressProps = Function +export type GetExpressProps = Function export interface RelationParams { module: string graphqlKey: string @@ -38,15 +38,14 @@ export interface RelationParams { [key: string]: string | number | null } } -export type useSchemaProps = string -export interface useModuleProps { +export interface WithModuleProps { /** * Your module name. */ name: string /** - * Are you using a database connection to this module? If yes, you will need to provide a database and table. + * If your module requires a database connection, you will need to provide a database and table using the 'useDatabase' property. This property should be set to the name of the database and table that your module will use to perform its operations. */ useDatabase: boolean /** @@ -83,7 +82,7 @@ export interface useModuleProps { /** * Wertik-js creates create a schema from the database table. Once defined this, Wertik JS will ignore creating create a schema from the table information. */ - createSchema: string + insertSchema: string mutations?: { /** * Overrides default behavior of updating a record from the database table. @@ -96,11 +95,11 @@ export interface useModuleProps { /** * Overrides default behavior of create a record from the database table. */ - create?: Function + insert?: Function /** * Overrides default behavior of creating or updating a record from the database table. */ - createOrUpdate?: Function + insertOrUpdate?: Function } } /** @@ -110,15 +109,15 @@ export interface useModuleProps { /** * This Method allows you adding graphql query to your module. */ - useQuery: (props: useQueryProps) => {} | void + addQuery: (props: AddQueryProps) => {} | void /** * This Method allows you adding graphql mutation to your module. */ - useMutation: (props: useMutationProps) => {} | void + addMutation: (props: AddMutationProps) => {} | void /** * This method gives you access to express app instance. */ - useExpress: (express: any) => void + getExpress: (express: any) => void /** * This method adds a one-to-one relationship to a module. */ @@ -132,16 +131,16 @@ export interface useModuleProps { */ belongsToMany: (props: RelationParams) => {} | void /** - * This method adds belogs to many relationship to a module. + * This method adds belongs to many relationship to a module. */ hasMany: (props: RelationParams) => {} | void /** * This method adds has many relationship to a module. */ - useSchema: (props: useSchemaProps) => {} | void + extendSchema: (props: string) => {} | void }) => void /** - * Graphql events when a CRUD opreation happens. + * Graphql events when a CRUD operation happens. */ events?: { /** @@ -159,7 +158,7 @@ export interface useModuleProps { /** * This events runs before beforeCreate mutation. */ - beforeCreate?: Function + beforeInsert?: Function /** * This events runs before beforeDelete mutation. */ @@ -169,19 +168,20 @@ export interface useModuleProps { */ beforeUpdate?: Function /** - * This events runs before beforeCreateOrUpdate mutation. + * This events runs before beforeInsertOrUpdate mutation. */ - beforeCreateOrUpdate?: Function + beforeInsertOrUpdate?: Function } } export interface WertikModule { - tableInstance: ModelCtor> + tableInstance: ModelStatic> schema: string inputSchema: { - create: string | any[] + insert: string | any[] update: string | any[] list: string filters: string } + props: WithModuleProps } diff --git a/src/types/queue.ts b/src/types/queue.ts index 3511f7f2..f034e402 100644 --- a/src/types/queue.ts +++ b/src/types/queue.ts @@ -1,6 +1,6 @@ import { QueueOptions } from "bull" -export interface useQueueProps { +export interface WithQueueProps { name: string url?: string options?: QueueOptions diff --git a/src/types/storage.ts b/src/types/storage.ts index 983df9b6..16681c43 100644 --- a/src/types/storage.ts +++ b/src/types/storage.ts @@ -1,4 +1,4 @@ -export interface useStorageProps { +export interface WithStorageProps { for: "dropbox" | "digitalocean" name: string dropboxOptions?: { diff --git a/src/utils/camelize.ts b/src/utils/camelize.ts new file mode 100644 index 00000000..ad8548ba --- /dev/null +++ b/src/utils/camelize.ts @@ -0,0 +1,9 @@ +function camelize(str) { + return str + .replace(/(?:^\w|[A-Z]|\b\w)/g, function (word, index) { + return index === 0 ? word.toLowerCase() : word.toUpperCase() + }) + .replace(/\s+/g, "") +} + +export default camelize diff --git a/src/utils/checkInstalledPackages.ts b/src/utils/checkInstalledPackages.ts index 29273e18..62f27b7f 100644 --- a/src/utils/checkInstalledPackages.ts +++ b/src/utils/checkInstalledPackages.ts @@ -15,7 +15,7 @@ export function isPackageInstalled(packageName) { } } -export function check(name: String) { +export function check(name: string) { const isInstalled = isPackageInstalled(name) if (isInstalled) { return true diff --git a/src/utils/convertFiltersIntoSequalizeObject.ts b/src/utils/convertFiltersIntoSequalizeObject.ts deleted file mode 100644 index a2f9c502..00000000 --- a/src/utils/convertFiltersIntoSequalizeObject.ts +++ /dev/null @@ -1,7 +0,0 @@ -import replaceFilterOperators from "./replaceFilterOperators" - -const convertFiltersIntoSequalizeObject = (filters) => { - return filters ? replaceFilterOperators(filters) : {} -} - -export default async (filters) => convertFiltersIntoSequalizeObject(filters) diff --git a/src/utils/convertFiltersIntoSequelizeObject.ts b/src/utils/convertFiltersIntoSequelizeObject.ts new file mode 100644 index 00000000..f9de4df3 --- /dev/null +++ b/src/utils/convertFiltersIntoSequelizeObject.ts @@ -0,0 +1,7 @@ +import replaceFilterOperators from "./replaceFilterOperators" + +const convertFiltersIntoSequelizeObject = (filters) => { + return filters ? replaceFilterOperators(filters) : {} +} + +export default (filters) => convertFiltersIntoSequelizeObject(filters) diff --git a/src/utils/dayjs.ts b/src/utils/dayjs.ts new file mode 100644 index 00000000..e73ff4f4 --- /dev/null +++ b/src/utils/dayjs.ts @@ -0,0 +1,8 @@ +import dayjs from "dayjs" +import localizedFormat from "dayjs/plugin/localizedFormat" +import timezone from "dayjs/plugin/timezone" + +dayjs.extend(localizedFormat) +dayjs.extend(timezone) + +export default dayjs diff --git a/src/utils/defaultOptions.ts b/src/utils/defaultOptions.ts index 607aa323..7f5d8be2 100644 --- a/src/utils/defaultOptions.ts +++ b/src/utils/defaultOptions.ts @@ -12,7 +12,7 @@ export const databaseDefaultOptions = { logging: false, operatorsAliases: "0", underscored: false, - freetableName: true, + freezeTableName: true, }, defaultTableOptions: { timestamps: false, diff --git a/src/utils/log.ts b/src/utils/log.ts new file mode 100644 index 00000000..f3d064de --- /dev/null +++ b/src/utils/log.ts @@ -0,0 +1,29 @@ +import dayjs from "../utils/dayjs" +import chalk from "chalk" + +export const wLog = console.log +export const wLogWithDateWithInfo = (info, ...params) => { + let day = dayjs() + console.log( + day.format("L-LT"), + day.format("Z"), + chalk.blueBright(info), + ...params + ) +} + +export const wLogWithInfo = (info, ...params) => { + console.log(chalk.blueBright(info), ...params) +} + +export const wLogWithError = (info, ...params) => { + console.log(chalk.redBright(info), ...params) +} + +export const wLogWithSuccess = (info, ...params) => { + console.log(chalk.green(info), ...params) +} + +export const wLogWithWarn = (info, ...params) => { + console.log(chalk.bgYellow.bold("WARN"), info, ...params) +} diff --git a/src/utils/replaceFilterOperators.ts b/src/utils/replaceFilterOperators.ts index ab5aa382..1b55c9e1 100644 --- a/src/utils/replaceFilterOperators.ts +++ b/src/utils/replaceFilterOperators.ts @@ -1,4 +1,4 @@ -import { isPlainObject } from "lodash" +import isPlainObject from "lodash.isplainobject" import sequelize from "sequelize" const Op = sequelize.Op @@ -6,7 +6,7 @@ const wrap = (operator: string) => { return Op[operator.replace("_", "")] } -const iterate = (obj) => { +const replaceFilterOperators = (obj) => { if (isPlainObject(obj)) { Object.keys(obj).forEach((element) => { const value = obj[element] @@ -16,13 +16,13 @@ const iterate = (obj) => { delete obj[element] } if (isPlainObject(value) || Array.isArray(value)) { - iterate(value) + replaceFilterOperators(value) } }) return obj } else if (Array.isArray(obj)) { - obj.forEach((element) => iterate(element)) + obj.forEach((element) => replaceFilterOperators(element)) } } -export default iterate +export default replaceFilterOperators diff --git a/src/utils/voidFunction.ts b/src/utils/voidFunction.ts new file mode 100644 index 00000000..9978b959 --- /dev/null +++ b/src/utils/voidFunction.ts @@ -0,0 +1 @@ +export const voidFunction = () => 1 + 2 === 1 diff --git a/test_database.sql b/test_database.sql new file mode 100644 index 00000000..1b054784 --- /dev/null +++ b/test_database.sql @@ -0,0 +1,56 @@ +-- ------------------------------------------------------------- +-- TablePlus 5.9.6(546) +-- +-- https://tableplus.com/ +-- +-- Database: wertik +-- Generation Time: 2024-04-13 14:45:19.0820 +-- ------------------------------------------------------------- + + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!40101 SET NAMES utf8mb4 */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + + +DROP TABLE IF EXISTS `category`; +CREATE TABLE `category` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `title` varchar(255) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=19 DEFAULT CHARSET=latin1; + +DROP TABLE IF EXISTS `product`; +CREATE TABLE `product` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `title` varchar(255) NOT NULL, + `sizes` enum('lg','sm','xl','xxl','xxxl') DEFAULT NULL, + `user_id` bigint DEFAULT NULL, + `category_id` bigint DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `user_id` (`user_id`), + KEY `category_id` (`category_id`), + CONSTRAINT `product_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE SET NULL, + CONSTRAINT `product_ibfk_2` FOREIGN KEY (`category_id`) REFERENCES `category` (`id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=latin1; + +DROP TABLE IF EXISTS `user`; +CREATE TABLE `user` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(10) DEFAULT NULL, + `email` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=118 DEFAULT CHARSET=latin1; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; \ No newline at end of file diff --git a/tests/database.test.js b/tests/database.test.js new file mode 100644 index 00000000..72f4d27e --- /dev/null +++ b/tests/database.test.js @@ -0,0 +1,20 @@ +require("dotenv").config() + +const { + default: wertik, + withMysqlDatabase, +} = require("../lib/index") + +const { database } = require("./testUtils") + +if (database.name) { + test("Expect test database to connect and does not causes error", async () => { + await expect( + wertik({ + database: { + default: withMysqlDatabase(database), + }, + }) + ).resolves.not.toThrowError() + }) +} diff --git a/tests/graphql.test.js b/tests/graphql.test.js new file mode 100644 index 00000000..98cde26a --- /dev/null +++ b/tests/graphql.test.js @@ -0,0 +1,143 @@ +require("dotenv").config() + +const { + default: wertik, + withModule, + withMysqlDatabase, + withApolloGraphql, +} = require("../lib/index") + +const { database, Product, User } = require("./testUtils") + +if (database.name) { + describe("Expect withMysqlDatabase, withModule and withApolloGraphql, and expect module graphql operations work", () => { + let app + test("Expect test database to connect and does not causes error", async () => { + await expect( + (app = wertik({ + database: { + default: withMysqlDatabase(database), + }, + modules: { + Product: withModule(Product), + User: withModule(User), + }, + graphql: withApolloGraphql(), + }).then((wertikApp) => { + app = wertikApp + })) + ).resolves.not.toThrowError() + }) + + let testItem = null + + test("Expect graphql to insert data", async () => { + // describe create works + testItem = await app.graphql.executeOperation({ + query: ` + mutation { + insert_products(input: { + sizes: lg + user_id: 120 + title: "My first product" + category_id: 50 + }) { + returning { + id + user_id + sizes + } + } + } + `, + }) + expect(testItem.data.insert_products.returning[0].id).toBeGreaterThan(0) + expect(testItem.data.insert_products.returning[0].sizes).toBe("lg") + }) + // update + test(`Expect graphql to update data`, async () => { + let updatedItem = await app.graphql.executeOperation({ + query: ` + mutation { + update_products(input: { sizes: xxxl, title: "My product title" }, where: { id: { _eq: ${testItem.data.insert_products.returning[0].id} } }) { + returning { + id + sizes + } + } + } + `, + }) + expect(updatedItem.data.update_products.returning[0].id).toBeGreaterThan(0) + expect(updatedItem.data.update_products.returning[0].sizes).toBe("xxxl") + }) + // view + test("Expect graphql to view data", async () => { + let viewItem = await app.graphql.executeOperation({ + query: ` + query { + product(where: { sizes: xxxl }) { + id + sizes + } + } + + `, + }) + expect(viewItem.data.product.sizes).toBe("xxxl") + }) + // delete + test("Expect graphql to delete data", async () => { + let deletedItem = await app.graphql.executeOperation({ + query: ` + mutation { + delete_products(where: { id: { _eq: ${testItem.data.insert_products.returning[0].id} } }) { + message + } + } + `, + }) + expect(deletedItem.data.delete_products.message.length).toBeGreaterThan(0) + }) + + test("Expect a one to one relationship to work", async () => { + let viewItem = await app.graphql.executeOperation({ + query: ` + query { + product(where: { sizes: xxxl }) { + id + sizes + user { + id + name + } + } + } + + `, + }) + expect(viewItem.data.product.sizes).toBe("xxxl") + expect(viewItem.data.product.user.id).toBeGreaterThan(0) + }) + test("Expect a one to many relationship to work", async () => { + let viewItem = await app.graphql.executeOperation({ + query: ` + query { + user(where: { id: { _eq: 122 } }) { + id + name + products { + id + sizes + } + } + } + + `, + }) + expect(viewItem.data.user.id).toBeGreaterThan(0) + expect(viewItem.data.user.products.length).toBeGreaterThan(0) + expect(viewItem.data.user.products[0].id).toBeGreaterThan(0) + }) + }) +} diff --git a/tests/index.test.js b/tests/index.test.js index 767ad6b0..c4711ebd 100644 --- a/tests/index.test.js +++ b/tests/index.test.js @@ -2,27 +2,17 @@ require("dotenv").config() const { default: wertik, - useModule, - useLogger, - useWinstonTransport, - useMysqlDatabase, - useIndependentWebSocketsServer, - useSocketIO, - useWebSockets, - useMailer, - useGraphql, + withLogger, + withWinstonTransport, + withIndependentWebSocketsServer, + withSocketIO, + withWebSockets, + withMailer, + withApolloGraphql, } = require("./../lib/index") -const database = { - name: process.env.TEST_DATABASE_NAME, - host: process.env.TEST_DATABASE_HOST, - password: process.env.TEST_DATABASE_PASSWORD, - port: process.env.TEST_DATABASE_PORT, - username: process.env.TEST_DATABASE_USERNAME, -} - test("Expect no configuration can start the server", async () => { - await expect((app = wertik())).resolves.not.toThrowError() + await expect(wertik()).resolves.not.toThrowError() }) test("Expect empty configuration object an start the server", async () => { @@ -33,40 +23,11 @@ test("Expect null configuration does not causes error", async () => { await expect(wertik(null)).resolves.not.toThrowError() }) -test("Expect test database to connect and does not causes error", async () => { - await expect( - wertik({ - database: { - default: useMysqlDatabase(database), - }, - }) - ).resolves.not.toThrowError() -}) - -test("Expect useMysqlDatabase, useModule and useGraphql", async () => { - await expect( - wertik({ - database: { - default: useMysqlDatabase(database), - }, - modules: { - test: useModule({ - name: "Shirts", - useDatabase: true, - database: "default", - table: process.env.TEST_DATABASE_TABLE, - }), - }, - graphql: useGraphql(), - }) - ).resolves.not.toThrowError() -}) - test("Expect mailer to work without configuration and does not causes error", async () => { await expect( wertik({ mailer: { - default: useMailer({ + default: withMailer({ name: "Default", }), }, @@ -74,30 +35,30 @@ test("Expect mailer to work without configuration and does not causes error", as ).resolves.not.toThrowError() }) -test("Expect graphql to work with useGraphql and does not causes error", async () => { +test("Expect graphql to work with withApolloGraphql and does not causes error", async () => { await expect( wertik({ - graphql: useGraphql(), + graphql: withApolloGraphql(), }) ).resolves.not.toThrowError() }) - - -test("Expect useWebsockets, useIndependentWebSocketsServer and useSocketIO works and does not throw any error", async () => { +test("Expect withWebSockets, withIndependentWebSocketsServer and withSocketIO works and does not throw any error", async () => { await expect( wertik({ sockets: { - mySockets: useWebSockets({ + mySockets: withWebSockets({ path: "/websockets", }), - socketio: useSocketIO({ + socketio: withSocketIO({ path: "/mysocketioserver", }), - mySockets2: useIndependentWebSocketsServer({ + mySockets2: withIndependentWebSocketsServer({ port: 1500, }), }, + }).then((app) => { + app.sockets.mySockets2.close() }) ).resolves.not.toThrowError() }) @@ -105,8 +66,8 @@ test("Expect useWebsockets, useIndependentWebSocketsServer and useSocketIO works test("Expect logger to run without throwing any error", async () => { await expect( wertik({ - logger: useLogger({ - transports: useWinstonTransport((winston) => { + logger: withLogger({ + transports: withWinstonTransport((winston) => { return [ new winston.transports.File({ filename: "info.log", diff --git a/tests/testUtils.js b/tests/testUtils.js new file mode 100644 index 00000000..9d432aad --- /dev/null +++ b/tests/testUtils.js @@ -0,0 +1,45 @@ +exports.database = { + name: process.env.TEST_DATABASE_NAME, + host: process.env.TEST_DATABASE_HOST, + password: process.env.TEST_DATABASE_PASSWORD, + port: process.env.TEST_DATABASE_PORT, + username: process.env.TEST_DATABASE_USERNAME, +} + +exports.Product = { + name: "Product", + useDatabase: true, + database: "default", + table: process.env.TEST_DATABASE_TABLE_PRODUCT, + on: function ({ belongsTo }) { + belongsTo({ + database: "ecommerce", + graphqlKey: "user", + module: "User", + options: { + as: "user", + foreignKey: "user_id", + targetKey: "id", + }, + }) + }, +} + +exports.User = { + name: "User", + useDatabase: true, + database: "default", + table: process.env.TEST_DATABASE_TABLE_USER, + on: function ({ hasMany }) { + hasMany({ + database: "ecommerce", + graphqlKey: "products", + module: "Product", + options: { + as: "products", + foreignKey: "user_id", + sourceKey: "id", + }, + }) + }, +} diff --git a/wertik.sql b/wertik.sql new file mode 100644 index 00000000..1b054784 --- /dev/null +++ b/wertik.sql @@ -0,0 +1,56 @@ +-- ------------------------------------------------------------- +-- TablePlus 5.9.6(546) +-- +-- https://tableplus.com/ +-- +-- Database: wertik +-- Generation Time: 2024-04-13 14:45:19.0820 +-- ------------------------------------------------------------- + + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!40101 SET NAMES utf8mb4 */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + + +DROP TABLE IF EXISTS `category`; +CREATE TABLE `category` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `title` varchar(255) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=19 DEFAULT CHARSET=latin1; + +DROP TABLE IF EXISTS `product`; +CREATE TABLE `product` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `title` varchar(255) NOT NULL, + `sizes` enum('lg','sm','xl','xxl','xxxl') DEFAULT NULL, + `user_id` bigint DEFAULT NULL, + `category_id` bigint DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `user_id` (`user_id`), + KEY `category_id` (`category_id`), + CONSTRAINT `product_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE SET NULL, + CONSTRAINT `product_ibfk_2` FOREIGN KEY (`category_id`) REFERENCES `category` (`id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=latin1; + +DROP TABLE IF EXISTS `user`; +CREATE TABLE `user` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(10) DEFAULT NULL, + `email` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=118 DEFAULT CHARSET=latin1; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; \ No newline at end of file