├── .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 |
--------------------------------------------------------------------------------