├── examples ├── .gitignore ├── README.md └── basic │ ├── rel.config.json │ ├── .env │ ├── rel │ ├── schema.graphql │ └── client │ │ ├── index.ts │ │ └── generated │ │ ├── guards.esm.js │ │ ├── guards.cjs.js │ │ ├── index.esm.js │ │ ├── index.js │ │ ├── index.d.ts │ │ ├── schema.graphql │ │ ├── types.cjs.js │ │ ├── types.esm.js │ │ └── schema.ts │ └── package.json ├── packages ├── rel-bundle │ ├── src │ │ ├── server.ts │ │ └── client.ts │ ├── README.md │ ├── tsconfig.json │ └── package.json ├── cyypher │ ├── src │ │ ├── logger.ts │ │ ├── util │ │ │ ├── beautify.ts │ │ │ ├── geo.ts │ │ │ ├── coercion.ts │ │ │ ├── sanitize.ts │ │ │ ├── cleanPrefix.ts │ │ │ ├── object.ts │ │ │ ├── buildWhereQuery.ts │ │ │ └── params.ts │ │ ├── test │ │ │ └── index.ts │ │ ├── helpers │ │ │ ├── count.ts │ │ │ ├── deleteMany.ts │ │ │ ├── node.ts │ │ │ ├── findOrCreate.ts │ │ │ ├── delete.ts │ │ │ ├── find.ts │ │ │ ├── rel.ts │ │ │ ├── create.ts │ │ │ ├── updateMany.ts │ │ │ ├── merge.ts │ │ │ ├── update.ts │ │ │ ├── list.ts │ │ │ └── relationships.ts │ │ ├── types.ts │ │ └── index.ts │ ├── CHANGELOG.md │ ├── tsconfig.json │ ├── package.json │ └── README.md ├── rel-cli │ ├── README.md │ ├── tsconfig.json │ ├── CHANGELOG.md │ ├── src │ │ ├── commands │ │ │ ├── gen.ts │ │ │ ├── dev.ts │ │ │ └── init.ts │ │ └── cli.ts │ └── package.json ├── rel-server │ ├── tsconfig.json │ ├── src │ │ ├── resolvers │ │ │ ├── count.ts │ │ │ ├── findMany.ts │ │ │ ├── create.ts │ │ │ ├── merge.ts │ │ │ ├── update.ts │ │ │ ├── deleteMany.ts │ │ │ ├── updateMany.ts │ │ │ ├── findOne.ts │ │ │ ├── relation.ts │ │ │ ├── index.ts │ │ │ └── delete.ts │ │ ├── scalars │ │ │ └── index.ts │ │ ├── schema │ │ │ ├── fields.ts │ │ │ ├── parser.ts │ │ │ └── makeAugmentedSchema.ts │ │ ├── types.ts │ │ └── index.ts │ ├── CHANGELOG.md │ └── package.json └── rel-client │ ├── tsconfig.json │ ├── package.json │ └── src │ └── index.ts ├── .prettierrc ├── docs ├── Architecture.png ├── architecture.jpg ├── how-it-works.jpg └── example_POC.rel ├── .gitignore ├── jest.config.js ├── pnpm-workspace.yaml ├── .changeset ├── config.json └── README.md ├── lerna.json ├── tsconfig.json ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── build.yml ├── contributing.md ├── package.json ├── LICENSE.md ├── README.md └── ideas.txt /examples/.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | yarn.lock -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # rel Examples 2 | 3 | - [Social](social) 4 | -------------------------------------------------------------------------------- /examples/basic/rel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseDir": "./rel" 3 | } 4 | -------------------------------------------------------------------------------- /packages/rel-bundle/src/server.ts: -------------------------------------------------------------------------------- 1 | export * from "rel-server" 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "semi": false 5 | } 6 | -------------------------------------------------------------------------------- /docs/Architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ian/rel/HEAD/docs/Architecture.png -------------------------------------------------------------------------------- /docs/architecture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ian/rel/HEAD/docs/architecture.jpg -------------------------------------------------------------------------------- /docs/how-it-works.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ian/rel/HEAD/docs/how-it-works.jpg -------------------------------------------------------------------------------- /examples/basic/.env: -------------------------------------------------------------------------------- 1 | NEO4J_URI=bolt://localhost:7687 2 | NEO4J_USERNAME=neo4j 3 | NEO4J_PASSWORD=rel -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env* 3 | dist/ 4 | coverage/ 5 | .vscode 6 | .DS_Store 7 | *.old 8 | *.log -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | verbose: true, 3 | verbose: true, 4 | rootDir: "./", 5 | transform: { 6 | "^.+\\.tsx?$": "ts-jest", 7 | }, 8 | } -------------------------------------------------------------------------------- /packages/cyypher/src/logger.ts: -------------------------------------------------------------------------------- 1 | import Logger from '@ptkdev/logger' 2 | const logger = new Logger({ 3 | debug: !!process.env.REL_DEBUG, 4 | }) 5 | 6 | export default logger 7 | -------------------------------------------------------------------------------- /packages/cyypher/src/util/beautify.ts: -------------------------------------------------------------------------------- 1 | export function beautifyCypher(query) { 2 | return query 3 | .split('\n') 4 | .map((s) => s.trim()) 5 | .join('\n') 6 | } 7 | -------------------------------------------------------------------------------- /packages/rel-bundle/README.md: -------------------------------------------------------------------------------- 1 | # rel 2 | 3 | See [https://rel.run](https://rel.run) for full documentation or [https://github.com/rel-js/rel](https://github.com/rel-js/rel) for repository. 4 | -------------------------------------------------------------------------------- /packages/rel-cli/README.md: -------------------------------------------------------------------------------- 1 | # rel 2 | 3 | See [https://rel.run](https://rel.run) for full documentation or [https://github.com/rel-js/rel](https://github.com/rel-js/rel) for repository. 4 | -------------------------------------------------------------------------------- /packages/cyypher/src/util/geo.ts: -------------------------------------------------------------------------------- 1 | export class Geo { 2 | lat = null 3 | lng = null 4 | 5 | constructor({ lat, lng }) { 6 | this.lat = lat 7 | this.lng = lng 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/rel-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist" 6 | }, 7 | "include": ["src"] 8 | } 9 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | prefer-workspace-packages: true 2 | packages: 3 | # all packages in subdirs of packages/ and components/ 4 | - "packages/**" 5 | # exclude packages that are inside test directories 6 | - "!**/test/**" 7 | -------------------------------------------------------------------------------- /packages/rel-client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | "esModuleInterop": true 7 | }, 8 | "include": ["src"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/rel-server/src/resolvers/count.ts: -------------------------------------------------------------------------------- 1 | export default function countResolver(label: string) { 2 | return async (obj, args, context, info) => { 3 | const { cypher } = context 4 | return cypher.count(label, args) 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/rel-server/src/resolvers/findMany.ts: -------------------------------------------------------------------------------- 1 | export default function findManyResolver(label: string) { 2 | return async (obj, args, context, info) => { 3 | const { cypher } = context 4 | return cypher.list(label, args) 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/rel-server/src/scalars/index.ts: -------------------------------------------------------------------------------- 1 | import { DateTimeResolver, UUIDResolver } from "graphql-scalars" 2 | 3 | // export { default as String } from "./String" 4 | 5 | export default { 6 | DateTime: DateTimeResolver, 7 | ID: UUIDResolver, 8 | } 9 | -------------------------------------------------------------------------------- /packages/rel-bundle/src/client.ts: -------------------------------------------------------------------------------- 1 | import Cookies from "js-cookie" 2 | 3 | const KEY = "_rel_auth_token" 4 | 5 | export function getStoredToken() { 6 | return Cookies.get(KEY) 7 | } 8 | 9 | export function setStoredToken(tok) { 10 | Cookies.set(KEY, tok) 11 | } 12 | -------------------------------------------------------------------------------- /packages/rel-server/src/resolvers/create.ts: -------------------------------------------------------------------------------- 1 | export default function createResolver(label: string) { 2 | return async (obj, args, context, info) => { 3 | const { data } = args 4 | const { cypher } = context 5 | 6 | return cypher.create(label, data) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/rel-server/src/resolvers/merge.ts: -------------------------------------------------------------------------------- 1 | export default function mergeResolver(label: string) { 2 | return async (obj, args, context, info) => { 3 | const { where, data } = args 4 | const { cypher } = context 5 | 6 | return cypher.merge(label, where, data) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/rel-server/src/resolvers/update.ts: -------------------------------------------------------------------------------- 1 | export default function updateResolver(label: string) { 2 | return async (obj, args, context, info) => { 3 | const { where, data } = args 4 | const { cypher } = context 5 | 6 | return cypher.update(label, where, data) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/rel-server/src/resolvers/deleteMany.ts: -------------------------------------------------------------------------------- 1 | export default function deleteManyResolver(label: string) { 2 | return async (obj, args, context, info) => { 3 | const { where, data } = args 4 | const { cypher } = context 5 | 6 | return cypher.deleteMany(label, where, data) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/rel-server/src/resolvers/updateMany.ts: -------------------------------------------------------------------------------- 1 | export default function updateManyResolver(label: string) { 2 | return async (obj, args, context, info) => { 3 | const { where, data } = args 4 | const { cypher } = context 5 | 6 | return cypher.updateMany(label, where, data) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/basic/rel/schema.graphql: -------------------------------------------------------------------------------- 1 | 2 | type Movie { 3 | title: String! 4 | year: Int 5 | rating: Float 6 | genres: [Genre]! @rel(label: "IN_GENRE", direction: OUT) 7 | } 8 | 9 | type Genre { 10 | name: String 11 | movies: [Movie]! @rel(label: "IN_GENRE", direction: IN) 12 | } 13 | 14 | -------------------------------------------------------------------------------- /packages/rel-server/src/resolvers/findOne.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "../types" 2 | 3 | export default function findOneResolver(label: string) { 4 | return async (obj, args, context: Context, info) => { 5 | const { where } = args 6 | const { cypher } = context 7 | 8 | return cypher.find(label, where) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@1.7.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /packages/rel-bundle/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | "esModuleInterop": true, 7 | "paths": { 8 | "client/*": ["./client/*"], 9 | "server/*": ["./server/*"] 10 | } 11 | }, 12 | "include": ["src"] 13 | } 14 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "version": "independent", 6 | "npmClient": "yarn", 7 | "useWorkspaces": true, 8 | "command": { 9 | "publish": { 10 | "conventionalCommits": true, 11 | "message": "chore(release): publish", 12 | "allowBranch": ["main"] 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/cyypher/src/test/index.ts: -------------------------------------------------------------------------------- 1 | // Jest is a nightmare with Typescript. Need to figure something else out. 2 | 3 | import { Client } from '../index' 4 | 5 | async function run() { 6 | const client = new Client('redis://localhost:6379') 7 | const count = await client.count('Person') 8 | console.log({ count }) 9 | } 10 | 11 | run() 12 | -------------------------------------------------------------------------------- /packages/rel-cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "module": "ESnext", 6 | "target": "ESnext", 7 | "importHelpers": false, 8 | "lib": ["esnext"], 9 | "moduleResolution": "node", 10 | "esModuleInterop": true 11 | }, 12 | "include": ["src"] 13 | } 14 | -------------------------------------------------------------------------------- /packages/cyypher/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # cyypher 2 | 3 | ## 0.1.0 4 | 5 | ### Minor Changes 6 | 7 | - Overhaul of CLI tooling, using rel.config.json now, init and dev scripts working well" 8 | 9 | ## 0.0.10 10 | 11 | ### Patch Changes 12 | 13 | - a8c7f72: Bugfix on cyypher 14 | 15 | ## 0.0.8 16 | 17 | ### Patch Changes 18 | 19 | - Adding changesets and releasing alpha 20 | -------------------------------------------------------------------------------- /examples/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "rel dev" 8 | }, 9 | "devDependencies": { 10 | "rel-cmd": "latest" 11 | }, 12 | "author": "Ian Hunter", 13 | "license": "ISC", 14 | "dependencies": { 15 | "rel-bundle": "latest" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "moduleResolution": "node", 5 | "esModuleInterop": true, 6 | "skipLibCheck": true, 7 | "removeComments": true, 8 | "declaration": true, 9 | "sourceMap": true, 10 | "lib": ["es6"] 11 | }, 12 | "include": [], 13 | "exclude": ["**/node_modules", "./dist", "./test"] 14 | } 15 | -------------------------------------------------------------------------------- /packages/cyypher/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | // "module": "commonjs", 5 | "module": "ESnext", 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true, 8 | "target": "es6", 9 | "moduleResolution": "node", 10 | "sourceMap": true, 11 | "declaration": true, 12 | "outDir": "dist", 13 | "baseUrl": "." 14 | }, 15 | "include": ["src/**/*.ts"] 16 | } 17 | -------------------------------------------------------------------------------- /packages/rel-server/src/schema/fields.ts: -------------------------------------------------------------------------------- 1 | import { Fields } from "../types" 2 | 3 | type Opts = { 4 | optional?: boolean 5 | } 6 | 7 | export function fieldsToComposer(fields: Fields, opts: Opts = {}): Fields { 8 | const { optional = false } = opts 9 | return Object.values(fields).reduce((acc, fieldGroup) => { 10 | const { name, type } = fieldGroup 11 | acc[name] = optional ? type.replace("!", "") : type // Make the field not required 12 | return acc 13 | }, {}) 14 | } 15 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /examples/basic/rel/client/index.ts: -------------------------------------------------------------------------------- 1 | // Autogenerated by Rel (https://rel.run). Do not modify this file directly. 2 | 3 | import { getStoredToken } from "rel-bundle/client" 4 | import { createClient } from "./generated/index" 5 | 6 | export * from "rel-bundle/client" 7 | export * from "./generated/index" 8 | 9 | const url = process.env.NEXT_PUBLIC_REL_URL || "http://localhost:4000/graphql" 10 | 11 | export default createClient({ 12 | url, 13 | headers: () => ({ 14 | Authorization: getStoredToken(), 15 | "x-auth-token": getStoredToken() 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /packages/rel-server/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # rel-server 2 | 3 | ## 0.1.0 4 | 5 | ### Minor Changes 6 | 7 | - Overhaul of CLI tooling, using rel.config.json now, init and dev scripts working well" 8 | 9 | ### Patch Changes 10 | 11 | - Updated dependencies 12 | - cyypher@0.1.0 13 | 14 | ## 0.0.3 15 | 16 | ### Patch Changes 17 | 18 | - a8c7f72: Bugfix on cyypher 19 | - Updated dependencies [a8c7f72] 20 | - cyypher@0.0.10 21 | 22 | ## 0.0.2 23 | 24 | ### Patch Changes 25 | 26 | - Adding changesets and releasing alpha 27 | - Updated dependencies 28 | - cyypher@0.0.8 29 | -------------------------------------------------------------------------------- /packages/rel-cli/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # rel-js 2 | 3 | ## 0.1.0 4 | 5 | ### Minor Changes 6 | 7 | - Overhaul of CLI tooling, using rel.config.json now, init and dev scripts working well" 8 | 9 | ### Patch Changes 10 | 11 | - Updated dependencies 12 | - rel-server@0.1.0 13 | 14 | ## 0.0.2 15 | 16 | ### Patch Changes 17 | 18 | - a8c7f72: Bugfix on cyypher 19 | - Updated dependencies [a8c7f72] 20 | - rel-server@0.0.3 21 | 22 | ## 0.0.1 23 | 24 | ### Patch Changes 25 | 26 | - Adding changesets and releasing alpha 27 | - Updated dependencies 28 | - rel-server@0.0.2 29 | -------------------------------------------------------------------------------- /packages/cyypher/src/helpers/count.ts: -------------------------------------------------------------------------------- 1 | import buildWhereQuery from '../util/buildWhereQuery.js' 2 | 3 | export async function cypherCount (label, opts) { 4 | const { where } = opts || {} 5 | 6 | const cypherQuery = [] 7 | 8 | cypherQuery.push(`MATCH (node:${label})`) 9 | 10 | if (where) { 11 | cypherQuery.push(`WHERE ${buildWhereQuery(where, { prefix: 'node.' })}`) 12 | } 13 | 14 | // @todo support multiple returns 15 | cypherQuery.push('RETURN COUNT(node) as count') 16 | 17 | return this.exec1(cypherQuery.join('\n')).then(res => res?.count || 0) 18 | } 19 | -------------------------------------------------------------------------------- /packages/rel-server/src/resolvers/relation.ts: -------------------------------------------------------------------------------- 1 | import { Context, Relation } from "../types" 2 | 3 | export default function relationResolver(parsedRelation: Relation) { 4 | const { relation } = parsedRelation 5 | const { label, direction, type } = relation 6 | 7 | return async (obj, args, context: Context, info) => { 8 | const { where, skip, limit } = args 9 | const { cypher } = context 10 | 11 | return cypher.listRelationship( 12 | cypher.ref(obj), 13 | { __typename: label, __direction: direction }, 14 | type, 15 | args 16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/cyypher/src/helpers/deleteMany.ts: -------------------------------------------------------------------------------- 1 | import buildWhereQuery from "../util/buildWhereQuery.js" 2 | 3 | export async function cypherDeleteMany(label, where, projection = []) { 4 | const query = [] 5 | query.push(`MATCH (node:${label})`) 6 | 7 | if (typeof where === "object" && Object.keys(where).length > 0) { 8 | query.push(`WHERE ${buildWhereQuery(where, { prefix: "node." })}`) 9 | } 10 | 11 | query.push(`DETACH DELETE node`) 12 | query.push(`RETURN COUNT(node) as count`) 13 | 14 | const res = await this.exec1(query.join("\n")) 15 | 16 | return res?.count || 0 17 | } 18 | -------------------------------------------------------------------------------- /packages/cyypher/src/helpers/node.ts: -------------------------------------------------------------------------------- 1 | import { paramify } from '../util/params.js' 2 | 3 | function normalizeNode (node) { 4 | if (typeof node === 'string') { 5 | return { 6 | __typename: node 7 | } 8 | } else { 9 | return node 10 | } 11 | } 12 | 13 | export function cypherNode (name, node) { 14 | const _cypher = [] 15 | 16 | const { __typename, ...params } = normalizeNode(node) 17 | 18 | _cypher.push(name) 19 | if (__typename) _cypher.push(`:${__typename}`) 20 | if (params) _cypher.push(` { ${paramify(params)}} `) 21 | 22 | return `(${_cypher.join('')})` 23 | } 24 | -------------------------------------------------------------------------------- /packages/cyypher/src/helpers/findOrCreate.ts: -------------------------------------------------------------------------------- 1 | import { Node } from 'src/types.js' 2 | 3 | type Params = Record 4 | type CreateOpts = Params | (() => Promise) 5 | 6 | export async function cypherFindOrCreate( 7 | label: string, 8 | find: Params, 9 | create: CreateOpts = {}, 10 | opts = {} 11 | ): Promise { 12 | let node = await this.find(label, find) 13 | if (!node) { 14 | const createOpts = typeof create === 'function' ? await create() : create 15 | node = await this.create(label, { ...find, ...createOpts }, opts) 16 | } 17 | 18 | return node 19 | } 20 | -------------------------------------------------------------------------------- /packages/cyypher/src/helpers/delete.ts: -------------------------------------------------------------------------------- 1 | import buildWhereQuery from "../util/buildWhereQuery.js" 2 | 3 | export async function cypherDelete(label, where, projection = []) { 4 | const query = [] 5 | query.push(`MATCH (node:${label})`) 6 | 7 | if (typeof where === "object" && Object.keys(where).length > 0) { 8 | query.push(`WHERE ${buildWhereQuery(where, { prefix: "node." })}`) 9 | } 10 | 11 | query.push(`DETACH DELETE node`) 12 | query.push(`RETURN COUNT(node) as count`) 13 | query.push(`LIMIT 1`) 14 | 15 | const res = await this.exec1(query.join("\n")) 16 | 17 | return res?.count 18 | } 19 | -------------------------------------------------------------------------------- /packages/rel-server/src/resolvers/index.ts: -------------------------------------------------------------------------------- 1 | export { default as findOneResolver } from "./findOne" 2 | export { default as findManyResolver } from "./findMany" 3 | export { default as countResolver } from "./count" 4 | 5 | export { default as createResolver } from "./create" 6 | export { default as updateResolver } from "./update" 7 | export { default as updateManyResolver } from "./updateMany" 8 | export { default as mergeResolver } from "./merge" 9 | export { default as deleteResolver } from "./delete" 10 | export { default as deleteManyResolver } from "./deleteMany" 11 | 12 | export { default as relationResolver } from "./relation" 13 | -------------------------------------------------------------------------------- /packages/cyypher/src/util/coercion.ts: -------------------------------------------------------------------------------- 1 | import { Geo } from './geo.js' 2 | 3 | export function coerce (val) { 4 | switch (true) { 5 | case val instanceof Geo: 6 | return `point({ latitude: ${val.lat}, longitude: ${val.lng} })` 7 | case typeof val === 'string': 8 | return `"${val.replace(/"/g, '\\"')}"` 9 | case val === null: 10 | case val === undefined: 11 | return 'null' 12 | case typeof val === 'object': 13 | return Object.keys(val).reduce((acc, key) => { 14 | acc[key] = coerce(val[key]) 15 | return acc 16 | }, {}) 17 | default: 18 | return val 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/cyypher/src/helpers/find.ts: -------------------------------------------------------------------------------- 1 | import { paramify } from '../util/params.js' 2 | import cleanPrefix from '../util/cleanPrefix.js' 3 | 4 | export async function cypherFind(label, params, projection = []) { 5 | const query = `MATCH (node:${label} { ${paramify(params)} }) 6 | RETURN ${ 7 | projection.length > 0 8 | ? projection.reduce( 9 | (previous, current, idx, arr) => 10 | previous + `node.${current}${idx === arr.length - 1 ? '' : ','}`, 11 | '' 12 | ) 13 | : 'node' 14 | };` 15 | const res = await this.exec1(query) 16 | return projection.length > 0 ? cleanPrefix(res, 'node.') : res?.node 17 | } 18 | -------------------------------------------------------------------------------- /packages/cyypher/src/util/sanitize.ts: -------------------------------------------------------------------------------- 1 | export function sanitize (maybeObject) { 2 | switch (true) { 3 | case maybeObject === undefined: 4 | return null 5 | // case isInt(maybeObject): 6 | // return parseInt(maybeObject.toString()) 7 | case typeof maybeObject === 'object': 8 | if (maybeObject.latitude && maybeObject.latitude) { 9 | return { 10 | lat: maybeObject.latitude, 11 | lng: maybeObject.longitude 12 | } 13 | } 14 | return Object.keys(maybeObject).reduce((acc, key) => { 15 | acc[key] = sanitize(maybeObject[key]) 16 | return acc 17 | }, {}) 18 | default: 19 | return maybeObject 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /packages/cyypher/src/helpers/rel.ts: -------------------------------------------------------------------------------- 1 | import { paramify } from "../util/params.js" 2 | 3 | function normalizeRel(rel) { 4 | if (typeof rel === "string") { 5 | return { 6 | __typename: rel, 7 | } 8 | } else { 9 | return rel 10 | } 11 | } 12 | 13 | export function cypherRel(name, rel) { 14 | const { __typename, __direction, ...values } = normalizeRel(rel) 15 | const inner = [`${name}:${__typename}`] 16 | if (values) inner.push(`{ ${paramify(values)} }`) 17 | 18 | switch (__direction) { 19 | case "IN": 20 | return `<-[${inner.join(" ")}]-` 21 | case "NONE": 22 | return `-[${inner.join(" ")}]-` 23 | case "OUT": 24 | default: 25 | return `-[${inner.join(" ")}]->` 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/cyypher/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cyypher", 3 | "version": "0.1.0", 4 | "description": "Cypher ORM + client for redis-graph", 5 | "author": "Ian Hunter ", 6 | "license": "ISC", 7 | "main": "dist/index.mjs", 8 | "scripts": { 9 | "clean": "rm -rf ./dist", 10 | "build": "tsup --format esm", 11 | "build:watch": "tsup --watch --format esm" 12 | }, 13 | "tsup": { 14 | "entry": [ 15 | "src/index.ts" 16 | ], 17 | "splitting": false, 18 | "sourcemap": true, 19 | "clean": true 20 | }, 21 | "dependencies": { 22 | "@ptkdev/logger": "^1.8.0", 23 | "lodash": "^4.17.21", 24 | "redisgraph.js": "^2.3.0", 25 | "uuid": "^8.3.2" 26 | }, 27 | "devDependencies": { 28 | "tsup": "^5.11.13" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/rel-server/src/types.ts: -------------------------------------------------------------------------------- 1 | import { Client } from "cyypher" 2 | 3 | export type Field = { 4 | name: string 5 | type: string 6 | directives: Directive[] 7 | } 8 | 9 | export type Fields = { 10 | [name: string]: Field 11 | } 12 | 13 | export type Relation = { 14 | name: string 15 | type: string 16 | relation: RelationDirective 17 | directives: Directive[] 18 | } 19 | 20 | export type Relations = { 21 | [name: string]: Relation 22 | } 23 | 24 | export type Args = {} 25 | 26 | export type Context = { 27 | cypher: Client 28 | } 29 | 30 | export type Directive = { 31 | name: string 32 | args: { 33 | [name: string]: string 34 | }[] 35 | } 36 | 37 | export type RelationDirective = { 38 | label: string 39 | direction: string 40 | type: string 41 | } 42 | -------------------------------------------------------------------------------- /packages/rel-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rel-client", 3 | "version": "0.0.0", 4 | "description": "Visit https://rel.run/docs to view the full documentation.", 5 | "author": "Ian Hunter ", 6 | "main": "dist/index.mjs", 7 | "scripts": { 8 | "clean": "rm -rf ./dist", 9 | "build": "tsup --format esm", 10 | "build:watch": "tsup --watch --format esm" 11 | }, 12 | "tsup": { 13 | "entry": [ 14 | "src/index.ts" 15 | ], 16 | "splitting": false, 17 | "dts": true, 18 | "sourcemap": true, 19 | "clean": true 20 | }, 21 | "license": "ISC", 22 | "devDependencies": { 23 | "tsup": "^5.11.13" 24 | }, 25 | "dependencies": { 26 | "@genql/cli": "^2.9.0", 27 | "@genql/runtime": "^2.9.0", 28 | "fs-utils": "^0.7.0", 29 | "graphql": "^16.3.0", 30 | "js-cookie": "^3.0.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/rel-cli/src/commands/gen.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs" 2 | import path from "path" 3 | import Rel from "rel-server" 4 | import { printSchema } from "graphql" 5 | import { generateClient } from "rel-client" 6 | 7 | type Opts = { 8 | dir: string 9 | logging: boolean 10 | } 11 | 12 | export default async (): Promise => { 13 | const currentDir = process.cwd() 14 | const config = fs.readFileSync(`${currentDir}/rel.config.json`).toString() 15 | const { baseDir } = JSON.parse(config) 16 | 17 | const typeDefs = fs 18 | .readFileSync(path.join(baseDir, "schema.graphql")) 19 | .toString() 20 | 21 | const server = new Rel({ 22 | typeDefs, 23 | }) 24 | const generatedSchema = await server.generateSchema() 25 | 26 | await generateClient({ 27 | schema: printSchema(generatedSchema), 28 | outputPath: path.join(baseDir, "client"), 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /packages/cyypher/src/util/cleanPrefix.ts: -------------------------------------------------------------------------------- 1 | export default (obj, prefix) => { 2 | if (Array.isArray(obj) || (typeof obj === 'object' && obj !== null)) { 3 | const cleanKeys = (_obj) => { 4 | if (typeof _obj === 'object' && _obj !== null) { 5 | const newObj = {} 6 | Object.keys(_obj).forEach((key) => { 7 | const cleanedKey = key 8 | .replace(prefix, '') 9 | .replace(/\([a-zA-Z0-9\s]+\)/, '') 10 | newObj[cleanedKey] = _obj[key] 11 | }) 12 | return newObj 13 | } else { 14 | return _obj 15 | } 16 | } 17 | if (Array.isArray(obj)) { 18 | const result = [] 19 | for (let i = 0; i < obj.length; i++) { 20 | result.push(cleanKeys(obj[i])) 21 | } 22 | return result 23 | } else { 24 | return cleanKeys(obj) 25 | } 26 | } else { 27 | return obj 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/rel-server/src/resolvers/delete.ts: -------------------------------------------------------------------------------- 1 | export default function deleteResolver(label: string) { 2 | return async (obj, args, context, info) => { 3 | const { where } = args 4 | const { cypher } = context 5 | 6 | // In an ideal world we'd be able to alias the deleted node and return its 7 | // contents. This unfortunately isn't possible in redis-graph right now. 8 | // 9 | // If it were it'd look something like this: 10 | // MATCH (node:Label) 11 | // WHERE node.id = "..." 12 | // WITH node, properties(node) AS match 13 | // DETACH DELETE node 14 | // RETURN match 15 | // LIMIT 1 16 | // 17 | // For now, let's just find an then delete the node. 18 | 19 | const node = await cypher.find(label, where) 20 | if (node) { 21 | const count = await cypher.delete(label, { id: node.id }) 22 | console.log({ count }) 23 | } 24 | return node 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/rel-cli/src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | process.removeAllListeners("warning") 4 | 5 | import { Command } from "commander" 6 | 7 | import DevCommand from "./commands/dev.js" 8 | import InitCommand from "./commands/init.js" 9 | import GenCommand from "./commands/gen.js" 10 | 11 | const program = new Command() 12 | 13 | // program.option('-d, --debug', 'output extra debugging') 14 | 15 | program 16 | .command("init") 17 | .description("Setup Rel for your app") 18 | .action(InitCommand) 19 | 20 | program 21 | .command("dev") 22 | .description("Start Rel in development mode") 23 | .option("-d, --dir ", "Base directory") 24 | .option("-v, --verbose", "Make Rel more talkative") 25 | .action(DevCommand) 26 | 27 | program 28 | .command("gen") 29 | .description("Generate Rel Typescript client") 30 | .action(GenCommand) 31 | 32 | program.parse(process.argv) 33 | 34 | const options = program.opts() 35 | if (options.debug) console.log(options) 36 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to rel 2 | 3 | 1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your own GitHub account and then [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device. 4 | 2. Create a new branch `git checkout -b MY_BRANCH_NAME` 5 | 3. Install yarn: `npm install -g yarn` 6 | 4. Install the dependencies: `yarn` 7 | 5. Run `yarn dev` to build and watch for code changes 8 | 6. `git push` your repo up to github. 9 | 7. open a pull request to the this repository. 10 | 8. _thanks for your contribution!_ 11 | 12 | ## Getting setup 13 | 14 | You will need a redis DB running 15 | 16 | - `brew install redis` on Mac OS X 17 | 18 | Next, edit your `.env` and set your redis database parameters: 19 | 20 | ``` 21 | REDIS_HOST=localhost 22 | REDIS_PORT=6379 23 | REDIS_USERNAME= 24 | REDIS_PASSWORD= 25 | ``` 26 | 27 | That's it for configuration. 28 | 29 | ## To run tests 30 | 31 | Running all tests: 32 | 33 | ```sh 34 | yarn test 35 | ``` 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /docs/example_POC.rel: -------------------------------------------------------------------------------- 1 | import { server, Fields, Auth, Direction } from "@reldb/run" 2 | const { phoneNumber, int, string, relation } = Fields 3 | 4 | server() 5 | // by default, Auth.SOCIAL provides 6 | // user.following (User-[FOLLOWS]->User) and 7 | // user.followers (User)<-[FOLLOWS]-(User) 8 | .auth(Auth.SOCIAL) 9 | // Schema extends / adds new models to the overall schema 10 | .schema({ 11 | User: { 12 | fields: { 13 | name: string().required(), 14 | location: string(), 15 | phone: phoneNumber(), 16 | // polymorphic example 17 | favorites: relation("FAVORITE").to(["Movies","Books", ...]) 18 | } 19 | }, 20 | Book: { 21 | fields: { 22 | title: string().required(), 23 | favoriters: relation("FAVORITE").inbound().to("User") 24 | } 25 | }, 26 | Movie: { 27 | fields: { 28 | title: string().required(), 29 | released: int().required() 30 | } 31 | } 32 | }) 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /examples/basic/rel/client/generated/guards.esm.js: -------------------------------------------------------------------------------- 1 | 2 | var Genre_possibleTypes = ['Genre'] 3 | export var isGenre = function(obj) { 4 | if (!obj || !obj.__typename) throw new Error('__typename is missing in "isGenre"') 5 | return Genre_possibleTypes.includes(obj.__typename) 6 | } 7 | 8 | 9 | 10 | var Movie_possibleTypes = ['Movie'] 11 | export var isMovie = function(obj) { 12 | if (!obj || !obj.__typename) throw new Error('__typename is missing in "isMovie"') 13 | return Movie_possibleTypes.includes(obj.__typename) 14 | } 15 | 16 | 17 | 18 | var Mutation_possibleTypes = ['Mutation'] 19 | export var isMutation = function(obj) { 20 | if (!obj || !obj.__typename) throw new Error('__typename is missing in "isMutation"') 21 | return Mutation_possibleTypes.includes(obj.__typename) 22 | } 23 | 24 | 25 | 26 | var Query_possibleTypes = ['Query'] 27 | export var isQuery = function(obj) { 28 | if (!obj || !obj.__typename) throw new Error('__typename is missing in "isQuery"') 29 | return Query_possibleTypes.includes(obj.__typename) 30 | } 31 | -------------------------------------------------------------------------------- /examples/basic/rel/client/generated/guards.cjs.js: -------------------------------------------------------------------------------- 1 | 2 | var Genre_possibleTypes = ['Genre'] 3 | module.exports.isGenre = function(obj) { 4 | if (!obj || !obj.__typename) throw new Error('__typename is missing in "isGenre"') 5 | return Genre_possibleTypes.includes(obj.__typename) 6 | } 7 | 8 | 9 | 10 | var Movie_possibleTypes = ['Movie'] 11 | module.exports.isMovie = function(obj) { 12 | if (!obj || !obj.__typename) throw new Error('__typename is missing in "isMovie"') 13 | return Movie_possibleTypes.includes(obj.__typename) 14 | } 15 | 16 | 17 | 18 | var Mutation_possibleTypes = ['Mutation'] 19 | module.exports.isMutation = function(obj) { 20 | if (!obj || !obj.__typename) throw new Error('__typename is missing in "isMutation"') 21 | return Mutation_possibleTypes.includes(obj.__typename) 22 | } 23 | 24 | 25 | 26 | var Query_possibleTypes = ['Query'] 27 | module.exports.isQuery = function(obj) { 28 | if (!obj || !obj.__typename) throw new Error('__typename is missing in "isQuery"') 29 | return Query_possibleTypes.includes(obj.__typename) 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Smoke Test 2 | on: [push] 3 | jobs: 4 | build: 5 | name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }} 6 | 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | node: ["12.x", "14.x"] 11 | os: [ubuntu-latest] 12 | 13 | steps: 14 | - name: Checkout repo 15 | uses: actions/checkout@v2 16 | 17 | - name: Use Node ${{ matrix.node }} 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: ${{ matrix.node }} 21 | 22 | - name: Cache pnpm modules 23 | uses: actions/cache@v2 24 | with: 25 | path: ~/.pnpm-store 26 | key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} 27 | restore-keys: | 28 | ${{ runner.os }}- 29 | 30 | - name: Install deps and build (with cache) 31 | uses: pnpm/action-setup@v2.0.1 32 | with: 33 | version: 6.0.2 34 | run_install: true 35 | 36 | - name: Build 37 | run: pnpm build 38 | -------------------------------------------------------------------------------- /packages/cyypher/src/util/object.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | /*! 4 | * Find the differences between two objects and push to a new object 5 | * (c) 2019 Chris Ferdinandi & Jascha Brinkmann, MIT License, https://gomakethings.com & https://twitter.com/jaschaio 6 | * @param {Object} obj1 The original object 7 | * @param {Object} obj2 The object to compare against it 8 | * @return {Object} An object of differences between the two 9 | */ 10 | 11 | export const diff = function (obj1, obj2, opts = { ignore: [] }) { 12 | const { ignore } = opts 13 | const changed = {} 14 | 15 | if (!obj2) return changed 16 | if (typeof obj1 !== 'object' || typeof obj2 !== 'object') { 17 | throw new Error('diff can only be used on two objects') 18 | } 19 | 20 | Object.entries(obj2).forEach(([k, v]) => { 21 | if (obj1[k] !== obj2[k] && !ignore.includes(k)) { 22 | changed[k] = v 23 | } 24 | }) 25 | 26 | return changed 27 | } 28 | 29 | export const clean = (obj) => { 30 | return _.pickBy(obj, function (value, key) { 31 | return !(value === undefined || value === null) 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rel", 3 | "private": true, 4 | "scripts": { 5 | "clean": "pnpm m run clean", 6 | "build": "pnpm m run build", 7 | "build:watch": "concurrently \"pnpm build:watch --dir packages/cyypher\" \"pnpm build:watch --dir packages/rel-cli\" \"pnpm build:watch --dir packages/rel-server\"", 8 | "dev": "pnpm m run dev", 9 | "release": "pnpx changeset version && pnpm publish -r --no-git-checks", 10 | "redis": "docker run -p 6379:6379 -it --rm redislabs/redisgraph" 11 | }, 12 | "oldscripts": { 13 | "bootstrap": "lerna bootstrap", 14 | "link": "lerna link", 15 | "build": "lerna run build", 16 | "build:watch": "lerna run build:watch", 17 | "test": "lerna run test -- --color", 18 | "test:ci": "lerna run test:ci -- --color", 19 | "test:coverage": "lerna run test:coverage", 20 | "release": "lerna run clean && lerna run build && lerna publish", 21 | "redis": "docker run -p 6379:6379 redislabs/redismod" 22 | }, 23 | "dependencies": { 24 | "concurrently": "^7.0.0" 25 | }, 26 | "devDependencies": { 27 | "@changesets/cli": "^2.21.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/rel-bundle/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rel-bundle", 3 | "version": "0.0.2", 4 | "description": "Visit https://rel.run/docs to view the full documentation.", 5 | "author": "Ian Hunter ", 6 | "license": "ISC", 7 | "type": "module", 8 | "exports": { 9 | "client": "./dist/client.js", 10 | "server": "./dist/server.js" 11 | }, 12 | "typesVersions": { 13 | "*": { 14 | "*": [ 15 | "dist/*" 16 | ] 17 | } 18 | }, 19 | "scripts": { 20 | "clean": "rm -rf ./dist", 21 | "build": "tsup", 22 | "build:watch": "tsup --watch" 23 | }, 24 | "tsup": { 25 | "entry": [ 26 | "src" 27 | ], 28 | "format": [ 29 | "esm" 30 | ], 31 | "legacy-output": true, 32 | "splitting": false, 33 | "sourcemap": true, 34 | "dts": true, 35 | "clean": true 36 | }, 37 | "dependencies": { 38 | "js-cookie": "^3.0.1", 39 | "rel-client": "workspace:^0.0.0", 40 | "rel-server": "workspace:^0.1.0" 41 | }, 42 | "devDependencies": { 43 | "ipjs": "^5.2.0", 44 | "tsup": "^5.11.13", 45 | "typescript": "^4.5.5" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/rel-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rel-server", 3 | "version": "0.1.0", 4 | "description": "", 5 | "author": "Ian Hunter ", 6 | "license": "ISC", 7 | "main": "dist/index.mjs", 8 | "scripts": { 9 | "clean": "rm -rf ./dist", 10 | "build": "tsup --format esm", 11 | "build:watch": "tsup --watch --format esm" 12 | }, 13 | "tsup": { 14 | "entry": [ 15 | "src" 16 | ], 17 | "splitting": false, 18 | "sourcemap": true, 19 | "dts": true, 20 | "clean": true 21 | }, 22 | "dependencies": { 23 | "@graphql-tools/merge": "^8.2.1", 24 | "@graphql-tools/schema": "^8.3.1", 25 | "@graphql-tools/utils": "^8.6.1", 26 | "@graphql-yoga/node": "^0.0.1-canary-ae94a57.0", 27 | "cyypher": "workspace:*", 28 | "fastify": "^3.24.0", 29 | "graphql": "^16.3.0", 30 | "graphql-compose": "^9.0.7", 31 | "graphql-playground-html": "^1.6.30", 32 | "graphql-scalars": "^1.13.5", 33 | "mercurius": "^8.9.1", 34 | "pluralize": "^8.0.0", 35 | "uuid": "^8.3.2" 36 | }, 37 | "devDependencies": { 38 | "tsup": "^5.11.13", 39 | "typescript": "^4.5.5" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/rel-client/src/index.ts: -------------------------------------------------------------------------------- 1 | import { generate } from "@genql/cli" 2 | import path from "path" 3 | import fsUtils from "fs-utils" 4 | 5 | type GenerateArgs = { 6 | schema: string 7 | outputPath: string 8 | } 9 | 10 | export async function generateClient(args: GenerateArgs) { 11 | const { schema, outputPath } = args 12 | 13 | await generate({ 14 | schema, 15 | output: path.join(outputPath, "generated"), 16 | headers: {}, 17 | sortProperties: true, 18 | }).catch(console.error) 19 | 20 | // @todo - generate top level client 21 | await fsUtils.writeFileSync( 22 | path.join(outputPath, "index.ts"), 23 | `// Autogenerated by Rel (https://rel.run). Do not modify this file directly. 24 | 25 | import { getStoredToken } from "rel-bundle/client"; 26 | import { createClient } from "./generated/index"; 27 | 28 | export * from "rel-bundle/client"; 29 | export * from "./generated/index"; 30 | 31 | const url = process.env.NEXT_PUBLIC_REL_URL || "http://localhost:4000/graphql" 32 | 33 | export default createClient({ 34 | url, 35 | headers: () => ({ 36 | Authorization: getStoredToken(), 37 | 'x-auth-token': getStoredToken(), 38 | }), 39 | });` 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /examples/basic/rel/client/generated/index.esm.js: -------------------------------------------------------------------------------- 1 | import { 2 | linkTypeMap, 3 | createClient as createClientOriginal, 4 | generateGraphqlOperation, 5 | assertSameVersion, 6 | } from '@genql/runtime' 7 | import types from './types.esm' 8 | var typeMap = linkTypeMap(types) 9 | export * from './guards.esm' 10 | 11 | export var version = '2.9.0' 12 | assertSameVersion(version) 13 | 14 | export var createClient = function(options) { 15 | options = options || {} 16 | var optionsCopy = { 17 | url: undefined, 18 | queryRoot: typeMap.Query, 19 | mutationRoot: typeMap.Mutation, 20 | subscriptionRoot: typeMap.Subscription, 21 | } 22 | for (var name in options) { 23 | optionsCopy[name] = options[name] 24 | } 25 | return createClientOriginal(optionsCopy) 26 | } 27 | 28 | export var generateQueryOp = function(fields) { 29 | return generateGraphqlOperation('query', typeMap.Query, fields) 30 | } 31 | export var generateMutationOp = function(fields) { 32 | return generateGraphqlOperation('mutation', typeMap.Mutation, fields) 33 | } 34 | export var generateSubscriptionOp = function(fields) { 35 | return generateGraphqlOperation('subscription', typeMap.Subscription, fields) 36 | } 37 | export var everything = { 38 | __scalar: true, 39 | } 40 | -------------------------------------------------------------------------------- /packages/rel-cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rel-cmd", 3 | "type": "module", 4 | "version": "0.1.0", 5 | "description": "Visit https://rel.run/docs to view the full documentation.", 6 | "repository": "https://github.com/rel-js/rel", 7 | "author": "Ian Hunter ", 8 | "license": "Apache-2.0", 9 | "main": "dist/cli.js", 10 | "bin": { 11 | "rel": "./dist/cli.js" 12 | }, 13 | "scripts": { 14 | "clean": "rm -rf ./dist", 15 | "build": "tsup --format esm", 16 | "build:watch": "tsup --watch --format esm" 17 | }, 18 | "tsup": { 19 | "entry": [ 20 | "src/cli.ts" 21 | ], 22 | "splitting": false, 23 | "dts": true, 24 | "sourcemap": true, 25 | "clean": true 26 | }, 27 | "dependencies": { 28 | "@ptkdev/logger": "^1.8.0", 29 | "chalk": "^5.0.0", 30 | "child_process": "^1.0.2", 31 | "chokidar": "^3.5.2", 32 | "commander": "^7.2.0", 33 | "debounce": "^1.2.1", 34 | "dotenv": "^8.2.0", 35 | "fs-utils": "^0.7.0", 36 | "graphql": "^16.3.0", 37 | "inquirer": "^8.2.0", 38 | "ora": "^5.3.0", 39 | "rel-client": "workspace:^0.0.0", 40 | "rel-server": "workspace:*", 41 | "slugify": "^1.6.5" 42 | }, 43 | "devDependencies": { 44 | "tsup": "^5.11.13" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/basic/rel/client/generated/index.js: -------------------------------------------------------------------------------- 1 | const { 2 | linkTypeMap, 3 | createClient: createClientOriginal, 4 | generateGraphqlOperation, 5 | assertSameVersion, 6 | } = require('@genql/runtime') 7 | var typeMap = linkTypeMap(require('./types.cjs')) 8 | 9 | var version = '2.9.0' 10 | assertSameVersion(version) 11 | 12 | module.exports.version = version 13 | 14 | module.exports.createClient = function(options) { 15 | options = options || {} 16 | var optionsCopy = { 17 | url: undefined, 18 | queryRoot: typeMap.Query, 19 | mutationRoot: typeMap.Mutation, 20 | subscriptionRoot: typeMap.Subscription, 21 | } 22 | for (var name in options) { 23 | optionsCopy[name] = options[name] 24 | } 25 | return createClientOriginal(optionsCopy) 26 | } 27 | 28 | module.exports.generateQueryOp = function(fields) { 29 | return generateGraphqlOperation('query', typeMap.Query, fields) 30 | } 31 | module.exports.generateMutationOp = function(fields) { 32 | return generateGraphqlOperation('mutation', typeMap.Mutation, fields) 33 | } 34 | module.exports.generateSubscriptionOp = function(fields) { 35 | return generateGraphqlOperation('subscription', typeMap.Subscription, fields) 36 | } 37 | module.exports.everything = { 38 | __scalar: true, 39 | } 40 | 41 | var schemaExports = require('./guards.cjs') 42 | for (var k in schemaExports) { 43 | module.exports[k] = schemaExports[k] 44 | } 45 | -------------------------------------------------------------------------------- /packages/cyypher/src/helpers/create.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuid } from "uuid" 2 | import { paramify } from "../util/params.js" 3 | import cleanPrefix from "../util/cleanPrefix.js" 4 | import { Node } from "src/types.js" 5 | 6 | export async function cypherCreate( 7 | label: string, 8 | params: Record, 9 | projection = [], 10 | opts = {} 11 | ): Promise { 12 | const toParams = { 13 | ...params, 14 | } 15 | 16 | const paramsCypher = paramify( 17 | { 18 | id: uuid(), 19 | createdAt: new Date().toISOString(), 20 | ...toParams, 21 | }, 22 | { 23 | ...opts, 24 | } 25 | ) 26 | 27 | let query = "" 28 | 29 | if (params.__unique) { 30 | query += `OPTIONAL MATCH (unique_node:${label} { __unique: "${params.__unique}"}) WITH unique_node WHERE unique_node IS NULL ` 31 | } 32 | 33 | query += ` 34 | CREATE (node:${label} { ${paramsCypher} }) 35 | RETURN ${ 36 | projection.length > 0 37 | ? projection.reduce( 38 | (previous, current, idx, arr) => 39 | previous + 40 | `node.${current}${idx === arr.length - 1 ? "" : ","}`, 41 | "" 42 | ) 43 | : "node" 44 | }; 45 | ` 46 | 47 | const res = await this.exec1(query) 48 | 49 | // if (opts.after) await opts.after(node) 50 | 51 | return projection.length > 0 ? cleanPrefix(res, "node.") : res?.node 52 | } 53 | -------------------------------------------------------------------------------- /packages/cyypher/src/util/buildWhereQuery.ts: -------------------------------------------------------------------------------- 1 | import { coerce } from "./coercion" 2 | 3 | const buildWhereQuery = (data = {}, opts = { prefix: "" }) => { 4 | let result = "" 5 | return Object.keys(data).reduce((querystring, key, idx, arr) => { 6 | if (["and", "or", "not"].includes(key.toLowerCase())) { 7 | result = querystring + (key.toLowerCase() === "not" ? "NOT" : "") 8 | if (Array.isArray(data[key])) { 9 | result += data[key].reduce((previous, current, idx, _arr) => { 10 | return ( 11 | previous + 12 | buildWhereQuery(current, opts) + 13 | (idx === _arr.length - 1 14 | ? ")" 15 | : key.toLowerCase() === "AND" 16 | ? " AND " 17 | : " OR ") 18 | ) 19 | }, "(") 20 | } else { 21 | result += "(" + buildWhereQuery(data[key], opts) + ")" 22 | } 23 | } else if (Array.isArray(data[key])) { 24 | result = querystring + data[key].join(" ") 25 | } else if ( 26 | typeof data[key] === "object" && 27 | Object.keys(data[key]).length > 0 28 | ) { 29 | result = 30 | querystring + opts.prefix + key + " " + buildWhereQuery(data[key], opts) 31 | } else { 32 | result = querystring + opts.prefix + key + " = " + coerce(data[key]) 33 | } 34 | result += idx === arr.length - 1 ? "" : " AND " 35 | return result 36 | }, "") 37 | } 38 | 39 | export default buildWhereQuery 40 | -------------------------------------------------------------------------------- /examples/basic/rel/client/generated/index.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FieldsSelection, 3 | GraphqlOperation, 4 | ClientOptions, 5 | Observable, 6 | } from '@genql/runtime' 7 | import { SubscriptionClient } from 'subscriptions-transport-ws' 8 | export * from './schema' 9 | import { 10 | QueryRequest, 11 | QueryPromiseChain, 12 | Query, 13 | MutationRequest, 14 | MutationPromiseChain, 15 | Mutation, 16 | } from './schema' 17 | export declare const createClient: (options?: ClientOptions) => Client 18 | export declare const everything: { __scalar: boolean } 19 | export declare const version: string 20 | 21 | export interface Client { 22 | wsClient?: SubscriptionClient 23 | 24 | query( 25 | request: R & { __name?: string }, 26 | ): Promise> 27 | 28 | mutation( 29 | request: R & { __name?: string }, 30 | ): Promise> 31 | 32 | chain: { 33 | query: QueryPromiseChain 34 | 35 | mutation: MutationPromiseChain 36 | } 37 | } 38 | 39 | export type QueryResult = FieldsSelection< 40 | Query, 41 | fields 42 | > 43 | 44 | export declare const generateQueryOp: ( 45 | fields: QueryRequest & { __name?: string }, 46 | ) => GraphqlOperation 47 | export type MutationResult = FieldsSelection< 48 | Mutation, 49 | fields 50 | > 51 | 52 | export declare const generateMutationOp: ( 53 | fields: MutationRequest & { __name?: string }, 54 | ) => GraphqlOperation 55 | -------------------------------------------------------------------------------- /packages/cyypher/src/helpers/updateMany.ts: -------------------------------------------------------------------------------- 1 | import { diff } from "../util/object.js" 2 | import { paramify } from "../util/params.js" 3 | import cleanPrefix from "../util/cleanPrefix.js" 4 | import buildWhereQuery from "../util/buildWhereQuery.js" 5 | import { Node } from "src/types.js" 6 | 7 | export async function cypherUpdateMany( 8 | label: string, 9 | where: object, 10 | params: Record, 11 | projection = [], 12 | opts = {} 13 | ): Promise { 14 | const toParams = diff({}, params, { 15 | ignore: ["id", "createdAt", "updatedAt", "__typename"], 16 | }) 17 | 18 | const paramsCypher = paramify( 19 | { updatedAt: new Date().toISOString(), ...toParams }, 20 | { 21 | ...opts, 22 | prefix: "node.", 23 | separator: "=", 24 | } 25 | ) 26 | 27 | const query = [] 28 | query.push(`MATCH (node:${label})`) 29 | 30 | if (typeof where === "object" && Object.keys(where).length > 0) { 31 | query.push(`WHERE ${buildWhereQuery(where, { prefix: "node." })}`) 32 | } 33 | 34 | query.push(`SET ${paramsCypher}`) 35 | query.push( 36 | `RETURN ${ 37 | projection.length > 0 38 | ? projection.reduce( 39 | (previous, current, idx, arr) => 40 | previous + `node.${current}${idx === arr.length - 1 ? "" : ","}`, 41 | "" 42 | ) 43 | : "node" 44 | }` 45 | ) 46 | 47 | const res = await this.exec(query.join(`\n`)) 48 | 49 | return projection.length > 0 50 | ? cleanPrefix(res, "node.") 51 | : res.map((x) => x.node) 52 | } 53 | -------------------------------------------------------------------------------- /packages/rel-server/src/schema/parser.ts: -------------------------------------------------------------------------------- 1 | import { visit, print } from "graphql" 2 | import { Fields, Relations } from "../types" 3 | 4 | type ParsedObject = { 5 | name: string 6 | fields: Fields 7 | relations: Relations 8 | } 9 | 10 | export function parseObject(definition): ParsedObject { 11 | const name = definition.name.value 12 | const fields = {} 13 | const relations = {} 14 | 15 | visit(definition, { 16 | FieldDefinition(node) { 17 | const directives = parseDirectives(node.directives) 18 | const relationDirective = directives.find((d) => d.name === "rel") 19 | const restDirectives = directives.find((d) => d.name !== "rel") 20 | 21 | const name = node.name.value 22 | const value = [print(node.type)] 23 | const isRelation = !!relationDirective 24 | 25 | if (isRelation) { 26 | relations[name] = { 27 | name, 28 | type: value.join(" "), 29 | relation: { 30 | ...relationDirective.args, 31 | type: value.join(" ").replace(/[^\w]/g, ""), 32 | }, 33 | directives: restDirectives, 34 | } 35 | } else { 36 | fields[name] = { 37 | name, 38 | type: value.join(" "), 39 | directives: restDirectives, 40 | } 41 | } 42 | }, 43 | }) 44 | 45 | return { 46 | name, 47 | fields, 48 | relations, 49 | } 50 | } 51 | 52 | export function parseDirectives(directives) { 53 | return directives.map((d) => { 54 | const args = d.arguments.reduce((acc, dir) => { 55 | acc[dir.name.value] = dir.value.value 56 | return acc 57 | }, {}) 58 | 59 | return { 60 | name: d.name.value, 61 | args, 62 | } 63 | }) 64 | } 65 | -------------------------------------------------------------------------------- /packages/cyypher/src/helpers/merge.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuid } from "uuid" 2 | import { paramify, setify } from "../util/params.js" 3 | import cleanPrefix from "../util/cleanPrefix.js" 4 | import { Node } from "src/types.js" 5 | 6 | const DEFAULT_CREATE_OPTS = { 7 | id: true, 8 | } 9 | 10 | const DEFAULT_UPDATE_OPTS = { 11 | id: false, 12 | } 13 | 14 | export async function cypherMerge( 15 | label: string, 16 | matchParams: Record, 17 | updateParams: Record, 18 | projection = [], 19 | opts = {} 20 | ): Promise { 21 | const matchCypher = paramify(matchParams, opts) 22 | const createCypher = setify( 23 | { 24 | id: uuid(), 25 | createdAt: new Date().toISOString(), 26 | ...matchParams, 27 | ...updateParams, 28 | }, 29 | { 30 | ...DEFAULT_CREATE_OPTS, 31 | ...opts, 32 | prefix: "node.", 33 | } 34 | ) 35 | const updateCypher = setify( 36 | { updatedAt: new Date().toISOString(), ...updateParams }, 37 | { 38 | ...DEFAULT_UPDATE_OPTS, 39 | ...opts, 40 | prefix: "node.", 41 | } 42 | ) 43 | 44 | const res = await this.exec1( 45 | ` 46 | MERGE (node:${label} { ${matchCypher} }) 47 | ON CREATE SET ${createCypher} 48 | ON MATCH SET ${updateCypher} 49 | RETURN ${ 50 | projection.length > 0 51 | ? projection.reduce( 52 | (previous, current, idx, arr) => 53 | previous + 54 | `node.${current}${idx === arr.length - 1 ? "" : ","}`, 55 | "" 56 | ) 57 | : "node" 58 | } 59 | LIMIT 1 60 | ` 61 | ) 62 | 63 | // if (opts.after) await opts.after(node) 64 | 65 | return projection.length > 0 ? cleanPrefix(res, "node.") : res?.node 66 | } 67 | -------------------------------------------------------------------------------- /packages/cyypher/src/helpers/update.ts: -------------------------------------------------------------------------------- 1 | import { diff } from "../util/object.js" 2 | import { paramify } from "../util/params.js" 3 | import cleanPrefix from "../util/cleanPrefix.js" 4 | import { Node } from "../types.js" 5 | import buildWhereQuery from "../util/buildWhereQuery.js" 6 | 7 | export async function cypherUpdate( 8 | label: string, 9 | where: object, 10 | params: Record, 11 | projection = [], 12 | opts = {} 13 | ): Promise { 14 | const toParams = diff({}, params, { 15 | ignore: ["id", "createdAt", "updatedAt", "__typename"], 16 | }) 17 | 18 | const paramsCypher = paramify( 19 | { 20 | updatedAt: new Date().toISOString(), 21 | ...toParams, 22 | }, 23 | { 24 | ...opts, 25 | prefix: "node.", 26 | separator: "=", 27 | } 28 | ) 29 | 30 | const query = [] 31 | query.push(`MATCH (node:${label})`) 32 | 33 | // if (params.__unique) { 34 | // query.push( 35 | // `OPTIONAL MATCH (unique_node:${label} { __unique: "${params.__unique}"}) WITH unique_node WHERE unique_node IS NULL ` 36 | // ) 37 | // } 38 | 39 | if (typeof where === "object" && Object.keys(where).length > 0) { 40 | query.push(`WHERE ${buildWhereQuery(where, { prefix: "node." })}`) 41 | } 42 | 43 | query.push(`SET ${paramsCypher}`) 44 | query.push( 45 | `RETURN ${ 46 | projection.length > 0 47 | ? projection.reduce( 48 | (previous, current, idx, arr) => 49 | previous + `node.${current}${idx === arr.length - 1 ? "" : ","}`, 50 | "" 51 | ) 52 | : "node" 53 | }` 54 | ) 55 | query.push("LIMIT 1") 56 | 57 | const res = await this.exec1(query.join(`\n`)) 58 | 59 | if (!res) 60 | throw new Error( 61 | `${label} not found${ 62 | params.__unique ? " or UNIQUE constraint violated" : "" 63 | }` 64 | ) 65 | 66 | return projection.length > 0 ? cleanPrefix(res, "node.") : res?.node 67 | } 68 | -------------------------------------------------------------------------------- /packages/cyypher/src/helpers/list.ts: -------------------------------------------------------------------------------- 1 | import buildWhereQuery from '../util/buildWhereQuery.js' 2 | import cleanPrefix from '../util/cleanPrefix.js' 3 | 4 | export async function cypherList (label, opts, projection = [], fieldArgs = {}) { 5 | const { order , skip, limit, where } = opts || {} 6 | 7 | const cypherQuery = [] 8 | 9 | const aggregations = ['sum', 'count', 'max', 'min', 'avg'] 10 | 11 | const agg = aggregations.find(agg => !!fieldArgs[agg]) 12 | 13 | const aggField = agg ? fieldArgs[agg]?.__arguments.find(a => !!a.of)?.of : null 14 | 15 | const isDistinct = agg ? fieldArgs[agg]?.__arguments.find(a => !!a.distinct)?.distinct?.value : false 16 | 17 | cypherQuery.push(`MATCH (node:${label})`) 18 | 19 | if (typeof where === 'object' && Object.keys(where).length > 0) { 20 | const whereQuery = buildWhereQuery(where, { prefix: 'node.' }) 21 | cypherQuery.push(`WHERE ${whereQuery}`) 22 | } 23 | 24 | const fields = projection.length > 0 ? projection.reduce((previous, current, idx, arr) => previous + `node.${current}${idx === arr.length - 1 ? '' : ','}`, '') : (aggField ? '' : 'node') 25 | cypherQuery.push(`RETURN ${fields + (fields !== '' && aggField ? ',' : '') + (aggField ? agg + '(' + (isDistinct ? 'DISTINCT ' : '') + 'node.' + aggField.value + ')' : '')}`) 26 | 27 | // order 28 | if (Array.isArray(order) && !aggField) { 29 | const orderFields = order.reduce((previous, current, idx, arr) => { 30 | return previous + `node.${current.field} ${current.order ?? "asc"}${idx === arr.length - 1 ? '' : ','}` 31 | }, "") 32 | cypherQuery.push(`ORDER BY ${orderFields}`) 33 | } 34 | 35 | // pagination 36 | if (skip && !aggField) cypherQuery.push(`SKIP ${skip}`) 37 | if (limit && !aggField) cypherQuery.push(`LIMIT ${limit}`) 38 | 39 | const query = cypherQuery.join(' ') 40 | 41 | const res = await this.exec(query) 42 | 43 | return (projection.length > 0 || aggField ? cleanPrefix(res, 'node.') : res.map(x => x.node)) 44 | } 45 | -------------------------------------------------------------------------------- /packages/cyypher/src/util/params.ts: -------------------------------------------------------------------------------- 1 | import { coerce } from './coercion.js' 2 | 3 | type ParmsBuilderOpts = { 4 | except?: any 5 | only?: any 6 | } 7 | 8 | export function paramsBuilder(params, opts: ParmsBuilderOpts = {}) { 9 | const { except = null, only = null } = opts 10 | 11 | const res = {} 12 | 13 | // Prune out if requested 14 | let fieldKeys = Object.keys(params) 15 | if (only) fieldKeys = fieldKeys.filter((k) => only.includes(k)) 16 | else if (except) fieldKeys = fieldKeys.filter((k) => !except.includes(k)) 17 | 18 | for (const key of fieldKeys) { 19 | res[key] = params[key] 20 | } 21 | 22 | return res 23 | } 24 | 25 | type ParamsToCypherOpts = { 26 | separator?: string 27 | join?: string 28 | prefix?: string 29 | } 30 | 31 | export function paramsToCypher(params, opts: ParamsToCypherOpts = {}) { 32 | const { separator = ':', join = ' , ', prefix = null } = opts 33 | 34 | function mapper(key) { 35 | const field = prefix ? `${prefix}${key}` : key 36 | const value = coerce(this[key]) 37 | return `${field} ${separator} ${value}` 38 | } 39 | 40 | return Object.keys(params).map(mapper, params).join(join) 41 | } 42 | 43 | // Converts { id: "1", name: "Ian" } => `id: 1, name: "Ian"` 44 | export function paramify(params, opts = {}) { 45 | return paramsToCypher(paramsBuilder(params, opts), opts) 46 | } 47 | 48 | // Converts { id: "1", name: "Ian" } => `id = 1 AND name = "Ian"` 49 | export function andify(params, opts = {}) { 50 | return paramsToCypher(paramsBuilder(params, opts), { 51 | separator: '=', 52 | join: ' AND ', 53 | ...opts, 54 | }) 55 | // return paramsToCypher(params, { separator: '=', join: ' AND ', ...opts }) 56 | } 57 | 58 | // Converts { id: "1", name: "Ian" } => `SET id = 1, SET name = "Ian"` 59 | export function setify(params, opts = {}) { 60 | return paramsToCypher(paramsBuilder(params, opts), { 61 | separator: '=', 62 | join: ', ', 63 | ...opts, 64 | }) 65 | // return paramsToCypher(params, { separator: '=', join: ', ', ...opts }) 66 | } 67 | -------------------------------------------------------------------------------- /packages/cyypher/src/types.ts: -------------------------------------------------------------------------------- 1 | export type Cypher1Response = { 2 | [key: string]: any 3 | } 4 | 5 | export type CypherResponse = Cypher1Response[] 6 | 7 | export type CypherNodeOpts = { 8 | name: string 9 | params?: object 10 | label?: string 11 | } 12 | 13 | export type CypherCreateOpts = { 14 | id?: boolean 15 | timestamps?: boolean 16 | } 17 | 18 | export type CypherMergeOpts = { 19 | id?: boolean 20 | timestamps?: boolean 21 | } 22 | 23 | export type CypherUpdateOpts = { 24 | id?: boolean 25 | timestamps?: boolean 26 | } 27 | 28 | export type CypherListAssociationOpts = { 29 | where?: string 30 | order?: string 31 | skip?: number 32 | limit?: number 33 | orderRaw?: string 34 | singular?: boolean 35 | } 36 | 37 | export type CypherCreateAssociationOpts = { 38 | singular?: boolean 39 | } 40 | 41 | export type CypherDeleteAssociationOpts = { 42 | // @todo cascading? 43 | } 44 | 45 | export type Cypher = { 46 | raw(cypher: string, opts?: QueryConfig): Promise 47 | exec(cypher: string, opts?: QueryConfig): Promise 48 | exec1(cypher: string, opts?: QueryConfig): Promise 49 | } 50 | 51 | export type RawResponse = { 52 | records: { 53 | _headers: any 54 | _values: any 55 | }[] 56 | } 57 | 58 | // A node must conform to the id and __typename 59 | export type NodeRef = { 60 | id?: string 61 | __typename: string 62 | } 63 | 64 | export type Node = { 65 | [propName: string]: any 66 | } & NodeRef 67 | 68 | export type Rel = { 69 | __direction?: RelationDirection 70 | __typename: string 71 | [propName: string]: any 72 | } 73 | 74 | export enum RelationDirection { 75 | IN = 'IN', 76 | OUT = 'OUT', 77 | NONE = 'NONE', 78 | } 79 | 80 | export type ConnectionLogger = (cypher: string, time: [number, number]) => void 81 | export type ConnectionConfig = { 82 | host: string 83 | port: number | string 84 | username: string 85 | password: string 86 | } & QueryConfig 87 | 88 | export type QueryConfig = { 89 | logger?: (cypher: any, time: [number, number]) => void 90 | } 91 | -------------------------------------------------------------------------------- /packages/rel-cli/src/commands/dev.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs" 2 | import chokidar from "chokidar" 3 | import debounce from "debounce" 4 | import ora from "ora" 5 | import Rel from "rel-server" 6 | import Generate from "./gen" 7 | import Logger from "@ptkdev/logger" 8 | 9 | let server 10 | 11 | const handleChange = debounce(async (opts) => { 12 | console.clear() 13 | 14 | if (server) { 15 | await server.kill() 16 | } 17 | 18 | const { dir, verbose } = opts 19 | const reloadingIndicator = ora("Starting Rel ...").start() 20 | 21 | let logger 22 | 23 | if (verbose) { 24 | logger = new Logger({ 25 | debug: true, 26 | write: true, 27 | 28 | // @todo - do we want to support file logging? 29 | // type: 'log', 30 | // path: { 31 | // // remember: add string *.log to .gitignore 32 | // debug_log: dir + '/logs/debug.log', 33 | // error_log: dir + '/logs/errors.log', 34 | // }, 35 | }) 36 | } 37 | 38 | const typeDefs = fs.readFileSync(dir + "/schema.graphql").toString() 39 | const connection = "redis://localhost:6379" 40 | 41 | server = new Rel({ 42 | typeDefs, 43 | connection, 44 | }) 45 | 46 | await Generate() 47 | 48 | const port = process.env.PORT || 4000 49 | 50 | server 51 | .listen(port) 52 | .then(({ port, generatedSchema }) => { 53 | reloadingIndicator?.succeed(`Rel running`) 54 | console.log() 55 | console.log(`GraphQL Playground: http://localhost:${port}`) 56 | console.log(`GraphQL Endpoint: http://localhost:${port}/graphql`) 57 | // console.log(generatedSchema) 58 | }) 59 | .catch((err) => { 60 | reloadingIndicator?.fail("Error during server start") 61 | console.error(err) 62 | }) 63 | }, 300) 64 | 65 | type Opts = { 66 | dir: string 67 | logging: boolean 68 | } 69 | 70 | export default (opts: Opts): void => { 71 | const currentDir = process.cwd() 72 | const config = fs.readFileSync(`${currentDir}/rel.config.json`).toString() 73 | const { baseDir } = JSON.parse(config) 74 | 75 | chokidar 76 | .watch(baseDir + "/schema.graphql", { persistent: true }) 77 | .on("all", () => 78 | handleChange({ 79 | dir: baseDir, 80 | ...opts, 81 | }) 82 | ) 83 | } 84 | -------------------------------------------------------------------------------- /packages/cyypher/src/helpers/relationships.ts: -------------------------------------------------------------------------------- 1 | import { cypherNode } from "./node.js" 2 | import { cypherRel } from "./rel.js" 3 | import buildWhereQuery from "../util/buildWhereQuery.js" 4 | 5 | export async function cypherListRelationship(from, rel, to, opts) { 6 | const fromCypher = cypherNode("from", from) 7 | const relCypher = cypherRel("rel", rel) 8 | const toCypher = cypherNode("to", to) 9 | 10 | const { 11 | singular, 12 | order, 13 | orderRaw, 14 | skip = 0, 15 | limit = null, 16 | where, 17 | } = opts || {} 18 | 19 | const cypherQuery = [] 20 | cypherQuery.push(`MATCH ${fromCypher}${relCypher}${toCypher}`) 21 | 22 | if (typeof where === "object" && Object.keys(where).length > 0) { 23 | const whereQuery = buildWhereQuery(where, { prefix: "to." }) 24 | cypherQuery.push(`WHERE ${whereQuery}`) 25 | } 26 | 27 | cypherQuery.push(`RETURN to`) 28 | if (skip) cypherQuery.push(`SKIP ${skip}`) 29 | 30 | const orderBy = orderRaw || `to.${order || "id"}` 31 | cypherQuery.push(`ORDER BY ${orderBy}`) 32 | 33 | if (singular) { 34 | cypherQuery.push(`LIMIT 1`) 35 | } else { 36 | if (limit) { 37 | cypherQuery.push(`LIMIT ${limit}`) 38 | } 39 | } 40 | 41 | if (singular) { 42 | return this.exec1(cypherQuery.join("\n")).then((r) => r?.to) 43 | } else { 44 | return this.exec(cypherQuery.join("\n")).then((res) => 45 | res.map((r) => r?.to) 46 | ) 47 | } 48 | } 49 | 50 | export async function cypherClearRelation(from, rel) { 51 | const fromCypher = cypherNode("from", from) 52 | const relCypher = cypherRel("rel", rel) 53 | 54 | return this.exec(` 55 | MATCH ${fromCypher} 56 | OPTIONAL MATCH (from)${relCypher}() 57 | DELETE rel 58 | RETURN from; 59 | `) 60 | } 61 | 62 | export async function cypherCreateRelationship(from, rel, to, opts) { 63 | const { singular } = opts || {} 64 | 65 | const fromCypher = cypherNode("from", from) 66 | const toCypher = cypherNode("to", to) 67 | const relCypher = cypherRel("rel", rel) 68 | 69 | if (singular) { 70 | await this.clearRelationship(from, rel) 71 | } 72 | 73 | return this.exec1(` 74 | MATCH ${fromCypher}, ${toCypher} 75 | MERGE (from)${relCypher}(to) 76 | RETURN from, rel, to; 77 | `) 78 | } 79 | 80 | export async function cypherDeleteRelationship( 81 | from, 82 | rel, 83 | to 84 | // opts?: CypherDeleteAssociationOpts 85 | ) { 86 | const fromCypher = cypherNode("from", from) 87 | const toCypher = cypherNode("to", to) 88 | const relCypher = cypherRel("rel", rel) 89 | 90 | return this.exec1(` 91 | MATCH ${fromCypher}, ${toCypher} 92 | MATCH (from)${relCypher}(to) 93 | DELETE rel 94 | RETURN from, rel, to; 95 | `) 96 | } 97 | -------------------------------------------------------------------------------- /packages/rel-server/src/index.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema } from "graphql" 2 | import { Client } from "cyypher" 3 | import { Readable } from "stream" 4 | import Fastify, { FastifyInstance } from "fastify" 5 | import { createServer } from "@graphql-yoga/node" 6 | import { renderPlaygroundPage } from "graphql-playground-html" 7 | 8 | import { addResolversToSchema } from "@graphql-tools/schema" 9 | import { makeAugmentedSchema } from "./schema/makeAugmentedSchema" 10 | import { printSchema } from "graphql-compose" 11 | 12 | type Config = { 13 | typeDefs: string 14 | resolvers?: object 15 | connection: string 16 | } 17 | 18 | export default class Server { 19 | private connection: string 20 | private typeDefs: string 21 | private resolvers: object 22 | 23 | private app: FastifyInstance 24 | 25 | // private _nodes?: Node[] 26 | // private _relationships?: Relationship[] 27 | 28 | constructor(config: Config) { 29 | const { connection, typeDefs, resolvers } = config 30 | this.connection = connection 31 | this.typeDefs = typeDefs 32 | this.resolvers = resolvers 33 | this.app = Fastify({ 34 | trustProxy: true, 35 | }) 36 | } 37 | 38 | private async generateSchema(): Promise { 39 | const augmentedSchema = makeAugmentedSchema(this.typeDefs) 40 | const resolvers = {} 41 | 42 | return addResolversToSchema(augmentedSchema, resolvers) 43 | } 44 | 45 | async listen(port: number | string) { 46 | const schema = await this.generateSchema() 47 | const cypher = new Client(this.connection) 48 | 49 | const graphQLServer = createServer({ 50 | schema, 51 | context: async (context: any) => { 52 | return { 53 | cypher, 54 | ...context, 55 | } 56 | }, 57 | logging: { 58 | prettyLog: false, 59 | logLevel: "info", 60 | }, 61 | }) 62 | 63 | this.app.route({ 64 | url: "/graphql", 65 | method: ["POST", "OPTIONS"], 66 | handler: async (req, reply) => { 67 | const response = await graphQLServer.handleIncomingMessage(req) 68 | response.headers.forEach((value, key) => { 69 | reply.header(key, value) 70 | }) 71 | const nodeStream = Readable.from(response.body as any) 72 | reply.status(response.status).send(nodeStream) 73 | }, 74 | }) 75 | 76 | // GraphQL Playground 77 | this.app.get("/", async (_, reply) => { 78 | reply.headers({ 79 | "Content-Type": "text/html", 80 | }) 81 | 82 | reply.send( 83 | renderPlaygroundPage({ 84 | endpoint: `/graphql`, 85 | }) 86 | ) 87 | reply.status(200) 88 | }) 89 | 90 | return this.app.listen(port).then(() => { 91 | const generatedSchema = printSchema(schema) 92 | return { 93 | port, 94 | generatedSchema, 95 | } 96 | }) 97 | } 98 | 99 | async kill() { 100 | this.app.close() 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /examples/basic/rel/client/generated/schema.graphql: -------------------------------------------------------------------------------- 1 | scalar DateTime 2 | 3 | type Genre { 4 | createdAt: DateTime! 5 | id: ID! 6 | movies(limit: Int, skip: Int, where: MovieWhere): [Movie]! 7 | name: String 8 | updatedAt: DateTime 9 | } 10 | 11 | input GenreCreateInput { 12 | name: String 13 | } 14 | 15 | input GenreUpdateInput { 16 | name: String 17 | } 18 | 19 | input GenreWhere { 20 | AND: [GenreWhere!] 21 | NOT: [GenreWhere!] 22 | OR: [GenreWhere!] 23 | id: ID 24 | name: String 25 | } 26 | 27 | type Movie { 28 | createdAt: DateTime! 29 | genres(limit: Int, skip: Int, where: GenreWhere): [Genre]! 30 | id: ID! 31 | rating: Float 32 | title: String! 33 | updatedAt: DateTime 34 | year: Int 35 | } 36 | 37 | input MovieCreateInput { 38 | rating: Float 39 | title: String! 40 | year: Int 41 | } 42 | 43 | input MovieUpdateInput { 44 | rating: Float 45 | title: String 46 | year: Int 47 | } 48 | 49 | input MovieWhere { 50 | AND: [MovieWhere!] 51 | NOT: [MovieWhere!] 52 | OR: [MovieWhere!] 53 | id: ID 54 | rating: Float 55 | title: String 56 | year: Int 57 | } 58 | 59 | type Mutation { 60 | """Create a single Genre using 'data' values""" 61 | createGenre(data: GenreCreateInput!): Genre 62 | 63 | """Create a single Movie using 'data' values""" 64 | createMovie(data: MovieCreateInput!): Movie 65 | 66 | """Delete one Genre by 'where', returns node if found otherwise null""" 67 | deleteGenre(where: GenreWhere!): Genre 68 | 69 | """Delete multiple Genres by 'where', returns number of nodes deleted""" 70 | deleteManyGenre(where: GenreWhere!): Int! 71 | 72 | """Delete multiple Movies by 'where', returns number of nodes deleted""" 73 | deleteManyMovie(where: MovieWhere!): Int! 74 | 75 | """Delete one Movie by 'where', returns node if found otherwise null""" 76 | deleteMovie(where: MovieWhere!): Movie 77 | 78 | """ 79 | Merge will find or create a Genre matching 'where', if found will update using data, if not found will create using data + where 80 | """ 81 | mergeGenre(data: GenreUpdateInput, where: GenreWhere!): Genre 82 | 83 | """ 84 | Merge will find or create a Movie matching 'where', if found will update using data, if not found will create using data + where 85 | """ 86 | mergeMovie(data: MovieUpdateInput, where: MovieWhere!): Movie 87 | 88 | """Update first Genre matching 'where'""" 89 | updateGenre(data: GenreUpdateInput!, where: GenreWhere!): Genre 90 | 91 | """ 92 | Update multiple Genres matching 'where', sets all nodes to 'data' values 93 | """ 94 | updateManyGenre(data: GenreUpdateInput!, where: GenreWhere!): [Genre]! 95 | 96 | """ 97 | Update multiple Movies matching 'where', sets all nodes to 'data' values 98 | """ 99 | updateManyMovie(data: MovieUpdateInput!, where: MovieWhere!): [Movie]! 100 | 101 | """Update first Movie matching 'where'""" 102 | updateMovie(data: MovieUpdateInput!, where: MovieWhere!): Movie 103 | } 104 | 105 | type Query { 106 | """Count number of Genre nodes matching 'where'""" 107 | countGenres(where: GenreWhere): Int! 108 | 109 | """Count number of Movie nodes matching 'where'""" 110 | countMovies(where: MovieWhere): Int! 111 | 112 | """Find multiple Genres matching 'where'""" 113 | findManyGenres(limit: Int, offset: Int, where: GenreWhere): [Genre]! 114 | 115 | """Find multiple Movies matching 'where'""" 116 | findManyMovies(limit: Int, offset: Int, where: MovieWhere): [Movie]! 117 | 118 | """Find one Genre matching 'where'""" 119 | findOneGenre(where: GenreWhere): Genre 120 | 121 | """Find one Movie matching 'where'""" 122 | findOneMovie(where: MovieWhere): Movie 123 | } -------------------------------------------------------------------------------- /packages/rel-cli/src/commands/init.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs" 2 | import fsUtils from "fs-utils" 3 | import slugify from "slugify" 4 | import { execSync } from "child_process" 5 | import chalk from "chalk" 6 | import ora from "ora" 7 | import inquirer from "inquirer" 8 | 9 | const { version } = require("../../package.json") 10 | 11 | export default async function InitCommand() { 12 | console.log(` 13 | ___ _ 14 | | _ \\___| | 15 | | / -_) | 16 | |_|_\\___|_| Installer 17 | 18 | Rel is the zero-config backend framework for Javascripters. 19 | `) 20 | 21 | const dir = process.cwd() 22 | 23 | // default installation path 24 | // let projectDir = `${dir}/rel` 25 | 26 | const isNPM = fs.existsSync("./package-lock.json") 27 | const isYarn = fs.existsSync("./yarn.lock") 28 | const isPNPM = fs.existsSync("./pnpm-lock-yaml") 29 | const isExistingProject = isNPM || isYarn || isPNPM 30 | 31 | let cmd = "npm" 32 | let projectDir = "." 33 | 34 | if (isExistingProject) { 35 | console.log("Project found in current directory") 36 | 37 | switch (true) { 38 | case isNPM: 39 | cmd = "npm" 40 | break 41 | case isYarn: 42 | cmd = "yarn" 43 | break 44 | case isPNPM: 45 | cmd = "pnpm" 46 | break 47 | } 48 | } else { 49 | const { projectName } = await inquirer.prompt([ 50 | { 51 | type: "input", 52 | name: "projectName", 53 | message: "What's the name of your project?", 54 | default: "api", 55 | }, 56 | ]) 57 | 58 | const projectSlug = slugify(projectName, { 59 | remove: /[!@#$%^&*()_+|}{:"?><\[\];',./}]/g, 60 | }) 61 | projectDir = `${dir}/${projectSlug}` 62 | fs.mkdirSync(projectDir) 63 | process.chdir(projectDir) 64 | 65 | await fsUtils.writeFileSync( 66 | `./package.json`, 67 | ` 68 | { 69 | "name": "${projectName}", 70 | "version": "0.0.1", 71 | "description": "", 72 | "main": "index.js", 73 | "license": "MIT", 74 | "scripts": { 75 | "dev": "rel dev", 76 | "redis": "docker run -p 6379:6379 redislabs/redismod" 77 | }, 78 | "devDependencies": {}, 79 | "dependencies": {} 80 | }` 81 | ) 82 | } 83 | 84 | console.log() 85 | const initializing = ora(`Installing Rel packages`).start() 86 | execSync(`${cmd} add -D rel-cmd@latest`) 87 | execSync(`${cmd} add rel-bundle@latest`) 88 | initializing.succeed(`Rel packages installed`) 89 | 90 | console.log() 91 | const { path } = await inquirer.prompt([ 92 | { 93 | type: "input", 94 | name: "path", 95 | message: "Base directory for Rel project?", 96 | default: isExistingProject ? "./rel" : ".", 97 | }, 98 | ]) 99 | 100 | await fsUtils.writeFileSync( 101 | `${projectDir}/rel.config.json`, 102 | `{ 103 | "baseDir": "${path}" 104 | } 105 | ` 106 | ) 107 | 108 | await fsUtils.writeFileSync( 109 | `${path}/schema.graphql`, 110 | ` 111 | type Movie { 112 | title: String! 113 | year: Int 114 | rating: Float 115 | genres: [Genre]! @rel(label: "IN_GENRE", direction: OUT) 116 | } 117 | 118 | type Genre { 119 | name: String 120 | movies: [Movie]! @rel(label: "IN_GENRE", direction: IN) 121 | } 122 | 123 | ` 124 | ) 125 | 126 | console.log() 127 | console.log() 128 | console.log( 129 | `Success! Created reljs at directory`, 130 | chalk.greenBright(projectDir) 131 | ) 132 | console.log(`Next steps:`) 133 | console.log() 134 | console.log( 135 | "1. Run " + 136 | chalk.blueBright("rel dev") + 137 | " and visit https://localhost:4000" 138 | ) 139 | console.log( 140 | "2. Edit " + 141 | chalk.greenBright(`${path}/schema.graphql`) + 142 | " to update your schema." 143 | ) 144 | console.log("3. Read more documentation at https://rel.run/docs") 145 | console.log() 146 | } 147 | 148 | type Question = { 149 | type: string 150 | name: string 151 | message: string 152 | default?: string 153 | } 154 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # rel.js 8 | 9 | [Rel](https://rel.run) is a zero-config backend framework for the frontend of your choice. We've combined [GraphQL](https://graphql.org) with the deep relational capabilities of [redis-graph](https://github.com/RedisGraph/RedisGraph/). 10 | 11 | Rel is the end-to-end backend framework: 12 | 13 | - [x] Schema driven (`schema.graphql`) 14 | - [x] CRUD + custom endpoints 15 | - [x] Auto-generated client (TypeScript) 16 | - [ ] Standard and polymorphic relationships 17 | - [ ] Authentication and authorization 18 | - [ ] Realtime subscriptions 19 | - [ ] Delayed + scheduled jobs 20 | - [x] Event streams / hooks 21 | - [ ] Plugins + Extensions 22 | - [ ] Hosting (later 2022) 23 | 24 | ## Quickstart 25 | 26 | Install Rel to your existing project, or create a new one: 27 | 28 | ```sh 29 | npx rel-cmd@latest init 30 | ``` 31 | 32 | Afterwards, Then run `rel dev` to start the server on [http://localhost:4000](http://localhost:4000). 33 | 34 | Other things available to you after install: 35 | 36 | - Visit http://localhost:4000/ for a GraphQL playground. 37 | - Make queries to your GraphQL server at http://localhost:4000/graphql. 38 | - Edit `rel/schema.graphql` (or ./schema.graphql for standalone) to change your schema and generated client. 39 | 40 | 41 | 42 | 43 | ## Community 44 | 45 | - [Github Discussions](https://github.com/rel-js/rel/discussions) - Want to suggest a feature? Open a discussion. 46 | - [GitHub Issues](https://github.com/rel-js/rel/issues) - Report issues + errors while using rel.js. 47 | - [Twitter](https://twitter.com/rel_js) - Help spread the word and give us a follow! 48 | 49 | ## Roadmap 50 | 51 | - [x] Alpha (current): We are activelly developing Rel and not recommending for production yet. 52 | - [ ] Public Alpha (Q2 2022): [Public release](https://github.com/orgs/rel-js/projects/2) for curious developers and hobby projects. 53 | - [ ] Public Beta: Stable enough for most non-enterprise use-cases 54 | - [ ] Public: Production-ready 55 | 56 | 64 | 65 | ## Development 66 | 67 | Rel uses [pnpm](https://pnpm.io/) as our package manager and we love it. 68 | 69 | - pnpm install 70 | - pnpm run build 71 | - pnpm run bootstrap:redis 72 | 73 | 74 | 75 | 80 | 81 | 105 | -------------------------------------------------------------------------------- /ideas.txt: -------------------------------------------------------------------------------- 1 | { 2 | schema: { 3 | Author: { 4 | fields: { 5 | name: string(), 6 | }, 7 | }, 8 | }, 9 | jobs: { 10 | hourlyDoCalculations: { 11 | cron: "...", 12 | resolver: (job) => { 13 | ... 14 | } 15 | } 16 | } 17 | } 18 | 19 | 20 | ========= 21 | old 22 | 23 | // types 24 | 25 | const typeExample = { 26 | Author: { 27 | fields: { 28 | name: string().required(), 29 | optional: string() 30 | }, 31 | } 32 | } 33 | 34 | // generates GQL 35 | 36 | type Author { 37 | id: UUID! 38 | name: String!mark 39 | optional: String 40 | createdAt: DateTime! 41 | updatedAt: DateTime! 42 | } 43 | 44 | // ################################################################## 45 | // listing 46 | 47 | const readingExample = { 48 | 49 | } 50 | 51 | // generates GQL 52 | 53 | type Author { 54 | id: UUID! 55 | name: String! 56 | createdAt: DateTime! 57 | updatedAt: DateTime! 58 | } 59 | 60 | type Query { 61 | Authors(limit: Int, skip: Int, order: String): [Author]! 62 | } 63 | 64 | // ################################################################## 65 | // finding 66 | 67 | const findingExample = { 68 | Author: { 69 | fields: { 70 | name: string().required(), 71 | }, 72 | endpoints: { 73 | find: true 74 | } 75 | } 76 | } 77 | 78 | // generates GQL 79 | 80 | type Author { 81 | name: String! 82 | } 83 | 84 | type Query { 85 | Author(id: UUID!): Author 86 | } 87 | 88 | // ################################################################## 89 | // reading with mutiple match fields 90 | 91 | const multipleFieldsFindingExample = { 92 | Author: { 93 | fields: { 94 | name: string().required(), 95 | }, 96 | endpoints: { 97 | find: { 98 | match: ["id", "name"] 99 | } 100 | } 101 | } 102 | } 103 | 104 | // generates GQL 105 | 106 | type Author { 107 | name: String! 108 | } 109 | 110 | type Query { 111 | Author(id: UUID, name: String): Author 112 | } 113 | 114 | // ################################################################## 115 | // Mutability 116 | 117 | const mutabilityExample = { 118 | Author: { 119 | fields: { 120 | name: string().required(), 121 | }, 122 | endpoints: { 123 | create: true, 124 | merge: true, 125 | update: true, 126 | delete: true, 127 | } 128 | } 129 | } 130 | 131 | // generates GQL 132 | 133 | type Author { 134 | name: String! 135 | } 136 | 137 | type Query { 138 | CreateAuthor(fields: AuthorInput!): Author 139 | MergeAuthor(id: UUID!, fields: AuthorInput!): Author 140 | UpdateAuthor(id: UUID!, fields: AuthorInput!): Author 141 | DeleteAuthor(id: UUID!): Author 142 | } 143 | 144 | // ################################################################## 145 | // Associations 146 | 147 | const associationsExample = { 148 | Author: { 149 | endpoints: { 150 | find: { 151 | match: ["id"], 152 | }, 153 | }, 154 | fields: { 155 | name: string().required(), 156 | }, 157 | relations: { 158 | firstBook: { 159 | from: { 160 | label: "User", 161 | params: ({ obj }) => ({ id: obj.id }), 162 | }, 163 | rel: { 164 | label: "AUTHORED", 165 | }, 166 | to: { 167 | label: "Book", 168 | }, 169 | singular: true, 170 | order: "to.createdAt DESC", 171 | }, 172 | books: { 173 | from: { 174 | label: "User", 175 | params: ({ obj }) => ({ id: obj.id }), 176 | }, 177 | rel: { 178 | label: "AUTHORED", 179 | }, 180 | to: { 181 | label: "Book", 182 | }, 183 | }, 184 | }, 185 | }, 186 | Book: { 187 | fields: { 188 | name: string().required(), 189 | }, 190 | }, 191 | } 192 | 193 | // Generates GraphQL 194 | 195 | type Author { 196 | name: String! 197 | firstBook: Book 198 | books: [Book]! 199 | } 200 | 201 | type Book { 202 | name: String! 203 | } 204 | 205 | type Query { 206 | Author(id: String!): Author 207 | } 208 | 209 | type Mutation { 210 | AddAuthorBook(from: UUID, to: UUID): Book 211 | } 212 | 213 | // ################################################################## 214 | // Polymorphism 215 | 216 | const polymorphicExample = { 217 | Author: { 218 | fields: { 219 | name: string(), 220 | }, 221 | }, 222 | Book: { 223 | fields: { 224 | name: string(), 225 | }, 226 | }, 227 | Person: { 228 | endpoints: { 229 | find: true, 230 | }, 231 | fields: { 232 | name: string(), 233 | }, 234 | relations: { 235 | favorites: { 236 | from: { 237 | label: "Person", 238 | params: ({ obj }) => ({ id: obj.id }), 239 | }, 240 | rel: { 241 | label: "FAVORITE", 242 | }, 243 | to: { 244 | label: ["Restaurant", "Guide"], 245 | }, 246 | fields: { 247 | bucket: string(), 248 | }, 249 | }, 250 | }, 251 | }, 252 | } 253 | 254 | // Generates GQL 255 | 256 | type Author { 257 | name: String! 258 | } 259 | 260 | type Book { 261 | name: String! 262 | } 263 | 264 | type Query { 265 | Author(id: String!): Author 266 | } 267 | 268 | type Mutation { 269 | AddAuthorBook(from: UUID, to: UUID): Book 270 | } 271 | -------------------------------------------------------------------------------- /packages/cyypher/src/index.ts: -------------------------------------------------------------------------------- 1 | import { URL } from "url" 2 | import lodash from "lodash" 3 | import { Graph } from "redisgraph.js" 4 | import { cypherCreate } from "./helpers/create.js" 5 | import { cypherDelete } from "./helpers/delete.js" 6 | import { cypherDeleteMany } from "./helpers/deleteMany.js" 7 | import { cypherFind } from "./helpers/find.js" 8 | import { cypherFindOrCreate } from "./helpers/findOrCreate.js" 9 | import { cypherList } from "./helpers/list.js" 10 | import { cypherMerge } from "./helpers/merge.js" 11 | import { cypherUpdate } from "./helpers/update.js" 12 | import { cypherUpdateMany } from "./helpers/updateMany.js" 13 | import { cypherCount } from "./helpers/count.js" 14 | 15 | import { 16 | cypherClearRelation, 17 | cypherCreateRelationship, 18 | cypherDeleteRelationship, 19 | cypherListRelationship, 20 | } from "./helpers/relationships.js" 21 | 22 | import { beautifyCypher } from "./util/beautify.js" 23 | import { sanitize } from "./util/sanitize.js" 24 | 25 | import logger from "./logger.js" 26 | 27 | export function ref(node) { 28 | const { __typename, id } = node 29 | return { __typename, id } 30 | } 31 | 32 | function parseConnection(conn: ConnectionOpts) { 33 | if (typeof conn === "string") { 34 | const url = new URL(conn) 35 | let auth = null 36 | if (!lodash.isEmpty(url.username) && !lodash.isEmpty(url.password)) { 37 | auth = { 38 | username: url.username, 39 | password: url.password, 40 | } 41 | } 42 | return { 43 | host: url.hostname, 44 | port: url.port, 45 | auth, 46 | } 47 | } else { 48 | return conn 49 | } 50 | } 51 | 52 | type Connection = { 53 | host: string 54 | port: string 55 | auth?: { 56 | username: string 57 | password: string 58 | } 59 | } 60 | 61 | type ConnectionOpts = string | Connection 62 | export class Client { 63 | connection = null 64 | 65 | find = cypherFind.bind(this) 66 | list = cypherList.bind(this) 67 | count = cypherCount.bind(this) 68 | create = cypherCreate.bind(this) 69 | merge = cypherMerge.bind(this) 70 | update = cypherUpdate.bind(this) 71 | updateMany = cypherUpdateMany.bind(this) 72 | findOrCreate = cypherFindOrCreate.bind(this) 73 | delete = cypherDelete.bind(this) 74 | deleteMany = cypherDeleteMany.bind(this) 75 | 76 | listRelationship = cypherListRelationship.bind(this) 77 | createRelationship = cypherCreateRelationship.bind(this) 78 | clearRelationship = cypherClearRelation.bind(this) 79 | deleteRelationship = cypherDeleteRelationship.bind(this) 80 | 81 | ref = ref 82 | 83 | constructor(conn: ConnectionOpts) { 84 | this.connection = parseConnection(conn) 85 | } 86 | 87 | async raw(cypher, opts = {}, tries = 0) { 88 | const graph = new Graph( 89 | "graph", 90 | this.connection.host, 91 | this.connection.port, 92 | this.connection.auth 93 | ) 94 | 95 | const startTime = process.hrtime() 96 | 97 | try { 98 | const res = await graph.query(cypher) 99 | const time = process.hrtime(startTime) 100 | if (logger) 101 | logger.debug( 102 | "\n" + 103 | beautifyCypher(cypher) + 104 | "\n" + 105 | "Took [" + 106 | (time[0] * 1000000000 + time[1]) / 1000000 + 107 | "ms]" 108 | // 'CYPHER' 109 | ) 110 | 111 | return { 112 | records: res._results, 113 | } 114 | } catch (err) { 115 | // if (tries < 2) { 116 | // return this.raw(cypher, opts, tries + 1) 117 | // } 118 | 119 | const time = process.hrtime(startTime) 120 | logger.error( 121 | beautifyCypher(cypher) + 122 | "\n" + 123 | err + 124 | " [" + 125 | (time[0] * 1000000000 + time[1]) / 1000000 + 126 | "ms]" 127 | // 'CYPHER' 128 | ) 129 | return { 130 | records: [], 131 | } 132 | } finally { 133 | graph.close() 134 | } 135 | } 136 | 137 | async exec(query, opts = {}) { 138 | const res = await this.raw(query, opts) 139 | 140 | const recordMapper = (rec) => { 141 | const res = {} 142 | 143 | rec._header.forEach((varName, i) => { 144 | const node = rec._values[i] 145 | 146 | if (node?.constructor.name === "Node") { 147 | const properties = node?.properties 148 | const mapped = { ...properties } 149 | if (node.label) mapped.__typename = node.label 150 | res[varName] = sanitize(mapped) 151 | } else if (node?.constructor.name === "Array") { 152 | res[varName] = node.map((n) => { 153 | const mapped = { ...n.properties } 154 | if (n.label) mapped.__typename = n.label 155 | return sanitize(mapped) 156 | }) 157 | } else { 158 | res[varName] = node 159 | } 160 | }) 161 | 162 | return res 163 | } 164 | 165 | return res?.records?.map(recordMapper) 166 | } 167 | 168 | async exec1(query, opts) { 169 | const res = await this.exec(query, opts) 170 | return res && res[0] 171 | } 172 | 173 | deleteAll() { 174 | const query = "MATCH (n) DETACH DELETE n" 175 | logger.debug(query, "CYPHER") 176 | return this.exec(query) 177 | } 178 | } 179 | 180 | export default new Client( 181 | process.env.REDIS_URL || { 182 | host: process.env.REDIS_HOST, 183 | port: process.env.REDIS_PORT, 184 | auth: { 185 | username: process.env.REDIS_USERNAME, 186 | password: process.env.REDIS_PASSWORD, 187 | }, 188 | } 189 | ) 190 | -------------------------------------------------------------------------------- /packages/rel-server/src/schema/makeAugmentedSchema.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DirectiveDefinitionNode, 3 | EnumTypeDefinitionNode, 4 | InputObjectTypeDefinitionNode, 5 | InterfaceTypeDefinitionNode, 6 | ObjectTypeDefinitionNode, 7 | ScalarTypeDefinitionNode, 8 | } from "graphql" 9 | import { mergeTypeDefs } from "@graphql-tools/merge" 10 | import { pluralize, SchemaComposer } from "graphql-compose" 11 | 12 | import { parseObject } from "./parser" 13 | import { fieldsToComposer } from "./fields" 14 | 15 | import * as Resolvers from "../resolvers" 16 | import Scalars from "../scalars" 17 | import { Relation } from "../types" 18 | 19 | export function makeAugmentedSchema(typeDefs: string) { 20 | const composer = new SchemaComposer() 21 | const document = mergeTypeDefs(typeDefs) 22 | 23 | // const scalars = document.definitions.filter( 24 | // (x) => x.kind === "ScalarTypeDefinition" 25 | // ) as ScalarTypeDefinitionNode[] 26 | 27 | Object.keys(Scalars).forEach((scalar) => 28 | composer.addTypeDefs(`scalar ${scalar}`) 29 | ) 30 | 31 | const objectNodes = document.definitions.filter( 32 | (x) => 33 | x.kind === "ObjectTypeDefinition" && 34 | !["Query", "Mutation", "Subscription"].includes(x.name.value) 35 | ) as ObjectTypeDefinitionNode[] 36 | 37 | const enums = document.definitions.filter( 38 | (x) => x.kind === "EnumTypeDefinition" 39 | ) as EnumTypeDefinitionNode[] 40 | 41 | const inputs = document.definitions.filter( 42 | (x) => x.kind === "InputObjectTypeDefinition" 43 | ) as InputObjectTypeDefinitionNode[] 44 | 45 | let interfaces = document.definitions.filter( 46 | (x) => x.kind === "InterfaceTypeDefinition" 47 | ) as InterfaceTypeDefinitionNode[] 48 | 49 | const directives = document.definitions.filter( 50 | (x) => x.kind === "DirectiveDefinition" 51 | ) as DirectiveDefinitionNode[] 52 | 53 | // const unions = document.definitions.filter( 54 | // (x) => x.kind === "UnionTypeDefinition" 55 | // ) as UnionTypeDefinitionNode[] 56 | 57 | // console.log({ 58 | // scalars, 59 | // objectNodes, 60 | // enums, 61 | // inputs, 62 | // interfaces, 63 | // directives, 64 | // unions, 65 | // }) 66 | 67 | const nodes = objectNodes.map((definition) => { 68 | const { name, fields, relations } = parseObject(definition) 69 | 70 | // console.log("objectNodes.map", { name, fields, relations }) 71 | 72 | const objectType = composer.createObjectTC({ 73 | name, 74 | fields: { 75 | id: "ID!", 76 | createdAt: "DateTime!", 77 | updatedAt: "DateTime", 78 | ...fieldsToComposer(fields), 79 | }, 80 | }) 81 | 82 | const whereInput = composer.createInputTC({ 83 | name: `${name}Where`, 84 | fields: { 85 | id: "ID", 86 | ...fieldsToComposer(fields, { optional: true }), 87 | AND: `[${name}Where!]`, 88 | OR: `[${name}Where!]`, 89 | NOT: `[${name}Where!]`, 90 | }, 91 | }) 92 | 93 | Object.values(relations).forEach((rel: Relation) => { 94 | const { name, type, relation } = rel 95 | const args = { 96 | where: `${relation.type}Where`, 97 | skip: "Int", 98 | limit: "Int", 99 | } 100 | 101 | objectType.addFields({ 102 | [name]: { 103 | type: `[${relation.type}]!`, 104 | args, 105 | resolve: Resolvers.relationResolver(rel), 106 | }, 107 | }) 108 | }) 109 | 110 | // Queries 111 | 112 | composer.Query.addFields({ 113 | [`findOne${name}`]: { 114 | args: { 115 | where: whereInput, 116 | }, 117 | description: `Find one ${name} matching 'where'`, 118 | type: name, 119 | resolve: Resolvers.findOneResolver(name), 120 | }, 121 | }) 122 | 123 | composer.Query.addFields({ 124 | [`findMany${pluralize(name)}`]: { 125 | args: { 126 | where: whereInput, 127 | offset: "Int", 128 | limit: "Int", 129 | }, 130 | description: `Find multiple ${pluralize(name)} matching 'where'`, 131 | type: objectType.List.NonNull, 132 | resolve: Resolvers.findManyResolver(name), 133 | }, 134 | }) 135 | 136 | composer.Query.addFields({ 137 | [`count${pluralize(name)}`]: { 138 | args: { 139 | where: whereInput, 140 | }, 141 | description: `Count number of ${name} nodes matching 'where'`, 142 | type: `Int!`, 143 | resolve: Resolvers.countResolver(name), 144 | }, 145 | }) 146 | 147 | // Mutations 148 | 149 | const createInput = composer.createInputTC({ 150 | name: `${name}CreateInput`, 151 | fields: fieldsToComposer(fields), 152 | }) 153 | 154 | const updateInput = composer.createInputTC({ 155 | name: `${name}UpdateInput`, 156 | fields: fieldsToComposer(fields, { optional: true }), 157 | }) 158 | 159 | composer.Mutation.addFields({ 160 | [`create${name}`]: { 161 | args: { 162 | data: createInput.NonNull, 163 | }, 164 | description: `Create a single ${name} using 'data' values`, 165 | type: objectType, 166 | resolve: Resolvers.createResolver(name), 167 | }, 168 | }) 169 | 170 | // @todo 171 | // composer.Mutation.addFields({ 172 | // [`createMany${name}`]: { 173 | // args: { 174 | // where: whereInput.NonNull, 175 | // data: updateInput.NonNull.List.NonNull, 176 | // }, 177 | // // description: ``, 178 | // type: name, 179 | // resolve: Resolvers.updateManyResolver(name), 180 | // }, 181 | // }) 182 | 183 | composer.Mutation.addFields({ 184 | [`update${name}`]: { 185 | args: { 186 | where: whereInput.NonNull, 187 | data: updateInput.NonNull, 188 | }, 189 | description: `Update first ${name} matching 'where'`, 190 | type: objectType, 191 | resolve: Resolvers.updateResolver(name), 192 | }, 193 | }) 194 | 195 | composer.Mutation.addFields({ 196 | [`updateMany${name}`]: { 197 | args: { 198 | where: whereInput.NonNull, 199 | data: updateInput.NonNull, 200 | }, 201 | description: `Update multiple ${pluralize( 202 | name 203 | )} matching 'where', sets all nodes to 'data' values`, 204 | type: objectType.List.NonNull, 205 | resolve: Resolvers.updateManyResolver(name), 206 | }, 207 | }) 208 | 209 | composer.Mutation.addFields({ 210 | [`merge${name}`]: { 211 | args: { 212 | where: whereInput.NonNull, 213 | data: updateInput, 214 | }, 215 | description: `Merge will find or create a ${name} matching 'where', if found will update using data, if not found will create using data + where`, 216 | type: objectType, 217 | resolve: Resolvers.mergeResolver(name), 218 | }, 219 | }) 220 | 221 | composer.Mutation.addFields({ 222 | [`delete${name}`]: { 223 | args: { 224 | where: whereInput.NonNull, 225 | }, 226 | description: `Delete one ${name} by 'where', returns node if found otherwise null`, 227 | type: objectType, 228 | resolve: Resolvers.deleteResolver(name), 229 | }, 230 | }) 231 | 232 | composer.Mutation.addFields({ 233 | [`deleteMany${name}`]: { 234 | args: { 235 | where: whereInput.NonNull, 236 | }, 237 | description: `Delete multiple ${pluralize( 238 | name 239 | )} by 'where', returns number of nodes deleted`, 240 | type: "Int!", 241 | resolve: Resolvers.deleteManyResolver(name), 242 | }, 243 | }) 244 | }) 245 | 246 | const sortDirection = composer.createEnumTC({ 247 | name: "SortDirection", 248 | values: { 249 | ASC: { 250 | value: "ASC", 251 | description: "Sort by field values in ascending order.", 252 | }, 253 | DESC: { 254 | value: "DESC", 255 | description: "Sort by field values in descending order.", 256 | }, 257 | }, 258 | }) 259 | 260 | return composer.buildSchema() 261 | } 262 | -------------------------------------------------------------------------------- /packages/cyypher/README.md: -------------------------------------------------------------------------------- 1 | # cyypher 2 | 3 | Cypher ORM + client for redis-graph. 4 | 5 | ## Inspiration 6 | 7 | This project powers the [Rel](https://github.com/rel-js/rel) backend framework. We needed a more robust, relational client for redis-graph. 8 | 9 | If you are looking for a more complete schema -> GraphQL server, definitely take a look at our framework. 10 | 11 | ## Quickstart 12 | 13 | ### 1. Install npm package 14 | 15 | Add the cyypher npm package via your package manager of choice. 16 | 17 | ```sh 18 | npm install cyypher 19 | ``` 20 | 21 | ```sh 22 | yarn add cyypher 23 | ``` 24 | 25 | ```sh 26 | pnpm add cyypher 27 | ``` 28 | 29 | ### 2. Initialize the client 30 | 31 | ``` 32 | import cyypher from "cyypher" 33 | ``` 34 | 35 | or with custom connection 36 | 37 | ``` 38 | import { Client } from "cyypher" 39 | 40 | const cyypher = new Client({ 41 | host: "...", 42 | port: 1234, 43 | auth: { 44 | username: "redis", 45 | password: "1234 46 | } 47 | }) 48 | ``` 49 | 50 | ## Usage 51 | 52 | ### `find(label, where)` 53 | 54 | Finds a single node by label and params. 55 | 56 | ```ts 57 | cyypher.find('Person', { _id: '123' }) 58 | cyypher.find('Person', { name: 'Ian' }) 59 | ``` 60 | 61 | ### `list(label, where)` 62 | 63 | List multiple nodes by label and params. 64 | 65 | ```ts 66 | // List everyone 67 | cyypher.list('Person', {}) 68 | 69 | // List only admins 70 | cyypher.list('Person', { where: { admin: true } }) 71 | ``` 72 | 73 | ### `count(label, where)` 74 | 75 | Count number of matching nodes. 76 | 77 | ```ts 78 | // total count of a label 79 | cyypher.count('Person') 80 | 81 | // with where params 82 | cyypher.count('Person', { where: { admin: true } }) 83 | ``` 84 | 85 | ### `create(label, where)` 86 | 87 | ```ts 88 | cyypher.create('Person', { name: 'Inigo Montoya' }) 89 | ``` 90 | 91 | Ensure uniqueness across a field: 92 | 93 | ```ts 94 | cyypher.create('Person', { name: 'Inigo Montoya' }) 95 | 96 | // this call is idempotent 97 | cyypher.create('Person', { name: 'Inigo Montoya', __unique: 'name' }) 98 | ``` 99 | 100 | ### `findOrCreate(label, where, updateParams)` 101 | 102 | A find() and then create() call that will return an existing node if found. 103 | 104 | ```ts 105 | cyypher.findOrCreate('Person', { name: 'Inigo Montoya' }) 106 | // this won't create a new node 107 | cyypher.findOrCreate('Person', { name: 'Inigo Montoya' }) 108 | // this will create a new node 109 | cyypher.findOrCreate('Person', { name: 'Vizzini' }) 110 | ``` 111 | 112 | Optional: `create` params: 113 | 114 | ```ts 115 | cyypher.findOrCreate( 116 | 'Person', 117 | { _id: '1xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' }, 118 | { name: 'Inigo Montoya' } 119 | ) 120 | ``` 121 | 122 | > Note: It is not necessary to re-specify find params in the create params, the two will be merged together. 123 | 124 | ### `merge(label, where, updateParams)` 125 | 126 | Similar to `findOrCreate` but uses cypher's native merge command: 127 | 128 | ```ts 129 | cyypher.merge('Person', { name: 'Inigo Montoya' }) 130 | ``` 131 | 132 | ### `update(label, id, updateParams)` 133 | 134 | Update a node based on ID. 135 | 136 | ```ts 137 | cyypher.merge('Person', '123', { name: 'Inigo Montoya' }) 138 | ``` 139 | 140 | ### `updateBy(label, where, updateParams)` 141 | 142 | Update multiple nodes by params. 143 | 144 | ```ts 145 | cyypher.updateBy( 146 | 'Person', 147 | { name: 'Inigo Montoya' }, 148 | { name: 'Mandy Patinkin' } 149 | ) 150 | ``` 151 | 152 | ### `delete(label, id)` 153 | 154 | Delete a node by ID. 155 | 156 | ```ts 157 | cyypher.delete('Person', '123') 158 | ``` 159 | 160 | ### `deleteBy(label, params)` 161 | 162 | Delete multiple nodes by params. 163 | 164 | ```ts 165 | cyypher.deleteBy('Person', { name: 'Inigo Montoya' }) 166 | ``` 167 | 168 | ### `listRelationship(from, rel, to, opts)` 169 | 170 | List relationships between nodes. 171 | 172 | | Param | Type | Required | Description | Default | 173 | | :--------- | :----------------------------- | -------- | ------------------------------ | ------- | 174 | | `from` | `string` \| `object` \| `Node` | yes | From node | 175 | | `rel` | `string` \| `object` \| `Node` | yes | Relationship | 176 | | `to` | `string` \| `object` \| `Node` | yes | To node | 177 | | Options | | | | | 178 | | `singular` | `boolean` | | Singular relationship? | false | 179 | | `skip` | `number` | | Skip offset | 0 | 180 | | `limit` | `number` | | Number of results | 181 | | `order` | `object` | | Order the results | 182 | | `orderRaw` | `string` | | Direct order string to pass in | 183 | 184 | ```ts 185 | // List N-1 between many nodes 186 | cyypher.listRelationship('Person', 'FRIEND', { _id: '456' }) 187 | 188 | // List all FRIEND relationships between Persons 189 | cyypher.listRelationship('Person', 'FRIEND', 'Person') 190 | 191 | // List 1-1 between two nodes 192 | cyypher.listRelationship({ _id: '123' }, 'FRIEND', { _id: '456' }) 193 | 194 | // You can also pass a node instance 195 | import { ref } from 'cyypher' 196 | const fromNode = await cyypher.find('Person', 'ID') 197 | const toNode = await cyypher.find('Person', 'ID2') 198 | 199 | cyypher.listRelationship(ref(fromNode), 'FRIEND', ref(toNode)) 200 | 201 | // List 1-1 between two nodes with types 202 | cyypher.listRelationship({ __typename: 'Person', _id: '123' }, 'FRIEND', { 203 | __typename: 'Person', 204 | _id: '456', 205 | }) 206 | 207 | // List 1-1 between two nodes with relation params 208 | cyypher.listRelationship( 209 | { _id: '123' }, 210 | { __typename: 'FRIEND', relation: 'close', metAt: new Date() }, 211 | { _id: '456' } 212 | ) 213 | 214 | // List directed relationship (IN, OUT, NONE) 215 | cyypher.listRelationship( 216 | { _id: '123' }, 217 | { __typename: 'FRIEND', __direction: 'OUT' }, 218 | { _id: '456' } 219 | ) 220 | ``` 221 | 222 | ### `createRelationship(from, rel, to, opts)` 223 | 224 | Create relationship(s) between two or more nodes. 225 | 226 | | Param | Type | Required | Description | Default | 227 | | :--------- | :----------------------------- | -------- | ---------------------- | ------- | 228 | | `from` | `string` \| `object` \| `Node` | yes | From node | 229 | | `rel` | `string` \| `object` \| `Node` | yes | Relationship | 230 | | `to` | `string` \| `object` \| `Node` | yes | To node | 231 | | Options | | | | | 232 | | `singular` | `boolean` | | Singular relationship? | false | 233 | 234 | ```ts 235 | // Using params 236 | cyypher.createRelationship({ _id: '123' }, 'FRIEND', { _id: '456' }) 237 | 238 | // Using node references 239 | import { ref } from 'cyypher' 240 | const fromNode = await cyypher.find('Person', 'ID') 241 | const toNode = await cyypher.find('Person', 'ID2') 242 | cyypher.createRelationship(ref(fromNode), 'FRIEND', ref(toNode)) 243 | 244 | // Singular 245 | cyypher.createRelationship( 246 | { _id: '123' }, 247 | 'FRIEND', 248 | { _id: '456' }, 249 | { singular: true } 250 | ) 251 | ``` 252 | 253 | ### `clearRelationship()` 254 | 255 | Clear all relationships between two or more nodes. 256 | 257 | | Param | Type | Required | Description | Default | 258 | | :----- | :----------------------------- | -------- | ------------ | ------- | 259 | | `from` | `string` \| `object` \| `Node` | yes | From node | 260 | | `rel` | `string` \| `object` \| `Node` | yes | Relationship | 261 | 262 | ```ts 263 | // Using params 264 | cyypher.clearRelationship({ _id: '123' }, 'FRIEND') 265 | ``` 266 | 267 | ### `deleteRelationship()` 268 | 269 | Delete relationship(s) between two or more nodes. 270 | 271 | | Param | Type | Required | Description | Default | 272 | | :----- | :----------------------------- | -------- | ------------ | ------- | 273 | | `from` | `string` \| `object` \| `Node` | yes | From node | 274 | | `rel` | `string` \| `object` \| `Node` | yes | Relationship | 275 | | `to` | `string` \| `object` \| `Node` | yes | To node | 276 | 277 | ```ts 278 | // Using params 279 | cyypher.deleteRelationship({ _id: '123' }, 'FRIEND', { _id: '456' }) 280 | 281 | // Using node references 282 | import { ref } from 'cyypher' 283 | const fromNode = await cyypher.find('Person', 'ID') 284 | const toNode = await cyypher.find('Person', 'ID2') 285 | cyypher.deleteRelationship(ref(fromNode), 'FRIEND', ref(toNode)) 286 | ``` 287 | -------------------------------------------------------------------------------- /examples/basic/rel/client/generated/types.cjs.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "scalars": [ 3 | 0, 4 | 1, 5 | 2, 6 | 7, 7 | 8, 8 | 15 9 | ], 10 | "types": { 11 | "Boolean": {}, 12 | "DateTime": {}, 13 | "Float": {}, 14 | "Genre": { 15 | "createdAt": [ 16 | 1 17 | ], 18 | "id": [ 19 | 7 20 | ], 21 | "movies": [ 22 | 9, 23 | { 24 | "limit": [ 25 | 8 26 | ], 27 | "skip": [ 28 | 8 29 | ], 30 | "where": [ 31 | 12 32 | ] 33 | } 34 | ], 35 | "name": [ 36 | 15 37 | ], 38 | "updatedAt": [ 39 | 1 40 | ], 41 | "__typename": [ 42 | 15 43 | ] 44 | }, 45 | "GenreCreateInput": { 46 | "name": [ 47 | 15 48 | ], 49 | "__typename": [ 50 | 15 51 | ] 52 | }, 53 | "GenreUpdateInput": { 54 | "name": [ 55 | 15 56 | ], 57 | "__typename": [ 58 | 15 59 | ] 60 | }, 61 | "GenreWhere": { 62 | "AND": [ 63 | 6 64 | ], 65 | "NOT": [ 66 | 6 67 | ], 68 | "OR": [ 69 | 6 70 | ], 71 | "id": [ 72 | 7 73 | ], 74 | "name": [ 75 | 15 76 | ], 77 | "__typename": [ 78 | 15 79 | ] 80 | }, 81 | "ID": {}, 82 | "Int": {}, 83 | "Movie": { 84 | "createdAt": [ 85 | 1 86 | ], 87 | "genres": [ 88 | 3, 89 | { 90 | "limit": [ 91 | 8 92 | ], 93 | "skip": [ 94 | 8 95 | ], 96 | "where": [ 97 | 6 98 | ] 99 | } 100 | ], 101 | "id": [ 102 | 7 103 | ], 104 | "rating": [ 105 | 2 106 | ], 107 | "title": [ 108 | 15 109 | ], 110 | "updatedAt": [ 111 | 1 112 | ], 113 | "year": [ 114 | 8 115 | ], 116 | "__typename": [ 117 | 15 118 | ] 119 | }, 120 | "MovieCreateInput": { 121 | "rating": [ 122 | 2 123 | ], 124 | "title": [ 125 | 15 126 | ], 127 | "year": [ 128 | 8 129 | ], 130 | "__typename": [ 131 | 15 132 | ] 133 | }, 134 | "MovieUpdateInput": { 135 | "rating": [ 136 | 2 137 | ], 138 | "title": [ 139 | 15 140 | ], 141 | "year": [ 142 | 8 143 | ], 144 | "__typename": [ 145 | 15 146 | ] 147 | }, 148 | "MovieWhere": { 149 | "AND": [ 150 | 12 151 | ], 152 | "NOT": [ 153 | 12 154 | ], 155 | "OR": [ 156 | 12 157 | ], 158 | "id": [ 159 | 7 160 | ], 161 | "rating": [ 162 | 2 163 | ], 164 | "title": [ 165 | 15 166 | ], 167 | "year": [ 168 | 8 169 | ], 170 | "__typename": [ 171 | 15 172 | ] 173 | }, 174 | "Mutation": { 175 | "createGenre": [ 176 | 3, 177 | { 178 | "data": [ 179 | 4, 180 | "GenreCreateInput!" 181 | ] 182 | } 183 | ], 184 | "createMovie": [ 185 | 9, 186 | { 187 | "data": [ 188 | 10, 189 | "MovieCreateInput!" 190 | ] 191 | } 192 | ], 193 | "deleteGenre": [ 194 | 3, 195 | { 196 | "where": [ 197 | 6, 198 | "GenreWhere!" 199 | ] 200 | } 201 | ], 202 | "deleteManyGenre": [ 203 | 8, 204 | { 205 | "where": [ 206 | 6, 207 | "GenreWhere!" 208 | ] 209 | } 210 | ], 211 | "deleteManyMovie": [ 212 | 8, 213 | { 214 | "where": [ 215 | 12, 216 | "MovieWhere!" 217 | ] 218 | } 219 | ], 220 | "deleteMovie": [ 221 | 9, 222 | { 223 | "where": [ 224 | 12, 225 | "MovieWhere!" 226 | ] 227 | } 228 | ], 229 | "mergeGenre": [ 230 | 3, 231 | { 232 | "data": [ 233 | 5 234 | ], 235 | "where": [ 236 | 6, 237 | "GenreWhere!" 238 | ] 239 | } 240 | ], 241 | "mergeMovie": [ 242 | 9, 243 | { 244 | "data": [ 245 | 11 246 | ], 247 | "where": [ 248 | 12, 249 | "MovieWhere!" 250 | ] 251 | } 252 | ], 253 | "updateGenre": [ 254 | 3, 255 | { 256 | "data": [ 257 | 5, 258 | "GenreUpdateInput!" 259 | ], 260 | "where": [ 261 | 6, 262 | "GenreWhere!" 263 | ] 264 | } 265 | ], 266 | "updateManyGenre": [ 267 | 3, 268 | { 269 | "data": [ 270 | 5, 271 | "GenreUpdateInput!" 272 | ], 273 | "where": [ 274 | 6, 275 | "GenreWhere!" 276 | ] 277 | } 278 | ], 279 | "updateManyMovie": [ 280 | 9, 281 | { 282 | "data": [ 283 | 11, 284 | "MovieUpdateInput!" 285 | ], 286 | "where": [ 287 | 12, 288 | "MovieWhere!" 289 | ] 290 | } 291 | ], 292 | "updateMovie": [ 293 | 9, 294 | { 295 | "data": [ 296 | 11, 297 | "MovieUpdateInput!" 298 | ], 299 | "where": [ 300 | 12, 301 | "MovieWhere!" 302 | ] 303 | } 304 | ], 305 | "__typename": [ 306 | 15 307 | ] 308 | }, 309 | "Query": { 310 | "countGenres": [ 311 | 8, 312 | { 313 | "where": [ 314 | 6 315 | ] 316 | } 317 | ], 318 | "countMovies": [ 319 | 8, 320 | { 321 | "where": [ 322 | 12 323 | ] 324 | } 325 | ], 326 | "findManyGenres": [ 327 | 3, 328 | { 329 | "limit": [ 330 | 8 331 | ], 332 | "offset": [ 333 | 8 334 | ], 335 | "where": [ 336 | 6 337 | ] 338 | } 339 | ], 340 | "findManyMovies": [ 341 | 9, 342 | { 343 | "limit": [ 344 | 8 345 | ], 346 | "offset": [ 347 | 8 348 | ], 349 | "where": [ 350 | 12 351 | ] 352 | } 353 | ], 354 | "findOneGenre": [ 355 | 3, 356 | { 357 | "where": [ 358 | 6 359 | ] 360 | } 361 | ], 362 | "findOneMovie": [ 363 | 9, 364 | { 365 | "where": [ 366 | 12 367 | ] 368 | } 369 | ], 370 | "__typename": [ 371 | 15 372 | ] 373 | }, 374 | "String": {} 375 | } 376 | } -------------------------------------------------------------------------------- /examples/basic/rel/client/generated/types.esm.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "scalars": [ 3 | 0, 4 | 1, 5 | 2, 6 | 7, 7 | 8, 8 | 15 9 | ], 10 | "types": { 11 | "Boolean": {}, 12 | "DateTime": {}, 13 | "Float": {}, 14 | "Genre": { 15 | "createdAt": [ 16 | 1 17 | ], 18 | "id": [ 19 | 7 20 | ], 21 | "movies": [ 22 | 9, 23 | { 24 | "limit": [ 25 | 8 26 | ], 27 | "skip": [ 28 | 8 29 | ], 30 | "where": [ 31 | 12 32 | ] 33 | } 34 | ], 35 | "name": [ 36 | 15 37 | ], 38 | "updatedAt": [ 39 | 1 40 | ], 41 | "__typename": [ 42 | 15 43 | ] 44 | }, 45 | "GenreCreateInput": { 46 | "name": [ 47 | 15 48 | ], 49 | "__typename": [ 50 | 15 51 | ] 52 | }, 53 | "GenreUpdateInput": { 54 | "name": [ 55 | 15 56 | ], 57 | "__typename": [ 58 | 15 59 | ] 60 | }, 61 | "GenreWhere": { 62 | "AND": [ 63 | 6 64 | ], 65 | "NOT": [ 66 | 6 67 | ], 68 | "OR": [ 69 | 6 70 | ], 71 | "id": [ 72 | 7 73 | ], 74 | "name": [ 75 | 15 76 | ], 77 | "__typename": [ 78 | 15 79 | ] 80 | }, 81 | "ID": {}, 82 | "Int": {}, 83 | "Movie": { 84 | "createdAt": [ 85 | 1 86 | ], 87 | "genres": [ 88 | 3, 89 | { 90 | "limit": [ 91 | 8 92 | ], 93 | "skip": [ 94 | 8 95 | ], 96 | "where": [ 97 | 6 98 | ] 99 | } 100 | ], 101 | "id": [ 102 | 7 103 | ], 104 | "rating": [ 105 | 2 106 | ], 107 | "title": [ 108 | 15 109 | ], 110 | "updatedAt": [ 111 | 1 112 | ], 113 | "year": [ 114 | 8 115 | ], 116 | "__typename": [ 117 | 15 118 | ] 119 | }, 120 | "MovieCreateInput": { 121 | "rating": [ 122 | 2 123 | ], 124 | "title": [ 125 | 15 126 | ], 127 | "year": [ 128 | 8 129 | ], 130 | "__typename": [ 131 | 15 132 | ] 133 | }, 134 | "MovieUpdateInput": { 135 | "rating": [ 136 | 2 137 | ], 138 | "title": [ 139 | 15 140 | ], 141 | "year": [ 142 | 8 143 | ], 144 | "__typename": [ 145 | 15 146 | ] 147 | }, 148 | "MovieWhere": { 149 | "AND": [ 150 | 12 151 | ], 152 | "NOT": [ 153 | 12 154 | ], 155 | "OR": [ 156 | 12 157 | ], 158 | "id": [ 159 | 7 160 | ], 161 | "rating": [ 162 | 2 163 | ], 164 | "title": [ 165 | 15 166 | ], 167 | "year": [ 168 | 8 169 | ], 170 | "__typename": [ 171 | 15 172 | ] 173 | }, 174 | "Mutation": { 175 | "createGenre": [ 176 | 3, 177 | { 178 | "data": [ 179 | 4, 180 | "GenreCreateInput!" 181 | ] 182 | } 183 | ], 184 | "createMovie": [ 185 | 9, 186 | { 187 | "data": [ 188 | 10, 189 | "MovieCreateInput!" 190 | ] 191 | } 192 | ], 193 | "deleteGenre": [ 194 | 3, 195 | { 196 | "where": [ 197 | 6, 198 | "GenreWhere!" 199 | ] 200 | } 201 | ], 202 | "deleteManyGenre": [ 203 | 8, 204 | { 205 | "where": [ 206 | 6, 207 | "GenreWhere!" 208 | ] 209 | } 210 | ], 211 | "deleteManyMovie": [ 212 | 8, 213 | { 214 | "where": [ 215 | 12, 216 | "MovieWhere!" 217 | ] 218 | } 219 | ], 220 | "deleteMovie": [ 221 | 9, 222 | { 223 | "where": [ 224 | 12, 225 | "MovieWhere!" 226 | ] 227 | } 228 | ], 229 | "mergeGenre": [ 230 | 3, 231 | { 232 | "data": [ 233 | 5 234 | ], 235 | "where": [ 236 | 6, 237 | "GenreWhere!" 238 | ] 239 | } 240 | ], 241 | "mergeMovie": [ 242 | 9, 243 | { 244 | "data": [ 245 | 11 246 | ], 247 | "where": [ 248 | 12, 249 | "MovieWhere!" 250 | ] 251 | } 252 | ], 253 | "updateGenre": [ 254 | 3, 255 | { 256 | "data": [ 257 | 5, 258 | "GenreUpdateInput!" 259 | ], 260 | "where": [ 261 | 6, 262 | "GenreWhere!" 263 | ] 264 | } 265 | ], 266 | "updateManyGenre": [ 267 | 3, 268 | { 269 | "data": [ 270 | 5, 271 | "GenreUpdateInput!" 272 | ], 273 | "where": [ 274 | 6, 275 | "GenreWhere!" 276 | ] 277 | } 278 | ], 279 | "updateManyMovie": [ 280 | 9, 281 | { 282 | "data": [ 283 | 11, 284 | "MovieUpdateInput!" 285 | ], 286 | "where": [ 287 | 12, 288 | "MovieWhere!" 289 | ] 290 | } 291 | ], 292 | "updateMovie": [ 293 | 9, 294 | { 295 | "data": [ 296 | 11, 297 | "MovieUpdateInput!" 298 | ], 299 | "where": [ 300 | 12, 301 | "MovieWhere!" 302 | ] 303 | } 304 | ], 305 | "__typename": [ 306 | 15 307 | ] 308 | }, 309 | "Query": { 310 | "countGenres": [ 311 | 8, 312 | { 313 | "where": [ 314 | 6 315 | ] 316 | } 317 | ], 318 | "countMovies": [ 319 | 8, 320 | { 321 | "where": [ 322 | 12 323 | ] 324 | } 325 | ], 326 | "findManyGenres": [ 327 | 3, 328 | { 329 | "limit": [ 330 | 8 331 | ], 332 | "offset": [ 333 | 8 334 | ], 335 | "where": [ 336 | 6 337 | ] 338 | } 339 | ], 340 | "findManyMovies": [ 341 | 9, 342 | { 343 | "limit": [ 344 | 8 345 | ], 346 | "offset": [ 347 | 8 348 | ], 349 | "where": [ 350 | 12 351 | ] 352 | } 353 | ], 354 | "findOneGenre": [ 355 | 3, 356 | { 357 | "where": [ 358 | 6 359 | ] 360 | } 361 | ], 362 | "findOneMovie": [ 363 | 9, 364 | { 365 | "where": [ 366 | 12 367 | ] 368 | } 369 | ], 370 | "__typename": [ 371 | 15 372 | ] 373 | }, 374 | "String": {} 375 | } 376 | } -------------------------------------------------------------------------------- /examples/basic/rel/client/generated/schema.ts: -------------------------------------------------------------------------------- 1 | import {FieldsSelection,Observable} from '@genql/runtime' 2 | 3 | export type Scalars = { 4 | Boolean: boolean, 5 | DateTime: any, 6 | Float: number, 7 | ID: string, 8 | Int: number, 9 | String: string, 10 | } 11 | 12 | export interface Genre { 13 | createdAt: Scalars['DateTime'] 14 | id: Scalars['ID'] 15 | movies: (Movie | undefined)[] 16 | name?: Scalars['String'] 17 | updatedAt?: Scalars['DateTime'] 18 | __typename: 'Genre' 19 | } 20 | 21 | export interface Movie { 22 | createdAt: Scalars['DateTime'] 23 | genres: (Genre | undefined)[] 24 | id: Scalars['ID'] 25 | rating?: Scalars['Float'] 26 | title: Scalars['String'] 27 | updatedAt?: Scalars['DateTime'] 28 | year?: Scalars['Int'] 29 | __typename: 'Movie' 30 | } 31 | 32 | export interface Mutation { 33 | /** Create a single Genre using 'data' values */ 34 | createGenre?: Genre 35 | /** Create a single Movie using 'data' values */ 36 | createMovie?: Movie 37 | /** Delete one Genre by 'where', returns node if found otherwise null */ 38 | deleteGenre?: Genre 39 | /** Delete multiple Genres by 'where', returns number of nodes deleted */ 40 | deleteManyGenre: Scalars['Int'] 41 | /** Delete multiple Movies by 'where', returns number of nodes deleted */ 42 | deleteManyMovie: Scalars['Int'] 43 | /** Delete one Movie by 'where', returns node if found otherwise null */ 44 | deleteMovie?: Movie 45 | /** Merge will find or create a Genre matching 'where', if found will update using data, if not found will create using data + where */ 46 | mergeGenre?: Genre 47 | /** Merge will find or create a Movie matching 'where', if found will update using data, if not found will create using data + where */ 48 | mergeMovie?: Movie 49 | /** Update first Genre matching 'where' */ 50 | updateGenre?: Genre 51 | /** Update multiple Genres matching 'where', sets all nodes to 'data' values */ 52 | updateManyGenre: (Genre | undefined)[] 53 | /** Update multiple Movies matching 'where', sets all nodes to 'data' values */ 54 | updateManyMovie: (Movie | undefined)[] 55 | /** Update first Movie matching 'where' */ 56 | updateMovie?: Movie 57 | __typename: 'Mutation' 58 | } 59 | 60 | export interface Query { 61 | /** Count number of Genre nodes matching 'where' */ 62 | countGenres: Scalars['Int'] 63 | /** Count number of Movie nodes matching 'where' */ 64 | countMovies: Scalars['Int'] 65 | /** Find multiple Genres matching 'where' */ 66 | findManyGenres: (Genre | undefined)[] 67 | /** Find multiple Movies matching 'where' */ 68 | findManyMovies: (Movie | undefined)[] 69 | /** Find one Genre matching 'where' */ 70 | findOneGenre?: Genre 71 | /** Find one Movie matching 'where' */ 72 | findOneMovie?: Movie 73 | __typename: 'Query' 74 | } 75 | 76 | export interface GenreRequest{ 77 | createdAt?: boolean | number 78 | id?: boolean | number 79 | movies?: [{limit?: (Scalars['Int'] | null),skip?: (Scalars['Int'] | null),where?: (MovieWhere | null)},MovieRequest] | MovieRequest 80 | name?: boolean | number 81 | updatedAt?: boolean | number 82 | __typename?: boolean | number 83 | __scalar?: boolean | number 84 | } 85 | 86 | export interface GenreCreateInput {name?: (Scalars['String'] | null)} 87 | 88 | export interface GenreUpdateInput {name?: (Scalars['String'] | null)} 89 | 90 | export interface GenreWhere {AND?: (GenreWhere[] | null),NOT?: (GenreWhere[] | null),OR?: (GenreWhere[] | null),id?: (Scalars['ID'] | null),name?: (Scalars['String'] | null)} 91 | 92 | export interface MovieRequest{ 93 | createdAt?: boolean | number 94 | genres?: [{limit?: (Scalars['Int'] | null),skip?: (Scalars['Int'] | null),where?: (GenreWhere | null)},GenreRequest] | GenreRequest 95 | id?: boolean | number 96 | rating?: boolean | number 97 | title?: boolean | number 98 | updatedAt?: boolean | number 99 | year?: boolean | number 100 | __typename?: boolean | number 101 | __scalar?: boolean | number 102 | } 103 | 104 | export interface MovieCreateInput {rating?: (Scalars['Float'] | null),title: Scalars['String'],year?: (Scalars['Int'] | null)} 105 | 106 | export interface MovieUpdateInput {rating?: (Scalars['Float'] | null),title?: (Scalars['String'] | null),year?: (Scalars['Int'] | null)} 107 | 108 | export interface MovieWhere {AND?: (MovieWhere[] | null),NOT?: (MovieWhere[] | null),OR?: (MovieWhere[] | null),id?: (Scalars['ID'] | null),rating?: (Scalars['Float'] | null),title?: (Scalars['String'] | null),year?: (Scalars['Int'] | null)} 109 | 110 | export interface MutationRequest{ 111 | /** Create a single Genre using 'data' values */ 112 | createGenre?: [{data: GenreCreateInput},GenreRequest] 113 | /** Create a single Movie using 'data' values */ 114 | createMovie?: [{data: MovieCreateInput},MovieRequest] 115 | /** Delete one Genre by 'where', returns node if found otherwise null */ 116 | deleteGenre?: [{where: GenreWhere},GenreRequest] 117 | /** Delete multiple Genres by 'where', returns number of nodes deleted */ 118 | deleteManyGenre?: [{where: GenreWhere}] 119 | /** Delete multiple Movies by 'where', returns number of nodes deleted */ 120 | deleteManyMovie?: [{where: MovieWhere}] 121 | /** Delete one Movie by 'where', returns node if found otherwise null */ 122 | deleteMovie?: [{where: MovieWhere},MovieRequest] 123 | /** Merge will find or create a Genre matching 'where', if found will update using data, if not found will create using data + where */ 124 | mergeGenre?: [{data?: (GenreUpdateInput | null),where: GenreWhere},GenreRequest] 125 | /** Merge will find or create a Movie matching 'where', if found will update using data, if not found will create using data + where */ 126 | mergeMovie?: [{data?: (MovieUpdateInput | null),where: MovieWhere},MovieRequest] 127 | /** Update first Genre matching 'where' */ 128 | updateGenre?: [{data: GenreUpdateInput,where: GenreWhere},GenreRequest] 129 | /** Update multiple Genres matching 'where', sets all nodes to 'data' values */ 130 | updateManyGenre?: [{data: GenreUpdateInput,where: GenreWhere},GenreRequest] 131 | /** Update multiple Movies matching 'where', sets all nodes to 'data' values */ 132 | updateManyMovie?: [{data: MovieUpdateInput,where: MovieWhere},MovieRequest] 133 | /** Update first Movie matching 'where' */ 134 | updateMovie?: [{data: MovieUpdateInput,where: MovieWhere},MovieRequest] 135 | __typename?: boolean | number 136 | __scalar?: boolean | number 137 | } 138 | 139 | export interface QueryRequest{ 140 | /** Count number of Genre nodes matching 'where' */ 141 | countGenres?: [{where?: (GenreWhere | null)}] | boolean | number 142 | /** Count number of Movie nodes matching 'where' */ 143 | countMovies?: [{where?: (MovieWhere | null)}] | boolean | number 144 | /** Find multiple Genres matching 'where' */ 145 | findManyGenres?: [{limit?: (Scalars['Int'] | null),offset?: (Scalars['Int'] | null),where?: (GenreWhere | null)},GenreRequest] | GenreRequest 146 | /** Find multiple Movies matching 'where' */ 147 | findManyMovies?: [{limit?: (Scalars['Int'] | null),offset?: (Scalars['Int'] | null),where?: (MovieWhere | null)},MovieRequest] | MovieRequest 148 | /** Find one Genre matching 'where' */ 149 | findOneGenre?: [{where?: (GenreWhere | null)},GenreRequest] | GenreRequest 150 | /** Find one Movie matching 'where' */ 151 | findOneMovie?: [{where?: (MovieWhere | null)},MovieRequest] | MovieRequest 152 | __typename?: boolean | number 153 | __scalar?: boolean | number 154 | } 155 | 156 | 157 | const Genre_possibleTypes = ['Genre'] 158 | export const isGenre = (obj?: { __typename?: any } | null): obj is Genre => { 159 | if (!obj?.__typename) throw new Error('__typename is missing in "isGenre"') 160 | return Genre_possibleTypes.includes(obj.__typename) 161 | } 162 | 163 | 164 | 165 | const Movie_possibleTypes = ['Movie'] 166 | export const isMovie = (obj?: { __typename?: any } | null): obj is Movie => { 167 | if (!obj?.__typename) throw new Error('__typename is missing in "isMovie"') 168 | return Movie_possibleTypes.includes(obj.__typename) 169 | } 170 | 171 | 172 | 173 | const Mutation_possibleTypes = ['Mutation'] 174 | export const isMutation = (obj?: { __typename?: any } | null): obj is Mutation => { 175 | if (!obj?.__typename) throw new Error('__typename is missing in "isMutation"') 176 | return Mutation_possibleTypes.includes(obj.__typename) 177 | } 178 | 179 | 180 | 181 | const Query_possibleTypes = ['Query'] 182 | export const isQuery = (obj?: { __typename?: any } | null): obj is Query => { 183 | if (!obj?.__typename) throw new Error('__typename is missing in "isQuery"') 184 | return Query_possibleTypes.includes(obj.__typename) 185 | } 186 | 187 | 188 | export interface GenrePromiseChain{ 189 | createdAt: ({get: (request?: boolean|number, defaultValue?: Scalars['DateTime']) => Promise}), 190 | id: ({get: (request?: boolean|number, defaultValue?: Scalars['ID']) => Promise}), 191 | movies: ((args?: {limit?: (Scalars['Int'] | null),skip?: (Scalars['Int'] | null),where?: (MovieWhere | null)}) => {get: (request: R, defaultValue?: (FieldsSelection | undefined)[]) => Promise<(FieldsSelection | undefined)[]>})&({get: (request: R, defaultValue?: (FieldsSelection | undefined)[]) => Promise<(FieldsSelection | undefined)[]>}), 192 | name: ({get: (request?: boolean|number, defaultValue?: (Scalars['String'] | undefined)) => Promise<(Scalars['String'] | undefined)>}), 193 | updatedAt: ({get: (request?: boolean|number, defaultValue?: (Scalars['DateTime'] | undefined)) => Promise<(Scalars['DateTime'] | undefined)>}) 194 | } 195 | 196 | export interface GenreObservableChain{ 197 | createdAt: ({get: (request?: boolean|number, defaultValue?: Scalars['DateTime']) => Observable}), 198 | id: ({get: (request?: boolean|number, defaultValue?: Scalars['ID']) => Observable}), 199 | movies: ((args?: {limit?: (Scalars['Int'] | null),skip?: (Scalars['Int'] | null),where?: (MovieWhere | null)}) => {get: (request: R, defaultValue?: (FieldsSelection | undefined)[]) => Observable<(FieldsSelection | undefined)[]>})&({get: (request: R, defaultValue?: (FieldsSelection | undefined)[]) => Observable<(FieldsSelection | undefined)[]>}), 200 | name: ({get: (request?: boolean|number, defaultValue?: (Scalars['String'] | undefined)) => Observable<(Scalars['String'] | undefined)>}), 201 | updatedAt: ({get: (request?: boolean|number, defaultValue?: (Scalars['DateTime'] | undefined)) => Observable<(Scalars['DateTime'] | undefined)>}) 202 | } 203 | 204 | export interface MoviePromiseChain{ 205 | createdAt: ({get: (request?: boolean|number, defaultValue?: Scalars['DateTime']) => Promise}), 206 | genres: ((args?: {limit?: (Scalars['Int'] | null),skip?: (Scalars['Int'] | null),where?: (GenreWhere | null)}) => {get: (request: R, defaultValue?: (FieldsSelection | undefined)[]) => Promise<(FieldsSelection | undefined)[]>})&({get: (request: R, defaultValue?: (FieldsSelection | undefined)[]) => Promise<(FieldsSelection | undefined)[]>}), 207 | id: ({get: (request?: boolean|number, defaultValue?: Scalars['ID']) => Promise}), 208 | rating: ({get: (request?: boolean|number, defaultValue?: (Scalars['Float'] | undefined)) => Promise<(Scalars['Float'] | undefined)>}), 209 | title: ({get: (request?: boolean|number, defaultValue?: Scalars['String']) => Promise}), 210 | updatedAt: ({get: (request?: boolean|number, defaultValue?: (Scalars['DateTime'] | undefined)) => Promise<(Scalars['DateTime'] | undefined)>}), 211 | year: ({get: (request?: boolean|number, defaultValue?: (Scalars['Int'] | undefined)) => Promise<(Scalars['Int'] | undefined)>}) 212 | } 213 | 214 | export interface MovieObservableChain{ 215 | createdAt: ({get: (request?: boolean|number, defaultValue?: Scalars['DateTime']) => Observable}), 216 | genres: ((args?: {limit?: (Scalars['Int'] | null),skip?: (Scalars['Int'] | null),where?: (GenreWhere | null)}) => {get: (request: R, defaultValue?: (FieldsSelection | undefined)[]) => Observable<(FieldsSelection | undefined)[]>})&({get: (request: R, defaultValue?: (FieldsSelection | undefined)[]) => Observable<(FieldsSelection | undefined)[]>}), 217 | id: ({get: (request?: boolean|number, defaultValue?: Scalars['ID']) => Observable}), 218 | rating: ({get: (request?: boolean|number, defaultValue?: (Scalars['Float'] | undefined)) => Observable<(Scalars['Float'] | undefined)>}), 219 | title: ({get: (request?: boolean|number, defaultValue?: Scalars['String']) => Observable}), 220 | updatedAt: ({get: (request?: boolean|number, defaultValue?: (Scalars['DateTime'] | undefined)) => Observable<(Scalars['DateTime'] | undefined)>}), 221 | year: ({get: (request?: boolean|number, defaultValue?: (Scalars['Int'] | undefined)) => Observable<(Scalars['Int'] | undefined)>}) 222 | } 223 | 224 | export interface MutationPromiseChain{ 225 | 226 | /** Create a single Genre using 'data' values */ 227 | createGenre: ((args: {data: GenreCreateInput}) => GenrePromiseChain & {get: (request: R, defaultValue?: (FieldsSelection | undefined)) => Promise<(FieldsSelection | undefined)>}), 228 | 229 | /** Create a single Movie using 'data' values */ 230 | createMovie: ((args: {data: MovieCreateInput}) => MoviePromiseChain & {get: (request: R, defaultValue?: (FieldsSelection | undefined)) => Promise<(FieldsSelection | undefined)>}), 231 | 232 | /** Delete one Genre by 'where', returns node if found otherwise null */ 233 | deleteGenre: ((args: {where: GenreWhere}) => GenrePromiseChain & {get: (request: R, defaultValue?: (FieldsSelection | undefined)) => Promise<(FieldsSelection | undefined)>}), 234 | 235 | /** Delete multiple Genres by 'where', returns number of nodes deleted */ 236 | deleteManyGenre: ((args: {where: GenreWhere}) => {get: (request?: boolean|number, defaultValue?: Scalars['Int']) => Promise}), 237 | 238 | /** Delete multiple Movies by 'where', returns number of nodes deleted */ 239 | deleteManyMovie: ((args: {where: MovieWhere}) => {get: (request?: boolean|number, defaultValue?: Scalars['Int']) => Promise}), 240 | 241 | /** Delete one Movie by 'where', returns node if found otherwise null */ 242 | deleteMovie: ((args: {where: MovieWhere}) => MoviePromiseChain & {get: (request: R, defaultValue?: (FieldsSelection | undefined)) => Promise<(FieldsSelection | undefined)>}), 243 | 244 | /** Merge will find or create a Genre matching 'where', if found will update using data, if not found will create using data + where */ 245 | mergeGenre: ((args: {data?: (GenreUpdateInput | null),where: GenreWhere}) => GenrePromiseChain & {get: (request: R, defaultValue?: (FieldsSelection | undefined)) => Promise<(FieldsSelection | undefined)>}), 246 | 247 | /** Merge will find or create a Movie matching 'where', if found will update using data, if not found will create using data + where */ 248 | mergeMovie: ((args: {data?: (MovieUpdateInput | null),where: MovieWhere}) => MoviePromiseChain & {get: (request: R, defaultValue?: (FieldsSelection | undefined)) => Promise<(FieldsSelection | undefined)>}), 249 | 250 | /** Update first Genre matching 'where' */ 251 | updateGenre: ((args: {data: GenreUpdateInput,where: GenreWhere}) => GenrePromiseChain & {get: (request: R, defaultValue?: (FieldsSelection | undefined)) => Promise<(FieldsSelection | undefined)>}), 252 | 253 | /** Update multiple Genres matching 'where', sets all nodes to 'data' values */ 254 | updateManyGenre: ((args: {data: GenreUpdateInput,where: GenreWhere}) => {get: (request: R, defaultValue?: (FieldsSelection | undefined)[]) => Promise<(FieldsSelection | undefined)[]>}), 255 | 256 | /** Update multiple Movies matching 'where', sets all nodes to 'data' values */ 257 | updateManyMovie: ((args: {data: MovieUpdateInput,where: MovieWhere}) => {get: (request: R, defaultValue?: (FieldsSelection | undefined)[]) => Promise<(FieldsSelection | undefined)[]>}), 258 | 259 | /** Update first Movie matching 'where' */ 260 | updateMovie: ((args: {data: MovieUpdateInput,where: MovieWhere}) => MoviePromiseChain & {get: (request: R, defaultValue?: (FieldsSelection | undefined)) => Promise<(FieldsSelection | undefined)>}) 261 | } 262 | 263 | export interface MutationObservableChain{ 264 | 265 | /** Create a single Genre using 'data' values */ 266 | createGenre: ((args: {data: GenreCreateInput}) => GenreObservableChain & {get: (request: R, defaultValue?: (FieldsSelection | undefined)) => Observable<(FieldsSelection | undefined)>}), 267 | 268 | /** Create a single Movie using 'data' values */ 269 | createMovie: ((args: {data: MovieCreateInput}) => MovieObservableChain & {get: (request: R, defaultValue?: (FieldsSelection | undefined)) => Observable<(FieldsSelection | undefined)>}), 270 | 271 | /** Delete one Genre by 'where', returns node if found otherwise null */ 272 | deleteGenre: ((args: {where: GenreWhere}) => GenreObservableChain & {get: (request: R, defaultValue?: (FieldsSelection | undefined)) => Observable<(FieldsSelection | undefined)>}), 273 | 274 | /** Delete multiple Genres by 'where', returns number of nodes deleted */ 275 | deleteManyGenre: ((args: {where: GenreWhere}) => {get: (request?: boolean|number, defaultValue?: Scalars['Int']) => Observable}), 276 | 277 | /** Delete multiple Movies by 'where', returns number of nodes deleted */ 278 | deleteManyMovie: ((args: {where: MovieWhere}) => {get: (request?: boolean|number, defaultValue?: Scalars['Int']) => Observable}), 279 | 280 | /** Delete one Movie by 'where', returns node if found otherwise null */ 281 | deleteMovie: ((args: {where: MovieWhere}) => MovieObservableChain & {get: (request: R, defaultValue?: (FieldsSelection | undefined)) => Observable<(FieldsSelection | undefined)>}), 282 | 283 | /** Merge will find or create a Genre matching 'where', if found will update using data, if not found will create using data + where */ 284 | mergeGenre: ((args: {data?: (GenreUpdateInput | null),where: GenreWhere}) => GenreObservableChain & {get: (request: R, defaultValue?: (FieldsSelection | undefined)) => Observable<(FieldsSelection | undefined)>}), 285 | 286 | /** Merge will find or create a Movie matching 'where', if found will update using data, if not found will create using data + where */ 287 | mergeMovie: ((args: {data?: (MovieUpdateInput | null),where: MovieWhere}) => MovieObservableChain & {get: (request: R, defaultValue?: (FieldsSelection | undefined)) => Observable<(FieldsSelection | undefined)>}), 288 | 289 | /** Update first Genre matching 'where' */ 290 | updateGenre: ((args: {data: GenreUpdateInput,where: GenreWhere}) => GenreObservableChain & {get: (request: R, defaultValue?: (FieldsSelection | undefined)) => Observable<(FieldsSelection | undefined)>}), 291 | 292 | /** Update multiple Genres matching 'where', sets all nodes to 'data' values */ 293 | updateManyGenre: ((args: {data: GenreUpdateInput,where: GenreWhere}) => {get: (request: R, defaultValue?: (FieldsSelection | undefined)[]) => Observable<(FieldsSelection | undefined)[]>}), 294 | 295 | /** Update multiple Movies matching 'where', sets all nodes to 'data' values */ 296 | updateManyMovie: ((args: {data: MovieUpdateInput,where: MovieWhere}) => {get: (request: R, defaultValue?: (FieldsSelection | undefined)[]) => Observable<(FieldsSelection | undefined)[]>}), 297 | 298 | /** Update first Movie matching 'where' */ 299 | updateMovie: ((args: {data: MovieUpdateInput,where: MovieWhere}) => MovieObservableChain & {get: (request: R, defaultValue?: (FieldsSelection | undefined)) => Observable<(FieldsSelection | undefined)>}) 300 | } 301 | 302 | export interface QueryPromiseChain{ 303 | 304 | /** Count number of Genre nodes matching 'where' */ 305 | countGenres: ((args?: {where?: (GenreWhere | null)}) => {get: (request?: boolean|number, defaultValue?: Scalars['Int']) => Promise})&({get: (request?: boolean|number, defaultValue?: Scalars['Int']) => Promise}), 306 | 307 | /** Count number of Movie nodes matching 'where' */ 308 | countMovies: ((args?: {where?: (MovieWhere | null)}) => {get: (request?: boolean|number, defaultValue?: Scalars['Int']) => Promise})&({get: (request?: boolean|number, defaultValue?: Scalars['Int']) => Promise}), 309 | 310 | /** Find multiple Genres matching 'where' */ 311 | findManyGenres: ((args?: {limit?: (Scalars['Int'] | null),offset?: (Scalars['Int'] | null),where?: (GenreWhere | null)}) => {get: (request: R, defaultValue?: (FieldsSelection | undefined)[]) => Promise<(FieldsSelection | undefined)[]>})&({get: (request: R, defaultValue?: (FieldsSelection | undefined)[]) => Promise<(FieldsSelection | undefined)[]>}), 312 | 313 | /** Find multiple Movies matching 'where' */ 314 | findManyMovies: ((args?: {limit?: (Scalars['Int'] | null),offset?: (Scalars['Int'] | null),where?: (MovieWhere | null)}) => {get: (request: R, defaultValue?: (FieldsSelection | undefined)[]) => Promise<(FieldsSelection | undefined)[]>})&({get: (request: R, defaultValue?: (FieldsSelection | undefined)[]) => Promise<(FieldsSelection | undefined)[]>}), 315 | 316 | /** Find one Genre matching 'where' */ 317 | findOneGenre: ((args?: {where?: (GenreWhere | null)}) => GenrePromiseChain & {get: (request: R, defaultValue?: (FieldsSelection | undefined)) => Promise<(FieldsSelection | undefined)>})&(GenrePromiseChain & {get: (request: R, defaultValue?: (FieldsSelection | undefined)) => Promise<(FieldsSelection | undefined)>}), 318 | 319 | /** Find one Movie matching 'where' */ 320 | findOneMovie: ((args?: {where?: (MovieWhere | null)}) => MoviePromiseChain & {get: (request: R, defaultValue?: (FieldsSelection | undefined)) => Promise<(FieldsSelection | undefined)>})&(MoviePromiseChain & {get: (request: R, defaultValue?: (FieldsSelection | undefined)) => Promise<(FieldsSelection | undefined)>}) 321 | } 322 | 323 | export interface QueryObservableChain{ 324 | 325 | /** Count number of Genre nodes matching 'where' */ 326 | countGenres: ((args?: {where?: (GenreWhere | null)}) => {get: (request?: boolean|number, defaultValue?: Scalars['Int']) => Observable})&({get: (request?: boolean|number, defaultValue?: Scalars['Int']) => Observable}), 327 | 328 | /** Count number of Movie nodes matching 'where' */ 329 | countMovies: ((args?: {where?: (MovieWhere | null)}) => {get: (request?: boolean|number, defaultValue?: Scalars['Int']) => Observable})&({get: (request?: boolean|number, defaultValue?: Scalars['Int']) => Observable}), 330 | 331 | /** Find multiple Genres matching 'where' */ 332 | findManyGenres: ((args?: {limit?: (Scalars['Int'] | null),offset?: (Scalars['Int'] | null),where?: (GenreWhere | null)}) => {get: (request: R, defaultValue?: (FieldsSelection | undefined)[]) => Observable<(FieldsSelection | undefined)[]>})&({get: (request: R, defaultValue?: (FieldsSelection | undefined)[]) => Observable<(FieldsSelection | undefined)[]>}), 333 | 334 | /** Find multiple Movies matching 'where' */ 335 | findManyMovies: ((args?: {limit?: (Scalars['Int'] | null),offset?: (Scalars['Int'] | null),where?: (MovieWhere | null)}) => {get: (request: R, defaultValue?: (FieldsSelection | undefined)[]) => Observable<(FieldsSelection | undefined)[]>})&({get: (request: R, defaultValue?: (FieldsSelection | undefined)[]) => Observable<(FieldsSelection | undefined)[]>}), 336 | 337 | /** Find one Genre matching 'where' */ 338 | findOneGenre: ((args?: {where?: (GenreWhere | null)}) => GenreObservableChain & {get: (request: R, defaultValue?: (FieldsSelection | undefined)) => Observable<(FieldsSelection | undefined)>})&(GenreObservableChain & {get: (request: R, defaultValue?: (FieldsSelection | undefined)) => Observable<(FieldsSelection | undefined)>}), 339 | 340 | /** Find one Movie matching 'where' */ 341 | findOneMovie: ((args?: {where?: (MovieWhere | null)}) => MovieObservableChain & {get: (request: R, defaultValue?: (FieldsSelection | undefined)) => Observable<(FieldsSelection | undefined)>})&(MovieObservableChain & {get: (request: R, defaultValue?: (FieldsSelection | undefined)) => Observable<(FieldsSelection | undefined)>}) 342 | } --------------------------------------------------------------------------------