├── .editorconfig ├── .gitignore ├── .npmignore ├── .prettierrc ├── README.md ├── package.json ├── scripts └── demo.js ├── src ├── __tests__ │ └── index.test.ts ├── index.ts ├── internal │ ├── encode.ts │ ├── helpers.ts │ ├── index.ts │ ├── makeFields.ts │ ├── makeObject.ts │ ├── makeRelationship.ts │ ├── makeSchema.ts │ └── types.ts ├── level │ ├── index.ts │ ├── optimiseLevelJs.ts │ └── types.ts ├── relational │ ├── AsyncIdIterator.ts │ ├── AsyncLevelIterator.ts │ ├── AsyncObjectIterator.ts │ ├── ObjectFieldIndex.ts │ ├── ObjectFieldOrdinal.ts │ ├── ObjectTable.ts │ ├── __tests__ │ │ ├── ObjectFieldIndex.test.ts │ │ ├── ObjectTable.test.ts │ │ ├── helpers.test.ts │ │ ├── keys.test.ts │ │ └── mutexBatch.test.ts │ ├── helpers.ts │ ├── index.ts │ ├── keys.ts │ ├── mutexBatch.ts │ └── types.ts ├── schema │ ├── ResolverMap.ts │ ├── field.ts │ ├── index.ts │ ├── input.ts │ ├── object.ts │ ├── relationship.ts │ ├── scalar.ts │ └── types.ts └── utils │ └── promisify.ts ├── tsconfig.json ├── types └── deferred-leveldown.d.ts └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_size = 2 6 | end_of_line = lf 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | *.pid 4 | *.seed 5 | node_modules 6 | 7 | .rts2_cache_* 8 | lib 9 | dist 10 | *.d.ts 11 | *.mjs 12 | *.mjs.map 13 | *.d.ts.map 14 | *.umd.js 15 | *.js 16 | *.js.map 17 | *.tgz 18 | !types/*.d.ts 19 | !scripts/*.js 20 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /.* 2 | *.log 3 | *.tgz 4 | *.map 5 | *.lock 6 | types/ 7 | __tests__ 8 | tsconfig.json 9 | src 10 | dist 11 | lib 12 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "trailingComma": "es5", 5 | "printWidth": 100 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graphql-box 2 | 3 | Instant GraphQL [OpenCRUD](https://www.opencrud.org/#sec-undefined.Overview) executable schemas.
4 | Universally deployable (It's just JS™) and compatible with any [leveldown store](https://github.com/Level/awesome#stores)! 5 | 6 | ## What is it? 7 | 8 | `graphql-box` is a **GraphQL schema generator**. It accepts a [GraphQL SDL](https://graphql.org/learn/schema/) 9 | string and outputs an [OpenCRUD](https://www.opencrud.org/#sec-undefined.Overview) schema. 10 | This schema exposes CRUD queries and mutations, making it essentially a **GraphQL-based ORM**. 11 | 12 | It can use any leveldown store as its storage engine, which in turns supports databases like **IndexedDB**, 13 | **LevelDB**, **Redis**, **Mongo**, and [more](https://github.com/Level/awesome#stores). 14 | 15 | ## Why does it exist? 16 | 17 | GraphQL exists as a language and protocol facilitating a framework around specifying relational data 18 | and querying it. It speeds up the development of web apps by simplifying how to inject and fetch data. 19 | 20 | On the server-side tools like [Prisma](https://www.prisma.io/) help to speed up the other side of 21 | the GraphQL ecosystem. The development of GraphQL APIs can be sped a lot by writing data models in SDL 22 | and automating details of the data's storage away. 23 | 24 | `graphql-box` aims to make the latter as simple as possible, allowing you to quickly create ORM-like schemas 25 | instantly **in Node.js** on a multitude of storage engines, or also **just in the browser**. 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-box", 3 | "version": "0.1.0", 4 | "description": "[WIP] Instant GraphQL OpenCRUD database that is universally runnable & deployable", 5 | "main": "graphql-box.js", 6 | "module": "graphql-box.es.js", 7 | "umd:main": "graphql-box.umd.js", 8 | "source": "src/index.ts", 9 | "repository": "https://github.com/kitten/graphql-box", 10 | "author": "Phil Pluckthun ", 11 | "license": "MIT", 12 | "sideEffects": false, 13 | "scripts": { 14 | "demo": "micro scripts/demo.js", 15 | "bundle": "microbundle build --no-sourcemap --target web", 16 | "build": "tsc -m commonjs", 17 | "clean": "rimraf ./*.{map,js,mjs} ./*.d.ts ./internal ./relational ./utils ./level ./schema ./__tests__ ./.rts2_cache*", 18 | "test": "jest", 19 | "lint-staged": "lint-staged", 20 | "prepublishOnly": "run-p build bundle" 21 | }, 22 | "pre-commit": "lint-staged", 23 | "lint-staged": { 24 | "*.{js,json,css,md,ts}": [ 25 | "prettier --write", 26 | "git add" 27 | ] 28 | }, 29 | "jest": { 30 | "preset": "ts-jest", 31 | "testPathIgnorePatterns": [ 32 | "!*.d.ts", 33 | "!*.js", 34 | "/node_modules/", 35 | "/lib/" 36 | ] 37 | }, 38 | "dependencies": { 39 | "cuid": "^2.1.4", 40 | "deferred-leveldown": "^4.0.2", 41 | "graphql-iso-date": "^3.6.1", 42 | "graphql-type-json": "^0.2.1", 43 | "prisma-datamodel": "1.23.2" 44 | }, 45 | "peerDependencies": { 46 | "graphql": "^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0" 47 | }, 48 | "devDependencies": { 49 | "@types/abstract-leveldown": "^5.0.1", 50 | "@types/cuid": "^1.3.0", 51 | "@types/encoding-down": "^5.0.0", 52 | "@types/graphql": "^14.0.3", 53 | "@types/graphql-iso-date": "^3.3.1", 54 | "@types/graphql-type-json": "^0.1.3", 55 | "@types/jest": "^23.3.9", 56 | "@types/memdown": "^3.0.0", 57 | "apollo-server-micro": "^2.2.2", 58 | "graphql": "^14.0.2", 59 | "jest": "^23.6.0", 60 | "lint-staged": "^8.0.4", 61 | "memdown": "^3.0.0", 62 | "micro": "^9.3.3", 63 | "microbundle": "^0.9.0", 64 | "npm-run-all": "^4.1.3", 65 | "pre-commit": "^1.2.2", 66 | "prettier": "^1.15.2", 67 | "rimraf": "^2.6.2", 68 | "ts-jest": "^23.10.4", 69 | "typescript": "^3.2.2" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /scripts/demo.js: -------------------------------------------------------------------------------- 1 | const { ApolloServer } = require('apollo-server-micro'); 2 | const memdown = require('memdown'); 3 | 4 | const { makeExecutableSchema } = require('../index'); 5 | 6 | const sdl = ` 7 | type Commit { 8 | id: ID! @unique 9 | hash: String! @unique 10 | message: String 11 | files: [File]! 12 | } 13 | 14 | type File { 15 | id: ID! @unique 16 | createdAt: DateTime! 17 | name: String! 18 | } 19 | `; 20 | 21 | const schema = makeExecutableSchema(sdl, memdown()); 22 | 23 | const apolloServer = new ApolloServer({ schema, tracing: true }); 24 | module.exports = apolloServer.createHandler(); 25 | -------------------------------------------------------------------------------- /src/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { graphql, GraphQLSchema } from 'graphql'; 2 | import { AbstractLevelDOWN } from 'abstract-leveldown'; 3 | import memdown from 'memdown'; 4 | 5 | import { makeExecutableSchema } from '../index'; 6 | 7 | const sdl = ` 8 | type Commit { 9 | id: ID! @unique 10 | hash: String! @unique 11 | message: String 12 | } 13 | `; 14 | 15 | describe('makeExecutableSchema', () => { 16 | let store: AbstractLevelDOWN; 17 | let schema: GraphQLSchema; 18 | 19 | beforeEach(() => { 20 | store = memdown(); 21 | schema = makeExecutableSchema(sdl, store); 22 | }); 23 | 24 | describe('Basic CRUD', () => { 25 | it('supports creating items with complete data', async () => { 26 | expect( 27 | await graphql( 28 | schema, 29 | ` 30 | mutation { 31 | createCommit(data: { hash: "dc6dff6", message: "Initial Commit" }) { 32 | id 33 | hash 34 | message 35 | } 36 | } 37 | ` 38 | ) 39 | ).toEqual({ 40 | data: { 41 | createCommit: { 42 | id: expect.any(String), 43 | hash: 'dc6dff6', 44 | message: 'Initial Commit', 45 | }, 46 | }, 47 | }); 48 | }); 49 | 50 | it('supports creating items with required fields only', async () => { 51 | expect( 52 | await graphql( 53 | schema, 54 | ` 55 | mutation { 56 | createCommit(data: { hash: "dc6dff6" }) { 57 | id 58 | hash 59 | message 60 | } 61 | } 62 | ` 63 | ) 64 | ).toEqual({ 65 | data: { 66 | createCommit: { 67 | id: expect.any(String), 68 | hash: 'dc6dff6', 69 | message: null, 70 | }, 71 | }, 72 | }); 73 | }); 74 | 75 | it('supports creating items then retrieving them by id or indexed field', async () => { 76 | const { 77 | data: { 78 | createCommit: { id, hash }, 79 | }, 80 | } = await graphql( 81 | schema, 82 | ` 83 | mutation { 84 | createCommit(data: { hash: "dc6dff6", message: "Initial Commit" }) { 85 | id 86 | hash 87 | message 88 | } 89 | } 90 | ` 91 | ); 92 | 93 | const expected = { 94 | data: { 95 | commit: { 96 | id, 97 | hash: 'dc6dff6', 98 | message: 'Initial Commit', 99 | }, 100 | }, 101 | }; 102 | 103 | expect( 104 | await graphql( 105 | schema, 106 | ` 107 | { 108 | commit(where: { id: "${id}" }) { 109 | id, hash, message 110 | } 111 | } 112 | ` 113 | ) 114 | ).toEqual(expected); 115 | 116 | expect( 117 | await graphql(schema, `{ commit(where: { hash: "${hash}" }) { id, hash, message } }`) 118 | ).toEqual(expected); 119 | }); 120 | 121 | it('supports creating items, updating, and retrieving them', async () => { 122 | const { 123 | data: { createCommit }, 124 | } = await graphql( 125 | schema, 126 | ` 127 | mutation { 128 | createCommit(data: { hash: "dc6dff6", message: "Initial Commit" }) { 129 | id 130 | hash 131 | message 132 | } 133 | } 134 | ` 135 | ); 136 | 137 | const { 138 | data: { updateCommit }, 139 | } = await graphql( 140 | schema, 141 | ` 142 | mutation { 143 | updateCommit(where: { hash: "dc6dff6" }, data: { hash: "1111111" }) { 144 | id 145 | hash 146 | message 147 | } 148 | } 149 | ` 150 | ); 151 | 152 | expect(updateCommit.id).toBe(createCommit.id); 153 | expect(updateCommit.message).toBe(createCommit.message); 154 | 155 | const expected = { 156 | data: { 157 | commit: { 158 | id: createCommit.id, 159 | hash: updateCommit.hash, 160 | message: 'Initial Commit', 161 | }, 162 | }, 163 | }; 164 | 165 | expect( 166 | await graphql( 167 | schema, 168 | ` 169 | { 170 | commit(where: { hash: "1111111" }) { 171 | id 172 | hash 173 | message 174 | } 175 | } 176 | ` 177 | ) 178 | ).toEqual(expected); 179 | }); 180 | }); 181 | }); 182 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { AbstractLevelDOWN } from 'abstract-leveldown'; 2 | 3 | import level from './level'; 4 | import { makeSchemaDefinition } from './internal'; 5 | import { genSchema } from './schema'; 6 | 7 | export const makeExecutableSchema = (sdl: string, leveldown: AbstractLevelDOWN) => 8 | genSchema(level(leveldown), makeSchemaDefinition(sdl)); 9 | -------------------------------------------------------------------------------- /src/internal/encode.ts: -------------------------------------------------------------------------------- 1 | import { 2 | parseTime, 3 | serializeTime, 4 | parseDate, 5 | serializeDate, 6 | } from 'graphql-iso-date/dist/utils/formatter'; 7 | 8 | import { FieldDefinitionParams, Encoder, Serializer, Deserializer } from './types'; 9 | 10 | const NOT_NULL_PREFIX = ':'; 11 | const NOT_NULL_CHARCODE = NOT_NULL_PREFIX.charCodeAt(0); 12 | 13 | const LIST_SEPARATOR = ','; 14 | const LIST_SEPARATOR_RE = /,/g; 15 | const ESC_LIST_SEPARATOR = '%2C'; 16 | const ESC_LIST_SEPARATOR_RE = /%2C/g; 17 | 18 | const identity = (val) => val; 19 | 20 | const serializeJSON: Serializer = val => JSON.stringify(val); 21 | const deserializeJSON: Deserializer = val => JSON.parse(val); 22 | 23 | const serializeDateTime: Serializer = val => String(val.valueOf()); 24 | const deserializeDateTime: Deserializer = str => new Date(parseInt(str, 10)); 25 | 26 | const serializeBool: Serializer = val => (val ? '1' : '0'); 27 | const deserializeBool: Deserializer = str => str === '1'; 28 | 29 | const serializeFloat: Serializer = val => String(val); 30 | const deserializeFloat: Deserializer = str => parseFloat(str); 31 | 32 | const serializeInt: Serializer = val => String(val | 0); 33 | const deserializeInt: Deserializer = str => parseInt(str, 10); 34 | 35 | const serializeNull = (child: Serializer): Serializer => val => 36 | val === null || val === undefined ? '' : NOT_NULL_PREFIX + child(val); 37 | 38 | const deserializeNull = (child: Deserializer): Deserializer => str => 39 | str.charCodeAt(0) !== NOT_NULL_CHARCODE ? null : child(str.slice(1)); 40 | 41 | const serializeListItem = (str: string): string => 42 | str.replace(LIST_SEPARATOR_RE, ESC_LIST_SEPARATOR); 43 | const deserializeListItem = (str: string): string => 44 | str.replace(ESC_LIST_SEPARATOR_RE, ESC_LIST_SEPARATOR); 45 | 46 | const serializeList = (child: Serializer): Serializer => val => { 47 | let out = ''; 48 | for (let i = 0, l = val.length; i < l; i++) { 49 | out += serializeListItem(child(val[i])); 50 | if (i < l - 1) { 51 | out += LIST_SEPARATOR; 52 | } 53 | } 54 | return out; 55 | }; 56 | 57 | const deserializeList = (child: Deserializer): Deserializer => str => { 58 | const raw = str.split(LIST_SEPARATOR); 59 | const out = new Array(raw.length); 60 | for (let i = 0, l = raw.length; i < l; i++) { 61 | out[i] = child(deserializeListItem(raw[i])); 62 | } 63 | return out; 64 | }; 65 | 66 | export const makeEncoder = (field: FieldDefinitionParams): Encoder => { 67 | if (typeof field.type !== 'string') { 68 | throw new Error('Relationships in SDL types are currently unsupported'); 69 | } 70 | 71 | const { type, isRequired, isList } = field; 72 | 73 | let serializer: Serializer; 74 | let deserializer: Deserializer; 75 | 76 | switch (type) { 77 | case 'Date': 78 | serializer = serializeDate; 79 | deserializer = parseDate; 80 | break; 81 | case 'Time': 82 | serializer = serializeTime; 83 | deserializer = parseTime; 84 | break; 85 | case 'DateTime': 86 | serializer = serializeDateTime; 87 | deserializer = deserializeDateTime; 88 | break; 89 | case 'JSON': 90 | serializer = serializeJSON; 91 | deserializer = deserializeJSON; 92 | break; 93 | case 'Int': 94 | serializer = serializeInt; 95 | deserializer = deserializeInt; 96 | break; 97 | case 'Float': 98 | serializer = serializeFloat; 99 | deserializer = deserializeFloat; 100 | break; 101 | case 'Boolean': 102 | serializer = serializeBool; 103 | deserializer = deserializeBool; 104 | break; 105 | case 'ID': 106 | case 'String': 107 | serializer = identity; 108 | deserializer = identity; 109 | break; 110 | } 111 | 112 | if (isList) { 113 | serializer = serializeList(serializer); 114 | deserializer = deserializeList(deserializer); 115 | } 116 | 117 | if (!isRequired) { 118 | serializer = serializeNull(serializer); 119 | deserializer = deserializeNull(deserializer); 120 | } 121 | 122 | return { serializer, deserializer }; 123 | }; 124 | -------------------------------------------------------------------------------- /src/internal/helpers.ts: -------------------------------------------------------------------------------- 1 | import { IGQLField } from 'prisma-datamodel'; 2 | import { Scalar, FieldDefinitionParams, RelationshipKind } from './types'; 3 | 4 | export const isSystemField = (name: string) => 5 | name === 'id' || name === 'createdAt' || name === 'updatedAt'; 6 | 7 | export const systemFieldDefs: FieldDefinitionParams[] = [ 8 | { 9 | name: 'id', 10 | type: 'ID', 11 | isSystemField: true, 12 | isList: false, 13 | isRequired: true, 14 | // marked as non-unique since it shouldn't be indexed as it's already 15 | // the primary key 16 | isUnique: false, 17 | isOrdinal: false, 18 | isReadOnly: true, 19 | }, 20 | { 21 | name: 'createdAt', 22 | type: 'DateTime', 23 | isSystemField: true, 24 | isList: false, 25 | isRequired: true, 26 | isUnique: false, 27 | isOrdinal: false, 28 | isReadOnly: true, 29 | }, 30 | { 31 | name: 'updatedAt', 32 | type: 'DateTime', 33 | isSystemField: true, 34 | isList: false, 35 | isRequired: true, 36 | isUnique: false, 37 | isOrdinal: false, 38 | isReadOnly: false, 39 | }, 40 | ]; 41 | 42 | // Validate input scalars 43 | export const toScalar = (type: string): Scalar => { 44 | switch (type) { 45 | case 'Date': 46 | case 'Time': 47 | case 'DateTime': 48 | case 'JSON': 49 | case 'Int': 50 | case 'Float': 51 | case 'Boolean': 52 | case 'ID': 53 | case 'String': 54 | return type as Scalar; 55 | default: 56 | throw new Error(`Unrecognised scalar of type "${type}".`); 57 | } 58 | }; 59 | 60 | export const getRelationshipKind = (field: IGQLField): RelationshipKind => { 61 | if (!field.isList && !field.relatedField) { 62 | return RelationshipKind.ToOne; 63 | } else if (!field.isList && field.relatedField && !field.relatedField.isList) { 64 | return RelationshipKind.OneToOne; 65 | } else if (field.isList && (!field.relatedField || !field.relatedField.isList)) { 66 | return RelationshipKind.OneToMany; 67 | } else if (!field.isList && field.relatedField && field.relatedField.isList) { 68 | return RelationshipKind.OneToMany; 69 | } else if (field.isList && field.relatedField && field.relatedField.isList) { 70 | return RelationshipKind.ManyToMany; 71 | } else { 72 | throw new Error(`Invalid relationship on "${field.name}"`); 73 | } 74 | }; 75 | -------------------------------------------------------------------------------- /src/internal/index.ts: -------------------------------------------------------------------------------- 1 | export { makeFields, FieldDefinition } from './makeFields'; 2 | export { makeObject, ObjectDefinition } from './makeObject'; 3 | export { RelationshipDefinition, RelationFieldParams } from './makeRelationship'; 4 | export { makeSchemaDefinition, SchemaDefinition } from './makeSchema'; 5 | export { Serializer, Deserializer, RelationshipKind } from './types'; 6 | -------------------------------------------------------------------------------- /src/internal/makeFields.ts: -------------------------------------------------------------------------------- 1 | import { IGQLType } from 'prisma-datamodel'; 2 | import { Scalar, FieldDefinitionParams, Serializer, Deserializer } from './types'; 3 | import { makeEncoder } from './encode'; 4 | 5 | import { isSystemField, systemFieldDefs, toScalar } from './helpers'; 6 | 7 | export class FieldDefinition { 8 | name: K; 9 | type: Scalar; 10 | defaultValue?: any; 11 | isSystemField: boolean; // One of: 'id', 'createdAt', or 'updatedAt' 12 | isList: boolean; // Scalar is a list 13 | isRequired: boolean; // Scalar is non-nullable 14 | isUnique: boolean; // Column should be uniquely indexed 15 | isOrdinal: boolean; // Column should be non-uniquely indexed 16 | isReadOnly: boolean; // Column cannot be modified after creation 17 | 18 | encode: Serializer; 19 | decode: Deserializer; 20 | 21 | constructor(params: FieldDefinitionParams) { 22 | // Constraints of field types and indexing 23 | if (params.isUnique && params.isOrdinal) { 24 | throw new Error(`Field "${params.name}" has been marked as both ordinal and unique.`); 25 | } else if ((params.isUnique || params.isOrdinal) && params.isList) { 26 | throw new Error( 27 | `Field "${params.name}" of type List cannot been marked as ordinal or unique.` 28 | ); 29 | } 30 | 31 | Object.assign(this, params); 32 | const encoder = makeEncoder(params); 33 | this.encode = encoder.serializer; 34 | this.decode = encoder.deserializer; 35 | } 36 | } 37 | 38 | const systemFields = systemFieldDefs.map(params => new FieldDefinition(params)); 39 | 40 | export const makeFields = (obj: IGQLType): FieldDefinition[] => { 41 | const sparseFields = obj.fields.filter(field => { 42 | return typeof field.type === 'string' && !isSystemField(field.name) && !field.isId; 43 | }); 44 | 45 | // Convert IGQLField to FieldDefinitions 46 | const objFieldDefinitions = sparseFields.map(field => { 47 | const def = new FieldDefinition({ 48 | name: field.name, 49 | type: toScalar(field.type as string), 50 | defaultValue: field.defaultValue, 51 | isSystemField: false, 52 | isList: field.isList, 53 | isRequired: field.isRequired, 54 | isUnique: field.isUnique, 55 | isOrdinal: false, 56 | isReadOnly: field.isReadOnly, 57 | }); 58 | 59 | return def; 60 | }); 61 | 62 | // Add system fields which were previously filtered 63 | return [...systemFields, ...objFieldDefinitions]; 64 | }; 65 | -------------------------------------------------------------------------------- /src/internal/makeObject.ts: -------------------------------------------------------------------------------- 1 | import { IGQLType, camelCase, capitalize, plural } from 'prisma-datamodel'; 2 | import { ObjectTable } from '../relational'; 3 | import { makeFields, FieldDefinition } from './makeFields'; 4 | import { RelationshipDefinition } from './makeRelationship'; 5 | 6 | export class ObjectDefinition { 7 | typeName: string; 8 | singleName: string; 9 | multiName: string; 10 | fields: FieldDefinition[]; 11 | relations: RelationshipDefinition[]; 12 | table?: ObjectTable; 13 | 14 | constructor(obj: IGQLType) { 15 | const { name, isEnum } = obj; 16 | if (isEnum) { 17 | return null; 18 | } 19 | 20 | this.typeName = capitalize(name); 21 | this.singleName = camelCase(name); 22 | this.multiName = plural(camelCase(name)); 23 | this.fields = makeFields(obj); 24 | this.relations = []; 25 | } 26 | } 27 | 28 | export const combineTypeNames = (a: ObjectDefinition, b: ObjectDefinition) => { 29 | const nameA = a.typeName; 30 | const nameB = b.typeName; 31 | const orderedName = nameA >= nameB ? nameA + nameB : nameB + nameA; 32 | return `${camelCase(orderedName)}Relation`; 33 | }; 34 | 35 | export const makeObject = (obj: IGQLType): ObjectDefinition | null => { 36 | return new ObjectDefinition(obj); 37 | }; 38 | -------------------------------------------------------------------------------- /src/internal/makeRelationship.ts: -------------------------------------------------------------------------------- 1 | import { IGQLField } from 'prisma-datamodel'; 2 | import { ObjectDefinition } from './makeObject'; 3 | import { FieldDefinition } from './makeFields'; 4 | import { RelationshipKind } from './types'; 5 | import { getRelationshipKind } from './helpers'; 6 | 7 | type ObjectDefTuple = [ObjectDefinition, ObjectDefinition]; 8 | type FieldTuple = [IGQLField, null | IGQLField]; 9 | 10 | export interface RelationFieldParams { 11 | fieldName: string; 12 | relatedFieldName: string; 13 | relatedDefinition: ObjectDefinition; 14 | isList: boolean; 15 | isRequired: boolean; 16 | } 17 | 18 | export class RelationshipDefinition { 19 | fromObj: ObjectDefinition; 20 | toObj: ObjectDefinition; 21 | fromFieldName: string; 22 | toFieldName: null | string; 23 | isFromRequired: boolean; 24 | isToRequired: boolean; 25 | isFromList: boolean; 26 | isToList: boolean; 27 | kind: RelationshipKind; 28 | 29 | constructor(objTuple: ObjectDefTuple, fieldTuple: FieldTuple) { 30 | this.isFromList = false; 31 | this.isToList = false; 32 | 33 | const fromObj = (this.fromObj = objTuple[0]); 34 | const toObj = (this.toObj = objTuple[1]); 35 | const fromField = fieldTuple[0]; 36 | const toField = fieldTuple[1] || null; 37 | const kind = (this.kind = getRelationshipKind(fromField)); 38 | const fromFieldName = (this.fromFieldName = fromField.name); 39 | 40 | const isFromRequired = (this.isFromRequired = fromField.isRequired); 41 | const isToRequired = (this.isToRequired = 42 | toField !== null ? toField.isRequired : fromField.isRequired); 43 | 44 | // Add relationship to ObjectDefinitions 45 | fromObj.relations.push(this); 46 | if (kind !== RelationshipKind.ToOne) { 47 | toObj.relations.push(this); 48 | } 49 | 50 | if (kind === RelationshipKind.ToOne) { 51 | this.toFieldName = null; 52 | 53 | fromObj.fields.push( 54 | new FieldDefinition({ 55 | name: fromField.name, 56 | type: 'ID', 57 | isSystemField: true, 58 | isList: false, 59 | isRequired: isFromRequired, 60 | isUnique: false, 61 | isOrdinal: true, 62 | isReadOnly: false, 63 | }) 64 | ); 65 | } else if (kind === RelationshipKind.OneToOne) { 66 | if (isFromRequired !== isToRequired) { 67 | throw new Error( 68 | `Mismatching required type on related fields "${fromObj.typeName}:${ 69 | fromField.name 70 | }" and "${toObj.typeName}:${toField.name}".` 71 | ); 72 | } 73 | 74 | const fieldParams = { 75 | type: 'ID', 76 | isSystemField: true, 77 | isList: false, 78 | isRequired: isFromRequired, 79 | isUnique: true, 80 | isOrdinal: false, 81 | isReadOnly: false, 82 | }; 83 | 84 | const toFieldName = (this.toFieldName = toField !== null ? toField.name : fromObj.singleName); 85 | 86 | fromObj.fields.push( 87 | new FieldDefinition({ 88 | name: fromField.name, 89 | ...fieldParams, 90 | }) 91 | ); 92 | 93 | toObj.fields.push( 94 | new FieldDefinition({ 95 | name: toFieldName, 96 | ...fieldParams, 97 | }) 98 | ); 99 | } else if (kind === RelationshipKind.OneToMany && fromField.isList) { 100 | this.isFromList = true; 101 | const toFieldName = (this.toFieldName = toField !== null ? toField.name : fromObj.singleName); 102 | 103 | toObj.fields.push( 104 | new FieldDefinition({ 105 | name: toFieldName, 106 | type: 'ID', 107 | isSystemField: true, 108 | isList: false, 109 | isRequired: isToRequired, 110 | isUnique: false, 111 | isOrdinal: true, 112 | isReadOnly: false, 113 | }) 114 | ); 115 | } else if (kind === RelationshipKind.OneToMany && !fromField.isList) { 116 | this.isToList = true; 117 | this.toFieldName = toField !== null ? toField.name : fromObj.multiName; 118 | 119 | fromObj.fields.push( 120 | new FieldDefinition({ 121 | name: fromFieldName, 122 | type: 'ID', 123 | isSystemField: true, 124 | isList: false, 125 | isRequired: isFromRequired, 126 | isUnique: false, 127 | isOrdinal: true, 128 | isReadOnly: false, 129 | }) 130 | ); 131 | } else { 132 | // Many-to-many won't result in any fields being added 133 | this.toFieldName = toField !== null ? toField.name : fromObj.multiName; 134 | this.isFromList = true; 135 | this.isToList = true; 136 | } 137 | } 138 | 139 | getSelfField(self: ObjectDefinition): RelationFieldParams { 140 | return this.fromObj === self 141 | ? { 142 | fieldName: this.fromFieldName, 143 | relatedFieldName: this.toFieldName, 144 | relatedDefinition: this.toObj, 145 | isList: this.isFromList, 146 | isRequired: this.isFromRequired, 147 | } 148 | : { 149 | fieldName: this.toFieldName, 150 | relatedFieldName: this.fromFieldName, 151 | relatedDefinition: this.fromObj, 152 | isList: this.isToList, 153 | isRequired: this.isToRequired, 154 | }; 155 | } 156 | } 157 | 158 | export const makeRelationship = (objTuple: ObjectDefTuple, fieldTuple: FieldTuple) => { 159 | return new RelationshipDefinition(objTuple, fieldTuple); 160 | }; 161 | -------------------------------------------------------------------------------- /src/internal/makeSchema.ts: -------------------------------------------------------------------------------- 1 | import { Parser, DatabaseType, capitalize } from 'prisma-datamodel'; 2 | import { makeObject, combineTypeNames, ObjectDefinition } from './makeObject'; 3 | import { makeRelationship, RelationshipDefinition } from './makeRelationship'; 4 | 5 | export interface SchemaDefinition { 6 | objects: ObjectDefinition[]; 7 | objByName: Record; 8 | relationsByName: Record; 9 | } 10 | 11 | export const makeSchemaDefinition = (sdl: string): SchemaDefinition => { 12 | const output = Parser.create(DatabaseType.postgres).parseFromSchemaString(sdl); 13 | const { types: internalTypes } = output; 14 | const objects = internalTypes.map(obj => makeObject(obj)).filter(obj => obj !== null); 15 | 16 | const objByName: Record = objects.reduce((acc, obj) => { 17 | acc[obj.typeName] = obj; 18 | return acc; 19 | }, {}); 20 | 21 | const relationsByName: Record = {}; 22 | 23 | // Find all unique relations and create them 24 | for (const obj of internalTypes) { 25 | const fromObj = objByName[capitalize(obj.name)]; 26 | if (fromObj !== undefined) { 27 | for (const field of obj.fields) { 28 | if (typeof field.type !== 'string') { 29 | const toObj = objByName[capitalize(field.type.name)]; 30 | if (toObj !== undefined) { 31 | // Skip if this relation has been created already 32 | const relationName = field.relationName || combineTypeNames(fromObj, toObj); 33 | if (relationsByName[relationName] === undefined) { 34 | // This will create the relation and return it but it'll also write it to fromObj/toObj and 35 | // add appropriate fields to the ObjectDefinitions if needed 36 | relationsByName[relationName] = makeRelationship( 37 | [fromObj, toObj], 38 | [field, field.relatedField] 39 | ); 40 | } 41 | } 42 | } 43 | } 44 | } 45 | } 46 | 47 | return { 48 | objects, 49 | objByName, 50 | relationsByName, 51 | }; 52 | }; 53 | -------------------------------------------------------------------------------- /src/internal/types.ts: -------------------------------------------------------------------------------- 1 | export type Scalar = 2 | | 'Date' 3 | | 'Time' 4 | | 'DateTime' 5 | | 'JSON' 6 | | 'Int' 7 | | 'Float' 8 | | 'Boolean' 9 | | 'ID' 10 | | 'String'; 11 | 12 | export interface FieldDefinitionParams { 13 | name: K; 14 | type: string; 15 | defaultValue?: any; 16 | isSystemField: boolean; 17 | isList: boolean; 18 | isRequired: boolean; 19 | isUnique: boolean; 20 | isOrdinal: boolean; 21 | isReadOnly: boolean; 22 | } 23 | 24 | export type Serializer = (val: T) => string; 25 | export type Deserializer = (str: string) => T; 26 | 27 | export interface Encoder { 28 | serializer: Serializer; 29 | deserializer: Deserializer; 30 | } 31 | 32 | export enum RelationshipKind { 33 | ToOne = 'ToOne', 34 | OneToOne = 'OneToOne', 35 | OneToMany = 'OneToMany', 36 | ManyToMany = 'ManyToMany', 37 | } 38 | -------------------------------------------------------------------------------- /src/level/index.ts: -------------------------------------------------------------------------------- 1 | import { AbstractLevelDOWN, AbstractIteratorOptions, AbstractIterator } from 'abstract-leveldown'; 2 | 3 | import DeferredLevelDOWN from 'deferred-leveldown'; 4 | 5 | import { promisify } from '../utils/promisify'; 6 | import { LevelInterface, LevelChainInterface, InternalLevelDown } from './types'; 7 | import { optimiseLevelJs } from './optimiseLevelJs'; 8 | 9 | type K = string; 10 | type V = string; 11 | 12 | function noop() {} 13 | 14 | export class LevelWrapper implements LevelInterface { 15 | db: InternalLevelDown; 16 | open: () => Promise; 17 | close: () => Promise; 18 | put: (key: K, value: V) => Promise; 19 | del: (key: K) => Promise; 20 | _get: (K, AbstractGetOptions) => Promise; 21 | 22 | constructor(db: AbstractLevelDOWN) { 23 | this.db = new DeferredLevelDOWN(optimiseLevelJs(db)) as InternalLevelDown; 24 | this.put = promisify(this.db.put).bind(this.db); 25 | this.del = promisify(this.db.del).bind(this.db); 26 | this._get = promisify(this.db.get).bind(this.db); 27 | 28 | this.db.open( 29 | { 30 | createIfMissing: true, 31 | errorIfExists: false, 32 | }, 33 | noop 34 | ); 35 | } 36 | 37 | async get(key: K): Promise { 38 | try { 39 | return await this._get(key, { asBuffer: false }); 40 | } catch (_err) { 41 | return null; 42 | } 43 | } 44 | 45 | iterator(options: AbstractIteratorOptions = {}): AbstractIterator { 46 | options.keyAsBuffer = false; 47 | options.valueAsBuffer = false; 48 | return this.db.iterator(options); 49 | } 50 | 51 | batch(): LevelChainInterface { 52 | const batch = this.db.batch(); 53 | batch.write = promisify(batch.write).bind(batch); 54 | return batch as LevelChainInterface; 55 | } 56 | } 57 | 58 | const level = (db: AbstractLevelDOWN) => new LevelWrapper(db); 59 | 60 | export { LevelInterface, LevelChainInterface }; 61 | export default level; 62 | -------------------------------------------------------------------------------- /src/level/optimiseLevelJs.ts: -------------------------------------------------------------------------------- 1 | import { AbstractLevelDOWN } from 'abstract-leveldown'; 2 | 3 | function identity(arg: T): T { 4 | return arg; 5 | } 6 | 7 | // We only store string keys and values 8 | // So we can replace the serialize functions on level-js 9 | export function optimiseLevelJs(db: AbstractLevelDOWN): AbstractLevelDOWN { 10 | const obj = db as any; 11 | 12 | if ( 13 | typeof obj.store === 'function' && 14 | typeof obj.await === 'function' && 15 | typeof obj.destroy === 'function' 16 | ) { 17 | obj._serializeKey = identity; 18 | obj._serializeValue = identity; 19 | } 20 | 21 | return db; 22 | } 23 | -------------------------------------------------------------------------------- /src/level/types.ts: -------------------------------------------------------------------------------- 1 | import { AbstractLevelDOWN } from 'abstract-leveldown'; 2 | import { AbstractIterator, AbstractIteratorOptions } from 'abstract-leveldown'; 3 | 4 | export interface LevelChainInterface { 5 | put(key: K, value: V): this; 6 | del(key: K): this; 7 | clear(): this; 8 | write(): Promise; 9 | } 10 | 11 | export interface LevelInterface { 12 | get(key: K): Promise; 13 | put(key: K, value: V): Promise; 14 | del(key: K): Promise; 15 | batch(): LevelChainInterface; 16 | iterator(options?: AbstractIteratorOptions): AbstractIterator; 17 | } 18 | 19 | export interface InternalLevelDown extends AbstractLevelDOWN { 20 | _open: AbstractLevelDOWN['open']; 21 | _close: AbstractLevelDOWN['close']; 22 | _get: AbstractLevelDOWN['get']; 23 | _put: AbstractLevelDOWN['put']; 24 | _del: AbstractLevelDOWN['del']; 25 | _batch: AbstractLevelDOWN['batch']; 26 | _iterator: AbstractLevelDOWN['iterator']; 27 | } 28 | -------------------------------------------------------------------------------- /src/relational/AsyncIdIterator.ts: -------------------------------------------------------------------------------- 1 | import { nextOrNull } from './helpers'; 2 | import AsyncLevelIterator from './AsyncLevelIterator'; 3 | 4 | class AsyncIdIterator extends AsyncLevelIterator { 5 | async nextOrNull(): Promise { 6 | const entry = await nextOrNull(this.iterator); 7 | return entry !== null ? entry[1] : null; 8 | } 9 | } 10 | 11 | export default AsyncIdIterator; 12 | -------------------------------------------------------------------------------- /src/relational/AsyncLevelIterator.ts: -------------------------------------------------------------------------------- 1 | import { Iterator } from './types'; 2 | import { closeIter } from './helpers'; 3 | 4 | abstract class AsyncLevelIterator implements AsyncIterableIterator { 5 | iterator: Iterator; 6 | done: boolean; 7 | 8 | constructor(iterator: Iterator) { 9 | this.iterator = iterator; 10 | this.done = false; 11 | } 12 | 13 | abstract nextOrNull(): Promise; 14 | 15 | async next(): Promise> { 16 | if (this.done) { 17 | return { done: true } as IteratorResult; 18 | } 19 | 20 | const value = await this.nextOrNull(); 21 | if (value === null) { 22 | return { done: this.done = true } as any; 23 | } 24 | 25 | return { done: false, value }; 26 | } 27 | 28 | async return() { 29 | await closeIter(this.iterator); 30 | return { done: true } as IteratorResult; 31 | } 32 | 33 | [Symbol.asyncIterator]() { 34 | return this; 35 | } 36 | } 37 | 38 | export default AsyncLevelIterator; 39 | -------------------------------------------------------------------------------- /src/relational/AsyncObjectIterator.ts: -------------------------------------------------------------------------------- 1 | import { Deserializer } from '../internal'; 2 | import { Iterator, ObjectLike } from './types'; 3 | import { nextObjectOrNull } from './helpers'; 4 | import AsyncLevelIterator from './AsyncLevelIterator'; 5 | 6 | class AsyncObjectIterator extends AsyncLevelIterator { 7 | fieldNames: K[]; 8 | decoders: Deserializer[]; 9 | 10 | constructor(fieldNames: K[], decoders: Deserializer[], iterator: Iterator) { 11 | super(iterator); 12 | this.fieldNames = fieldNames; 13 | this.decoders = decoders; 14 | } 15 | 16 | nextOrNull(): Promise { 17 | return nextObjectOrNull(this.fieldNames, this.decoders, this.iterator); 18 | } 19 | } 20 | 21 | export default AsyncObjectIterator; 22 | -------------------------------------------------------------------------------- /src/relational/ObjectFieldIndex.ts: -------------------------------------------------------------------------------- 1 | import { LevelInterface, LevelChainInterface } from '../level'; 2 | import { ObjectFieldIndexParams, IteratorOptions } from './types'; 3 | import { gen2DKey, rangeOfKey } from './keys'; 4 | import AsyncIdIterator from './AsyncIdIterator'; 5 | 6 | class ObjectFieldIndex { 7 | name: string; 8 | store: LevelInterface; 9 | 10 | constructor(params: ObjectFieldIndexParams) { 11 | this.name = gen2DKey(params.typeName, params.fieldName); 12 | this.store = params.store; 13 | } 14 | 15 | lookup(value: string): Promise { 16 | const key = gen2DKey(this.name, value); 17 | return this.store.get(key); 18 | } 19 | 20 | unindex(value: string, id: string, batch: LevelChainInterface): LevelChainInterface { 21 | const key = gen2DKey(this.name, value); 22 | return batch.del(key); 23 | } 24 | 25 | async index(value: string, id: string, batch: LevelChainInterface): Promise { 26 | const key = gen2DKey(this.name, value); 27 | const prev = await this.store.get(key); 28 | if (prev !== null) { 29 | throw new Error(`Duplicate index value on "${key}"`); 30 | } 31 | 32 | return batch.put(key, id); 33 | } 34 | 35 | reindex( 36 | prev: string, 37 | value: string, 38 | id: string, 39 | batch: LevelChainInterface 40 | ): Promise { 41 | return this.index(value, id, this.unindex(prev, id, batch)); 42 | } 43 | 44 | iterator({ reverse = false, limit = -1 }: IteratorOptions = {}) { 45 | const { name, store } = this; 46 | const range = rangeOfKey(name); 47 | range.reverse = reverse; 48 | range.limit = limit; 49 | const iterator = store.iterator(range); 50 | return new AsyncIdIterator(iterator); 51 | } 52 | } 53 | 54 | export default ObjectFieldIndex; 55 | -------------------------------------------------------------------------------- /src/relational/ObjectFieldOrdinal.ts: -------------------------------------------------------------------------------- 1 | import { LevelInterface, LevelChainInterface } from '../level'; 2 | import { ObjectFieldIndexParams, IteratorOptions } from './types'; 3 | import { gen2DKey, gen3DKey, rangeOfKey } from './keys'; 4 | import AsyncIdIterator from './AsyncIdIterator'; 5 | 6 | class ObjectFieldOrdinal { 7 | name: string; 8 | store: LevelInterface; 9 | 10 | constructor(params: ObjectFieldIndexParams) { 11 | this.name = gen2DKey(params.typeName, params.fieldName); 12 | this.store = params.store; 13 | } 14 | 15 | unindex(value: string, id: string, batch: LevelChainInterface): LevelChainInterface { 16 | const key = gen3DKey(this.name, value, id); 17 | return batch.del(key); 18 | } 19 | 20 | index(value: string, id: string, batch: LevelChainInterface) { 21 | const key = gen3DKey(this.name, value, id); 22 | return batch.put(key, id); 23 | } 24 | 25 | reindex(prev: string, value: string, id: string, batch: LevelChainInterface) { 26 | return this.index(value, id, this.unindex(prev, id, batch)); 27 | } 28 | 29 | iterator(value: string, { reverse = false, limit = -1 }: IteratorOptions = {}) { 30 | const { name, store } = this; 31 | const range = rangeOfKey(gen2DKey(name, value)); 32 | range.reverse = reverse; 33 | range.limit = limit; 34 | const iterator = store.iterator(range); 35 | return new AsyncIdIterator(iterator); 36 | } 37 | } 38 | 39 | export default ObjectFieldOrdinal; 40 | -------------------------------------------------------------------------------- /src/relational/ObjectTable.ts: -------------------------------------------------------------------------------- 1 | import { LevelInterface } from '../level'; 2 | import { FieldDefinition, Serializer, Deserializer } from '../internal'; 3 | 4 | import { sortFields, nextObjectOrNull, closeIter } from './helpers'; 5 | import { genId, gen2DKey, gen3DKey, rangeOfKey } from './keys'; 6 | import { mutexBatchFactory, MutexBatch } from './mutexBatch'; 7 | import ObjectFieldIndex from './ObjectFieldIndex'; 8 | import ObjectFieldOrdinal from './ObjectFieldOrdinal'; 9 | import AsyncObjectIterator from './AsyncObjectIterator'; 10 | 11 | import { 12 | ObjectLike, 13 | ObjectTableParams, 14 | IteratorOptions, 15 | FieldEncoderMap, 16 | FieldIndexMap, 17 | FieldOrdinalMap, 18 | } from './types'; 19 | 20 | class ObjectTable { 21 | name: string; 22 | store: LevelInterface; 23 | mutexBatch: MutexBatch; 24 | 25 | fields: FieldDefinition[]; 26 | fieldsLength: number; 27 | fieldNames: K[]; 28 | 29 | encoders: Serializer[]; 30 | decoders: Deserializer[]; 31 | 32 | encoderMap: FieldEncoderMap; 33 | index: FieldIndexMap; 34 | ordinal: FieldOrdinalMap; 35 | 36 | constructor(params: ObjectTableParams) { 37 | this.name = params.name; 38 | this.store = params.store; 39 | this.mutexBatch = mutexBatchFactory(this.store); 40 | 41 | this.fields = sortFields(params.fields); 42 | const fieldsLength = (this.fieldsLength = this.fields.length); 43 | this.fieldNames = new Array(fieldsLength); 44 | 45 | this.encoders = new Array(fieldsLength); 46 | this.decoders = new Array(fieldsLength); 47 | 48 | this.encoderMap = {} as FieldEncoderMap; 49 | this.index = {} as FieldIndexMap; 50 | this.ordinal = {} as FieldOrdinalMap; 51 | 52 | for (let i = 0; i < fieldsLength; i++) { 53 | const field = this.fields[i]; 54 | const fieldName = (this.fieldNames[i] = field.name); 55 | 56 | this.encoders[i] = field.encode; 57 | this.decoders[i] = field.decode; 58 | this.encoderMap[fieldName] = field.encode; 59 | 60 | const indexParams = { 61 | typeName: this.name, 62 | fieldName: fieldName, 63 | store: this.store, 64 | }; 65 | 66 | if (field.isUnique) { 67 | this.index[fieldName] = new ObjectFieldIndex(indexParams); 68 | } else if (field.isOrdinal) { 69 | this.ordinal[fieldName] = new ObjectFieldOrdinal(indexParams); 70 | } 71 | } 72 | } 73 | 74 | iterator({ reverse = false, limit = -1 }: IteratorOptions = {}): AsyncObjectIterator { 75 | const { store, name, fieldsLength, fieldNames, decoders } = this; 76 | const range = rangeOfKey(name); 77 | range.reverse = reverse; 78 | range.limit = limit > 0 ? limit * fieldsLength : limit; 79 | const iterator = store.iterator(range); 80 | return new AsyncObjectIterator(fieldNames, decoders, iterator); 81 | } 82 | 83 | async getObject(id: string) { 84 | const { store, name, fieldNames, decoders } = this; 85 | const range = rangeOfKey(gen2DKey(name, id)); 86 | const iterator = store.iterator(range); 87 | const res = await nextObjectOrNull(fieldNames, decoders, iterator); 88 | await closeIter(iterator); 89 | return res; 90 | } 91 | 92 | async getIdByIndex(where: Partial): Promise { 93 | const { store, name, fieldsLength, index, fieldNames, encoders } = this; 94 | 95 | let firstId = null; 96 | if (where.id !== null) { 97 | const key = gen3DKey(name, where.id, 'id'); 98 | firstId = await store.get(key); 99 | } 100 | 101 | for (let i = 0, l = fieldsLength; i < l; i++) { 102 | const fieldName = fieldNames[i]; 103 | const fieldIndex = index[fieldName]; 104 | if (fieldIndex !== undefined && fieldName in where) { 105 | const value = encoders[i](where[fieldName]); 106 | const id = await fieldIndex.lookup(value); 107 | 108 | if (id === null || (firstId !== null && firstId !== id)) { 109 | return null; 110 | } else { 111 | firstId = id; 112 | } 113 | } 114 | } 115 | 116 | return firstId; 117 | } 118 | 119 | async findObjectByIndex(where: Partial): Promise { 120 | const id = await this.getIdByIndex(where); 121 | if (id === null) { 122 | return null; 123 | } 124 | 125 | return await this.getObject(id); 126 | } 127 | 128 | async findObjectsByOrdinal(fieldName: K, where: T[K]): Promise { 129 | const ordinal = this.ordinal[fieldName]; 130 | const encoder = this.encoderMap[fieldName]; 131 | const value = encoder(where); 132 | const iterator = ordinal.iterator(value); 133 | const res = []; 134 | for await (const id of iterator) { 135 | res.push(await this.getObject(id)); 136 | } 137 | 138 | return res; 139 | } 140 | 141 | async createObject(data: Partial): Promise { 142 | const { name, index, ordinal, fields, fieldsLength, encoders } = this; 143 | const id = (data.id = genId()); 144 | data.createdAt = data.updatedAt = new Date(); 145 | 146 | await this.mutexBatch(async b => { 147 | let batch = b; 148 | 149 | for (let i = 0, l = fieldsLength; i < l; i++) { 150 | const { name: fieldName, defaultValue } = fields[i]; 151 | const fieldIndex = index[fieldName]; 152 | const fieldOrdinal = ordinal[fieldName]; 153 | const encode = encoders[i]; 154 | 155 | const fallbackValue = defaultValue === undefined ? null : defaultValue; 156 | const shouldDefault = !(fieldName in data); 157 | const value = encode(shouldDefault ? fallbackValue : data[fieldName]); 158 | 159 | batch = batch.put(gen3DKey(name, id, fieldName), value); 160 | if (fieldIndex !== undefined) { 161 | batch = await fieldIndex.index(value, id, batch); 162 | } else if (fieldOrdinal !== undefined) { 163 | batch = fieldOrdinal.index(value, id, batch); 164 | } 165 | } 166 | 167 | return batch; 168 | }); 169 | 170 | return data as T; 171 | } 172 | 173 | async updateObject(where: Partial, data: Partial): Promise { 174 | const id = await this.getIdByIndex(where); 175 | if (id === null) { 176 | throw new Error('No object has been found to update'); 177 | } 178 | 179 | const { name, index, ordinal, fields, fieldsLength, encoders } = this; 180 | const prev = await this.getObject(id); 181 | data.updatedAt = new Date(); 182 | 183 | await this.mutexBatch(async b => { 184 | let batch = b; 185 | 186 | for (let i = 0, l = fieldsLength; i < l; i++) { 187 | const { name: fieldName, isReadOnly, defaultValue } = fields[i]; 188 | if (isReadOnly || !(fieldName in data)) { 189 | data[fieldName] = prev[fieldName]; 190 | } else { 191 | const fieldIndex = index[fieldName]; 192 | const fieldOrdinal = ordinal[fieldName]; 193 | const encode = encoders[i]; 194 | 195 | const prevVal = encode(prev[fieldName]); 196 | const input = data[fieldName]; 197 | const shouldDefault = !!defaultValue && input === null; 198 | const value = encode(shouldDefault ? defaultValue : input); 199 | 200 | batch = batch.put(gen3DKey(name, id, fieldName), value); 201 | if (fieldIndex !== undefined) { 202 | batch = await fieldIndex.reindex(prevVal, value, id, batch); 203 | } else if (fieldOrdinal !== undefined) { 204 | batch = fieldOrdinal.reindex(prevVal, value, id, batch); 205 | } 206 | } 207 | } 208 | 209 | return batch; 210 | }); 211 | 212 | return data as T; 213 | } 214 | 215 | async deleteObject(where: Partial): Promise { 216 | const id = await this.getIdByIndex(where); 217 | if (id === null) { 218 | throw new Error('No object has been found to delete'); 219 | } 220 | 221 | const { name, index, ordinal, fields, fieldsLength, encoders } = this; 222 | const data = await this.getObject(id); 223 | 224 | await this.mutexBatch(async b => { 225 | let batch = b; 226 | 227 | for (let i = 0, l = fieldsLength; i < l; i++) { 228 | const { name: fieldName } = fields[i]; 229 | const fieldIndex = index[fieldName]; 230 | const fieldOrdinal = ordinal[fieldName]; 231 | const encode = encoders[i]; 232 | 233 | batch = batch.del(gen3DKey(name, id, fieldName)); 234 | 235 | if (fieldIndex !== undefined) { 236 | batch = fieldIndex.unindex(encode(data[fieldName]), id, batch); 237 | } else if (fieldOrdinal !== undefined) { 238 | batch = fieldOrdinal.unindex(encode(data[fieldName]), id, batch); 239 | } 240 | } 241 | 242 | return batch; 243 | }); 244 | 245 | return data; 246 | } 247 | } 248 | 249 | export default ObjectTable; 250 | -------------------------------------------------------------------------------- /src/relational/__tests__/ObjectFieldIndex.test.ts: -------------------------------------------------------------------------------- 1 | import memdown from 'memdown'; 2 | import level, { LevelInterface } from '../../level'; 3 | import { genId, gen3DKey } from '../keys'; 4 | import ObjectFieldIndex from '../ObjectFieldIndex'; 5 | 6 | const typeName = 'Type'; 7 | const fieldName = 'field'; 8 | 9 | describe('level/ObjectFieldIndex', () => { 10 | let store: LevelInterface; 11 | let index: ObjectFieldIndex; 12 | 13 | beforeEach(() => { 14 | store = level(memdown()); 15 | index = new ObjectFieldIndex({ typeName, fieldName, store }); 16 | }); 17 | 18 | it('can retrieve indexed values', async () => { 19 | const id = genId(); 20 | const value = 'test'; 21 | await store.put(gen3DKey(typeName, fieldName, value), id); 22 | const actual = await index.lookup(value); 23 | expect(actual).toBe(id); 24 | }); 25 | 26 | it('can index values', async () => { 27 | const id = genId(); 28 | const value = 'test'; 29 | await (await index.index(value, id, store.batch())).write(); 30 | const actual = await store.get(gen3DKey(typeName, fieldName, value)); 31 | expect(actual).toBe(id); 32 | }); 33 | 34 | it('can index and lookup values', async () => { 35 | const id = genId(); 36 | const value = 'test'; 37 | 38 | await (await index.index(value, id, store.batch())).write(); 39 | const actual = await index.lookup(value); 40 | 41 | expect(actual).toBe(id); 42 | }); 43 | 44 | it('throws when a value is already indexed', async () => { 45 | expect.assertions(1); 46 | 47 | const id = genId(); 48 | const value = 'test'; 49 | 50 | await store.put(gen3DKey(typeName, fieldName, value), id); 51 | 52 | try { 53 | await (await index.index(value, id, store.batch())).write(); 54 | } catch (err) { 55 | expect(err).toMatchInlineSnapshot(`[Error: Duplicate index value on "Type!field!test"]`); 56 | } 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /src/relational/__tests__/ObjectTable.test.ts: -------------------------------------------------------------------------------- 1 | import memdown from 'memdown'; 2 | import level, { LevelInterface } from '../../level'; 3 | import { makeFields } from '../../internal'; 4 | import { genId, gen3DKey } from '../keys'; 5 | import ObjectTable from '../ObjectTable'; 6 | 7 | type Test = { 8 | id: string; 9 | createdAt: Date; 10 | updatedAt: Date; 11 | test: string; 12 | }; 13 | 14 | const name = 'Test'; 15 | 16 | const fields = makeFields({ 17 | name, 18 | isEmbedded: false, 19 | isEnum: false, 20 | fields: [ 21 | { 22 | name: 'test', 23 | type: 'String', 24 | isList: false, 25 | isRequired: true, 26 | isUnique: true, 27 | isReadOnly: false, 28 | }, 29 | ] as any, 30 | }) as any; 31 | 32 | describe('level/ObjectTable', () => { 33 | let store: LevelInterface; 34 | let table: ObjectTable; 35 | 36 | beforeEach(() => { 37 | store = level(memdown()); 38 | table = new ObjectTable({ name, store, fields }); 39 | }); 40 | 41 | it('can create objects', async () => { 42 | const dataA = await table.createObject({ 43 | id: 'should be replaced', 44 | test: 'test-1', 45 | }); 46 | 47 | const dataB = await table.createObject({ 48 | id: 'should be replaced', 49 | test: 'test-2', 50 | }); 51 | 52 | expect(dataB.test).toBe('test-2'); 53 | expect(dataB.id.length).toBe(25); 54 | expect(await store.get(gen3DKey(name, dataB.id, 'test'))).toBe('test-2'); 55 | 56 | expect(dataA.test).toBe('test-1'); 57 | expect(dataA.id.length).toBe(25); 58 | expect(await store.get(gen3DKey(name, dataA.id, 'test'))).toBe('test-1'); 59 | }); 60 | 61 | it('can update objects', async () => { 62 | const id = genId(); 63 | 64 | await store.put(gen3DKey(name, id, 'id'), id); 65 | await store.put(gen3DKey(name, id, 'createdAt'), '1'); 66 | await store.put(gen3DKey(name, id, 'updatedAt'), '2'); 67 | await store.put(gen3DKey(name, id, 'test'), 'manual creation'); 68 | 69 | await table.updateObject({ id }, { test: 'updated' }); 70 | 71 | expect(await store.get(gen3DKey(name, id, 'test'))).toBe('updated'); 72 | expect(await store.get(gen3DKey(name, id, 'id'))).toBe(id); 73 | expect(await store.get(gen3DKey(name, id, 'createdAt'))).toBe('1'); 74 | expect(await store.get(gen3DKey(name, id, 'updatedAt'))).not.toBe('2'); 75 | }); 76 | 77 | it('can delete objects', async () => { 78 | const id = genId(); 79 | 80 | await store.put(gen3DKey(name, id, 'id'), id); 81 | await store.put(gen3DKey(name, id, 'createdAt'), '1'); 82 | await store.put(gen3DKey(name, id, 'updatedAt'), '2'); 83 | await store.put(gen3DKey(name, id, 'test'), 'manual creation'); 84 | 85 | await table.deleteObject({ id }); 86 | 87 | expect(await store.get(gen3DKey(name, id, 'id'))).toBe(null); 88 | expect(await store.get(gen3DKey(name, id, 'test'))).toBe(null); 89 | expect(await store.get(gen3DKey(name, id, 'createdAt'))).toBe(null); 90 | expect(await store.get(gen3DKey(name, id, 'updatedAt'))).toBe(null); 91 | }); 92 | 93 | it('can get objects by id', async () => { 94 | const id = genId(); 95 | 96 | await store.put(gen3DKey(name, id, 'id'), id); 97 | await store.put(gen3DKey(name, id, 'createdAt'), '1'); 98 | await store.put(gen3DKey(name, id, 'updatedAt'), '2'); 99 | await store.put(gen3DKey(name, id, 'test'), 'manual creation'); 100 | 101 | expect(await table.getObject(id)).toEqual({ 102 | id, 103 | createdAt: new Date(1), 104 | updatedAt: new Date(2), 105 | test: 'manual creation', 106 | }); 107 | }); 108 | 109 | it('can store then retrieve objects', async () => { 110 | const data = await table.createObject({ 111 | id: 'should be replaced', 112 | test: 'test-1', 113 | }); 114 | 115 | const actual = await table.getObject(data.id); 116 | 117 | expect(actual).toEqual(data); 118 | expect(actual).toEqual({ 119 | id: expect.any(String), 120 | createdAt: expect.any(Date), 121 | updatedAt: expect.any(Date), 122 | test: 'test-1', 123 | }); 124 | }); 125 | 126 | it('can store then retrieve IDs by indexed values', async () => { 127 | const expected = await table.createObject({ test: 'test-1' }); 128 | const actualId = await table.getIdByIndex({ test: 'test-1' }); 129 | expect(actualId).toEqual(expected.id); 130 | }); 131 | 132 | it('can store & update, then retrieve IDs by new indexed values', async () => { 133 | const obj = await table.createObject({ test: 'test-1' }); 134 | const updated = await table.updateObject({ id: obj.id }, { test: 'updated' }); 135 | 136 | expect(updated).toEqual({ 137 | ...obj, 138 | updatedAt: expect.any(Date), 139 | test: 'updated', 140 | }); 141 | 142 | expect(await table.getIdByIndex({ test: 'test-1' })).toBe(null); 143 | expect(await table.getIdByIndex({ test: 'updated' })).toBe(obj.id); 144 | expect(await table.getIdByIndex({ id: updated.id })).toBe(obj.id); 145 | }); 146 | 147 | it('can delete objects and clean up indexed values', async () => { 148 | const id = genId(); 149 | 150 | await store.put(gen3DKey(name, id, 'id'), id); 151 | await store.put(gen3DKey(name, id, 'createdAt'), '1'); 152 | await store.put(gen3DKey(name, id, 'updatedAt'), '2'); 153 | await store.put(gen3DKey(name, id, 'test'), 'manual creation'); 154 | 155 | await table.deleteObject({ id }); 156 | 157 | expect(await table.getIdByIndex({ test: 'test-1' })).toBe(null); 158 | expect(await table.getIdByIndex({ id })).toBe(null); 159 | }); 160 | 161 | it('can iterate over created objects', async () => { 162 | const data = [{ test: 'x' }, { test: 'y' }, { test: 'z' }]; 163 | 164 | await Promise.all(data.map(x => table.createObject(x))); 165 | 166 | let size = 0; 167 | for await (const item of table.iterator()) { 168 | expect(item).toEqual({ 169 | id: expect.any(String), 170 | createdAt: expect.any(Date), 171 | updatedAt: expect.any(Date), 172 | test: expect.any(String), 173 | }); 174 | 175 | size++; 176 | } 177 | 178 | expect(size).toBe(data.length); 179 | }); 180 | 181 | it('can iterate and abort early', async () => { 182 | const data = [{ test: 'x' }, { test: 'y' }, { test: 'z' }]; 183 | 184 | await Promise.all(data.map(x => table.createObject(x))); 185 | 186 | for await (const item of table.iterator({})) { 187 | expect(item).toEqual({ 188 | id: expect.any(String), 189 | createdAt: expect.any(Date), 190 | updatedAt: expect.any(Date), 191 | test: expect.any(String), 192 | }); 193 | 194 | break; 195 | } 196 | }); 197 | }); 198 | -------------------------------------------------------------------------------- /src/relational/__tests__/helpers.test.ts: -------------------------------------------------------------------------------- 1 | import memdown from 'memdown'; 2 | import level, { LevelInterface } from '../../level'; 3 | import { nextOrNull, closeIter } from '../helpers'; 4 | 5 | describe('level/helpers', () => { 6 | let store: LevelInterface; 7 | 8 | beforeEach(() => { 9 | store = level(memdown()); 10 | }); 11 | 12 | describe('nextOrNull & closeIter', () => { 13 | it('works', async () => { 14 | const entryA = ['test-key-1', 'test-value-1']; 15 | const entryB = ['test-key-2', 'test-value-2']; 16 | await Promise.all([store.put(entryA[0], entryA[1]), store.put(entryB[0], entryB[1])]); 17 | const iterator = store.iterator(); 18 | 19 | const output = [ 20 | await nextOrNull(iterator), 21 | await nextOrNull(iterator), 22 | await nextOrNull(iterator), 23 | ]; 24 | 25 | expect(output).toEqual([entryA, entryB, null]); 26 | 27 | await closeIter(iterator); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/relational/__tests__/keys.test.ts: -------------------------------------------------------------------------------- 1 | import { genId, gen2DKey, gen3DKey, rangeOfKey, idOfKey, fieldOfKey } from '../keys'; 2 | 3 | describe('level/keys', () => { 4 | describe('genId', () => { 5 | it('works', () => { 6 | const id = genId(); 7 | expect(id.charAt(0)).toBe('c'); 8 | expect(id.length).toBe(25); 9 | }); 10 | }); 11 | 12 | describe('gen2DKey', () => { 13 | it('works', () => { 14 | expect(gen2DKey('first', 'second')).toMatchInlineSnapshot(`"first!second"`); 15 | }); 16 | }); 17 | 18 | describe('gen3DKey', () => { 19 | it('works', () => { 20 | expect(gen3DKey('first', 'second', 'third')).toMatchInlineSnapshot(`"first!second!third"`); 21 | }); 22 | }); 23 | 24 | describe('rangeOfKey', () => { 25 | it('works', () => { 26 | expect(rangeOfKey('test')).toEqual({ 27 | gt: 'test!', 28 | lt: 'test!\xff', 29 | }); 30 | }); 31 | }); 32 | 33 | describe('idOfKey', () => { 34 | it('works', () => { 35 | const name = 'Hello'; 36 | const id = '1234567890123456789012345'; 37 | const key = 'Hello!' + id + '!test'; 38 | expect(idOfKey(name, key)).toBe(id); 39 | }); 40 | }); 41 | 42 | describe('fieldOfKey', () => { 43 | it('works', () => { 44 | const name = 'Hello'; 45 | const key = 'Hello!1234567890123456789012345!test'; 46 | expect(fieldOfKey(name, key)).toBe('test'); 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/relational/__tests__/mutexBatch.test.ts: -------------------------------------------------------------------------------- 1 | import memdown from 'memdown'; 2 | import level, { LevelInterface } from '../../level'; 3 | import { mutexBatchFactory, MutexBatch } from '../mutexBatch'; 4 | 5 | describe('level/mutexBatch', () => { 6 | let store: LevelInterface; 7 | let mutexBatch: MutexBatch; 8 | 9 | beforeEach(() => { 10 | store = level(memdown()); 11 | mutexBatch = mutexBatchFactory(store); 12 | }); 13 | 14 | it('runs a batch', async () => { 15 | await mutexBatch(batch => { 16 | return batch.put('test-key', 'test-val'); 17 | }); 18 | 19 | expect(await store.get('test-key')).toBe('test-val'); 20 | }); 21 | 22 | it('runs multiple batches in series', async () => { 23 | await store.put('test', '1'); 24 | 25 | await Promise.all([ 26 | mutexBatch(async batch => { 27 | expect(await store.get('test')).toBe('1'); 28 | await store.put('test', '2'); 29 | return batch.put('test-key', 'first'); 30 | }), 31 | mutexBatch(async batch => { 32 | expect(await store.get('test')).toBe('2'); 33 | await store.put('test', '3'); 34 | return batch.put('test-key', 'second'); 35 | }), 36 | ]); 37 | 38 | expect(await store.get('test-key')).toBe('second'); 39 | expect(await store.get('test')).toBe('3'); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/relational/helpers.ts: -------------------------------------------------------------------------------- 1 | import { AbstractIterator } from 'abstract-leveldown'; 2 | import { FieldDefinition, Deserializer } from '../internal'; 3 | import { ObjectLike } from './types'; 4 | 5 | export const nextOrNull = ( 6 | iter: AbstractIterator 7 | ): Promise<[string, string] | null> => 8 | new Promise((resolve, reject) => { 9 | iter.next((err, key: void | string, value: string) => { 10 | if (err) { 11 | iter.end(() => reject(err)); 12 | } else if (key === undefined) { 13 | resolve(null); 14 | } else { 15 | resolve([key as string, value]); 16 | } 17 | }); 18 | }); 19 | 20 | export const closeIter = (iter: AbstractIterator) => 21 | new Promise((resolve, reject) => { 22 | iter.end(err => { 23 | if (err) { 24 | reject(err); 25 | } else { 26 | resolve(); 27 | } 28 | }); 29 | }); 30 | 31 | export const nextObjectOrNull = async ( 32 | keys: K[], 33 | decoders: Deserializer[], 34 | iter: AbstractIterator 35 | ): Promise => { 36 | const res = {} as T; 37 | 38 | for (let i = 0, s = keys.length; i < s; i++) { 39 | const entry = await nextOrNull(iter); 40 | if (entry === null) { 41 | return null; 42 | } else { 43 | res[keys[i]] = decoders[i](entry[1]) as T[K]; 44 | } 45 | } 46 | 47 | return res; 48 | }; 49 | 50 | export const sortFields = (fields: FieldDefinition[]): FieldDefinition[] => { 51 | return fields.slice().sort((a, b) => { 52 | if (a.name < b.name) { 53 | return -1; 54 | } else if (a.name > b.name) { 55 | return 1; 56 | } else { 57 | return 0; 58 | } 59 | }); 60 | }; 61 | -------------------------------------------------------------------------------- /src/relational/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ObjectTable } from './ObjectTable'; 2 | -------------------------------------------------------------------------------- /src/relational/keys.ts: -------------------------------------------------------------------------------- 1 | import { AbstractIteratorOptions } from 'abstract-leveldown'; 2 | import cuid from 'cuid'; 3 | 4 | const CHAR_END = '\xff'; 5 | const CHAR_SEPARATOR = '!'; 6 | const SEPARATOR_LENGTH = 1; 7 | const ID_LENGTH = 25; 8 | 9 | export const genId = (): string => cuid().slice(0, ID_LENGTH); 10 | 11 | export const gen2DKey = (a: string, b: any): string => a + CHAR_SEPARATOR + (b as string); 12 | 13 | export const gen3DKey = (a: string, b: any, c: any): string => 14 | a + CHAR_SEPARATOR + (b as string) + CHAR_SEPARATOR + (c as string); 15 | 16 | export const rangeOfKey = (input: string): AbstractIteratorOptions => { 17 | const gt = input + CHAR_SEPARATOR; 18 | const lt = gt + CHAR_END; 19 | return { gt, lt }; 20 | }; 21 | 22 | export const idOfKey = (name: string, key: any): string => { 23 | const start = name.length + SEPARATOR_LENGTH; 24 | const end = start + ID_LENGTH; 25 | return (key as string).slice(start, end); 26 | }; 27 | 28 | export const fieldOfKey = (name: string, key: any): string => 29 | (key as string).slice(name.length + 2 * SEPARATOR_LENGTH + ID_LENGTH); 30 | -------------------------------------------------------------------------------- /src/relational/mutexBatch.ts: -------------------------------------------------------------------------------- 1 | import { LevelInterface, LevelChainInterface } from '../level'; 2 | 3 | type BatchFn = (batch: LevelChainInterface) => LevelChainInterface | Promise; 4 | 5 | export interface MutexBatch { 6 | (fn: BatchFn): Promise; 7 | } 8 | 9 | export const mutexBatchFactory = (store: LevelInterface): MutexBatch => { 10 | let mutex: void | Promise; 11 | 12 | return async function mutexBatch(fn: BatchFn) { 13 | if (mutex !== undefined) { 14 | await mutex; 15 | } 16 | 17 | await (mutex = (async () => { 18 | const batch = store.batch(); 19 | try { 20 | await fn(batch); 21 | await batch.write(); 22 | } finally { 23 | mutex = undefined; 24 | } 25 | })()); 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /src/relational/types.ts: -------------------------------------------------------------------------------- 1 | import { AbstractIterator } from 'abstract-leveldown'; 2 | import { FieldDefinition, Serializer } from '../internal'; 3 | import { LevelInterface } from '../level'; 4 | import ObjectFieldIndex from './ObjectFieldIndex'; 5 | import ObjectFieldOrdinal from './ObjectFieldOrdinal'; 6 | 7 | export type Iterator = AbstractIterator; 8 | 9 | export interface ObjectFieldIndexParams { 10 | typeName: string; 11 | fieldName: K; 12 | store: LevelInterface; 13 | } 14 | 15 | export interface EdgeRelationshipParams { 16 | relationName: null | string; 17 | typeNames: [string, string]; 18 | store: LevelInterface; 19 | } 20 | 21 | export interface ObjectLike { 22 | id: string; 23 | createdAt: Date; 24 | updatedAt: Date; 25 | } 26 | 27 | export interface ObjectTableParams { 28 | name: string; 29 | fields: FieldDefinition[]; 30 | store: LevelInterface; 31 | } 32 | 33 | export interface IteratorOptions { 34 | reverse?: boolean; 35 | limit?: number; 36 | } 37 | 38 | export type FieldEncoderMap = { [K in keyof T]?: Serializer }; 39 | export type FieldIndexMap = { [K in keyof T]?: ObjectFieldIndex }; 40 | export type FieldOrdinalMap = { [K in keyof T]?: ObjectFieldOrdinal }; 41 | -------------------------------------------------------------------------------- /src/schema/ResolverMap.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLObjectType, GraphQLInputObjectType } from 'graphql/type'; 2 | import { LevelInterface } from '../level'; 3 | import { ObjectTable } from '../relational'; 4 | import { SchemaDefinition } from '../internal'; 5 | import { 6 | FieldResolverMap, 7 | ResolverTypeName, 8 | InputObjectTypeMap, 9 | ObjectTypeMap, 10 | FieldConfig, 11 | } from './types'; 12 | 13 | export class ResolverMap { 14 | store: LevelInterface; 15 | schema: SchemaDefinition; 16 | tableByName: Record>; 17 | 18 | objectTypes: ObjectTypeMap = {}; 19 | connectionInputs: InputObjectTypeMap = {}; 20 | resolvers: FieldResolverMap = { 21 | Query: {}, 22 | Mutation: {}, 23 | }; 24 | 25 | constructor(store: LevelInterface, schema: SchemaDefinition) { 26 | this.store = store; 27 | this.schema = schema; 28 | 29 | this.tableByName = schema.objects.reduce((acc, obj) => { 30 | acc[obj.typeName] = new ObjectTable({ 31 | name: obj.typeName, 32 | fields: obj.fields, 33 | store, 34 | }); 35 | 36 | return acc; 37 | }, {}); 38 | } 39 | 40 | getTable(typeName: string) { 41 | return this.tableByName[typeName]; 42 | } 43 | 44 | addField(typeName: ResolverTypeName, fieldName: string, conf: FieldConfig) { 45 | if (this.resolvers[typeName] === undefined) { 46 | this.resolvers[typeName] = {}; 47 | } 48 | 49 | this.resolvers[typeName][fieldName] = conf; 50 | } 51 | 52 | addObjectType(typeName: string, objType: GraphQLObjectType) { 53 | this.objectTypes[typeName] = objType; 54 | } 55 | 56 | getObjectType(typeName: string) { 57 | return this.objectTypes[typeName]; 58 | } 59 | 60 | addConnectionInput(typeName: string, inputType: GraphQLInputObjectType) { 61 | this.connectionInputs[typeName] = inputType; 62 | } 63 | 64 | getConnectionInput(typeName: string) { 65 | return this.connectionInputs[typeName]; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/schema/field.ts: -------------------------------------------------------------------------------- 1 | import { FieldDefinition } from '../internal'; 2 | import { getScalarForString, list, nonNull } from './scalar'; 3 | import { FieldConfig } from './types'; 4 | 5 | const genFieldType = (field: FieldDefinition, withRequired) => { 6 | const fieldScalar = getScalarForString(field.type); 7 | let scalar = field.isList ? list(fieldScalar) : fieldScalar; 8 | scalar = field.isRequired && withRequired ? nonNull(scalar) : scalar; 9 | return scalar; 10 | }; 11 | 12 | export const genFieldConf = (field: FieldDefinition, withRequired = true): FieldConfig => { 13 | return { type: genFieldType(field, withRequired) }; 14 | }; 15 | -------------------------------------------------------------------------------- /src/schema/index.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema, GraphQLObjectType } from 'graphql/type'; 2 | import { LevelInterface } from '../level'; 3 | import { SchemaDefinition } from '../internal'; 4 | import { ResolverMap } from './ResolverMap'; 5 | import { addObjectResolvers } from './object'; 6 | 7 | export const genSchema = (store: LevelInterface, definition: SchemaDefinition) => { 8 | const ctx = new ResolverMap(store, definition); 9 | 10 | for (const obj of definition.objects) { 11 | addObjectResolvers(ctx, obj); 12 | } 13 | 14 | return new GraphQLSchema({ 15 | query: new GraphQLObjectType({ 16 | name: 'Query', 17 | fields: ctx.resolvers.Query, 18 | }), 19 | mutation: new GraphQLObjectType({ 20 | name: 'Mutation', 21 | fields: ctx.resolvers.Mutation, 22 | }), 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /src/schema/input.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLInputObjectType } from 'graphql/type'; 2 | import { ObjectDefinition } from '../internal'; 3 | import { ResolverMap } from './ResolverMap'; 4 | import { genFieldConf } from './field'; 5 | import { list } from './scalar'; 6 | 7 | export const genCreateInput = (ctx: ResolverMap, obj: ObjectDefinition) => { 8 | const { typeName, fields, relations } = obj; 9 | const inputName = `${typeName}Create`; 10 | 11 | return new GraphQLInputObjectType({ 12 | name: inputName, 13 | fields: () => { 14 | const fieldMap = {}; 15 | 16 | for (const field of fields) { 17 | if (!field.isSystemField) { 18 | fieldMap[field.name] = genFieldConf(field); 19 | } 20 | } 21 | 22 | for (const relation of relations) { 23 | const selfField = relation.getSelfField(obj); 24 | const input = ctx.getConnectionInput(selfField.relatedDefinition.typeName); 25 | 26 | fieldMap[selfField.fieldName] = { 27 | type: selfField.isList ? list(input) : input, 28 | }; 29 | } 30 | 31 | return fieldMap; 32 | }, 33 | }); 34 | }; 35 | 36 | export const genUpdateInput = (ctx: ResolverMap, obj: ObjectDefinition) => { 37 | const { typeName, fields, relations } = obj; 38 | const inputName = `${typeName}Update`; 39 | 40 | return new GraphQLInputObjectType({ 41 | name: inputName, 42 | fields: () => { 43 | const fieldMap = {}; 44 | 45 | for (const field of fields) { 46 | if (!field.isReadOnly && !field.isSystemField) { 47 | fieldMap[field.name] = genFieldConf(field, false); 48 | } 49 | } 50 | 51 | for (const relation of relations) { 52 | const selfField = relation.getSelfField(obj); 53 | const input = ctx.getConnectionInput(selfField.relatedDefinition.typeName); 54 | 55 | fieldMap[selfField.fieldName] = { 56 | type: selfField.isList ? list(input) : input, 57 | }; 58 | } 59 | 60 | return fieldMap; 61 | }, 62 | }); 63 | }; 64 | 65 | export const genUniqueWhereInput = (obj: ObjectDefinition) => { 66 | const { typeName, fields } = obj; 67 | const inputName = `${typeName}WhereUnique`; 68 | const fieldMap = {}; 69 | 70 | for (const field of fields) { 71 | if (field.name === 'id' || field.isUnique) { 72 | fieldMap[field.name] = genFieldConf(field, false); 73 | } 74 | } 75 | 76 | return new GraphQLInputObjectType({ 77 | name: inputName, 78 | fields: fieldMap, 79 | }); 80 | }; 81 | 82 | export const genConnectionInput = ( 83 | obj: ObjectDefinition, 84 | whereInput: GraphQLInputObjectType, 85 | createInput: GraphQLInputObjectType 86 | ) => { 87 | const { typeName } = obj; 88 | 89 | return new GraphQLInputObjectType({ 90 | name: `${typeName}Connection`, 91 | fields: { 92 | where: { type: whereInput }, 93 | create: { type: createInput }, 94 | }, 95 | }); 96 | }; 97 | -------------------------------------------------------------------------------- /src/schema/object.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLObjectType } from 'graphql/type'; 2 | import { ObjectDefinition } from '../internal'; 3 | import { ResolverMap } from './ResolverMap'; 4 | import { nonNull } from './scalar'; 5 | import { genFieldConf } from './field'; 6 | import { genCreateInput, genUpdateInput, genUniqueWhereInput, genConnectionInput } from './input'; 7 | import { genRelationshipFieldConfig } from './relationship'; 8 | 9 | const genObjectType = (ctx: ResolverMap, obj: ObjectDefinition) => { 10 | const { typeName, fields, relations } = obj; 11 | 12 | return new GraphQLObjectType({ 13 | name: typeName, 14 | fields: () => { 15 | const fieldMap = {}; 16 | for (const field of fields) { 17 | const { name } = field; 18 | fieldMap[name] = genFieldConf(field); 19 | } 20 | 21 | for (const relation of relations) { 22 | const field = relation.getSelfField(obj); 23 | const fieldConf = genRelationshipFieldConfig(ctx, field, relation); 24 | fieldMap[field.fieldName] = fieldConf; 25 | } 26 | 27 | return fieldMap; 28 | }, 29 | }); 30 | }; 31 | 32 | export const addObjectResolvers = (ctx: ResolverMap, obj: ObjectDefinition) => { 33 | const { typeName, singleName } = obj; 34 | const table = ctx.getTable(obj.typeName); 35 | 36 | const getResolver = (_, { where }) => table.findObjectByIndex(where); 37 | const createResolver = (_, { data }) => table.createObject(data); 38 | const updateResolver = (_, { where, data }) => table.updateObject(where, data); 39 | const deleteResolver = (_, { where }) => table.deleteObject(where); 40 | 41 | const objType = genObjectType(ctx, obj); 42 | const uniqueWhereInput = genUniqueWhereInput(obj); 43 | const createInput = genCreateInput(ctx, obj); 44 | const updateInput = genUpdateInput(ctx, obj); 45 | const connectionInput = genConnectionInput(obj, uniqueWhereInput, createInput); 46 | 47 | ctx.addObjectType(typeName, objType); 48 | ctx.addConnectionInput(typeName, connectionInput); 49 | 50 | ctx.addField('Query', singleName, { 51 | type: objType, 52 | args: { 53 | where: { type: nonNull(uniqueWhereInput) }, 54 | }, 55 | resolve: getResolver, 56 | }); 57 | 58 | ctx.addField('Mutation', `create${typeName}`, { 59 | type: objType, 60 | args: { 61 | data: { type: nonNull(createInput) }, 62 | }, 63 | resolve: createResolver, 64 | }); 65 | 66 | ctx.addField('Mutation', `update${typeName}`, { 67 | type: objType, 68 | args: { 69 | where: { type: nonNull(uniqueWhereInput) }, 70 | data: { type: nonNull(updateInput) }, 71 | }, 72 | resolve: updateResolver, 73 | }); 74 | 75 | ctx.addField('Mutation', `delete${typeName}`, { 76 | type: objType, 77 | args: { 78 | where: { type: nonNull(uniqueWhereInput) }, 79 | }, 80 | resolve: deleteResolver, 81 | }); 82 | }; 83 | -------------------------------------------------------------------------------- /src/schema/relationship.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLObjectType } from 'graphql/type'; 2 | import { RelationshipDefinition, RelationshipKind, RelationFieldParams } from '../internal'; 3 | import { ObjectTable } from '../relational'; 4 | import { ResolverMap } from './ResolverMap'; 5 | import { nonNull, list } from './scalar'; 6 | import { FieldResolver, FieldConfig } from './types'; 7 | 8 | type Table = ObjectTable; 9 | 10 | const makeIdResolver = (table: Table, fieldName: string) => { 11 | return async parent => { 12 | const toId = parent[fieldName]; 13 | return toId === null ? null : await table.getObject(toId); 14 | }; 15 | }; 16 | 17 | const makeForeignKeyResolver = (table: Table, fieldName: string) => { 18 | return async parent => { 19 | const parentId = parent.id; 20 | return await table.findObjectsByOrdinal(fieldName, parentId); 21 | }; 22 | }; 23 | 24 | const genRelationFieldType = (relatedObjType: GraphQLObjectType, field: RelationFieldParams) => { 25 | let scalar = field.isList ? list(relatedObjType) : relatedObjType; 26 | scalar = field.isRequired ? nonNull(scalar) : scalar; 27 | return scalar; 28 | }; 29 | 30 | export const genRelationshipFieldConfig = ( 31 | ctx: ResolverMap, 32 | field: RelationFieldParams, 33 | relation: RelationshipDefinition 34 | ): FieldConfig => { 35 | const { kind } = relation; 36 | const { relatedDefinition, fieldName, relatedFieldName, isList } = field; 37 | const relatedTable = ctx.getTable(relatedDefinition.typeName); 38 | const relatedObjType = ctx.getObjectType(relatedDefinition.typeName); 39 | 40 | let resolve: FieldResolver; 41 | 42 | if (kind === RelationshipKind.ToOne || kind === RelationshipKind.OneToOne) { 43 | resolve = makeIdResolver(relatedTable, fieldName); 44 | } else if (kind === RelationshipKind.OneToMany && !isList) { 45 | resolve = makeForeignKeyResolver(relatedTable, relatedFieldName); 46 | } else if (kind === RelationshipKind.OneToMany && isList) { 47 | resolve = makeIdResolver(relatedTable, fieldName); 48 | } else if (kind === RelationshipKind.ManyToMany) { 49 | throw new Error('ManyToMany relationships are not implemented yet'); // TODO 50 | } 51 | 52 | const type = genRelationFieldType(relatedObjType, field); 53 | return { type, resolve }; 54 | }; 55 | -------------------------------------------------------------------------------- /src/schema/scalar.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getNullableType, 3 | GraphQLNonNull, 4 | GraphQLList, 5 | GraphQLID, 6 | GraphQLInt, 7 | GraphQLFloat, 8 | GraphQLString, 9 | GraphQLBoolean, 10 | } from 'graphql/type'; 11 | 12 | import GraphQLJSON from 'graphql-type-json'; 13 | import { GraphQLDate, GraphQLTime, GraphQLDateTime } from 'graphql-iso-date'; 14 | 15 | const NullableDate = getNullableType(GraphQLDate); 16 | const NullableTime = getNullableType(GraphQLTime); 17 | const NullableDateTime = getNullableType(GraphQLDateTime); 18 | export const NullableID = getNullableType(GraphQLID); 19 | const NullableJSON = getNullableType(GraphQLJSON); 20 | 21 | export const getScalarForString = (scalarType: string) => { 22 | switch (scalarType) { 23 | case 'Date': 24 | return NullableDate; 25 | case 'Time': 26 | return NullableTime; 27 | case 'DateTime': 28 | return NullableDateTime; 29 | case 'JSON': 30 | return NullableJSON; 31 | case 'ID': 32 | return NullableID; 33 | case 'Int': 34 | return GraphQLInt; 35 | case 'Float': 36 | return GraphQLFloat; 37 | case 'String': 38 | return GraphQLString; 39 | case 'Boolean': 40 | return GraphQLBoolean; 41 | default: 42 | throw new Error(`Unspecified scalar type "${scalarType}" found`); 43 | } 44 | }; 45 | 46 | export const nonNull = x => new GraphQLNonNull(x); 47 | export const list = x => new GraphQLList(nonNull(x)); 48 | -------------------------------------------------------------------------------- /src/schema/types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLFieldResolver, 3 | GraphQLFieldConfig, 4 | GraphQLInputObjectType, 5 | GraphQLObjectType, 6 | } from 'graphql/type'; 7 | 8 | import { LevelInterface } from '../level'; 9 | import { ObjectDefinition } from '../internal'; 10 | 11 | export interface ContextParams { 12 | store: LevelInterface; 13 | objects: ObjectDefinition[]; 14 | } 15 | 16 | export type FieldResolver = GraphQLFieldResolver; 17 | export type FieldConfig = GraphQLFieldConfig; 18 | export type ObjectResolverMap = Record; 19 | export type ResolverTypeName = 'Query' | 'Mutation'; 20 | export type FieldResolverMap = Record; 21 | export type ObjectTypeMap = Record; 22 | export type InputObjectTypeMap = Record; 23 | -------------------------------------------------------------------------------- /src/utils/promisify.ts: -------------------------------------------------------------------------------- 1 | type OriginalFn = (...args: any[]) => void; 2 | 3 | type PromiseFn = (() => Promise) & 4 | ((A) => Promise) & 5 | ((A, B) => Promise); 6 | 7 | export function promisify(f: OriginalFn): PromiseFn { 8 | function promisified(...args) { 9 | return new Promise((resolve, reject) => { 10 | function callback(err, value) { 11 | if (err !== null && err !== undefined) { 12 | reject(err); 13 | } else { 14 | resolve(value); 15 | } 16 | } 17 | 18 | f.call(this, ...args, callback); 19 | }); 20 | } 21 | 22 | return promisified as PromiseFn; 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/**/*.ts"], 3 | "exclude": [], 4 | "compilerOptions": { 5 | "typeRoots": ["./types", "./node_modules/@types/"], 6 | "rootDir": "src", 7 | "outDir": ".", 8 | "lib": ["es6", "dom", "es2017.object", "esnext"], 9 | "noImplicitAny": false, 10 | "noUnusedParameters": false, 11 | "noUnusedLocals": true, 12 | "skipLibCheck": true, 13 | "moduleResolution": "node", 14 | "removeComments": true, 15 | "sourceMap": false, 16 | "declaration": true, 17 | "declarationMap": true, 18 | "target": "esnext", 19 | // "module": "commonjs", 20 | "module": "esnext", 21 | "esModuleInterop": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /types/deferred-leveldown.d.ts: -------------------------------------------------------------------------------- 1 | import { AbstractLevelDOWN } from 'abstract-leveldown'; 2 | export interface DeferredLevelDOWN extends AbstractLevelDOWN {} 3 | 4 | export interface DeferredLevelDOWNConstructor { 5 | new (): DeferredLevelDOWN; 6 | } 7 | 8 | export default DeferredLevelDOWNConstructor; 9 | --------------------------------------------------------------------------------