diff --git a/packages/plugin-edgedb/.gitignore b/packages/plugin-edgedb/.gitignore new file mode 100644 index 000000000..261d7b54f --- /dev/null +++ b/packages/plugin-edgedb/.gitignore @@ -0,0 +1,3 @@ +dbschema/edgeql-js/* +TODO.md +api-design.md \ No newline at end of file diff --git a/packages/plugin-edgedb/.npmignore b/packages/plugin-edgedb/.npmignore new file mode 100644 index 000000000..fc61aaad2 --- /dev/null +++ b/packages/plugin-edgedb/.npmignore @@ -0,0 +1,7 @@ +dbschema +edgedb.toml +test +tests +.turbo +babel.config.js +tsconfig.tsbuildinfo \ No newline at end of file diff --git a/packages/plugin-edgedb/LICENSE b/packages/plugin-edgedb/LICENSE new file mode 100644 index 000000000..4d74f2bcf --- /dev/null +++ b/packages/plugin-edgedb/LICENSE @@ -0,0 +1,6 @@ +ISC License (ISC) +Copyright 2021 Michael Hayes + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/packages/plugin-edgedb/README.md b/packages/plugin-edgedb/README.md new file mode 100644 index 000000000..365a64d56 --- /dev/null +++ b/packages/plugin-edgedb/README.md @@ -0,0 +1,92 @@ +# EdgeDB Plugin for Pothos + +This plugin provides tighter integration with `edgedb`, making it easier to define edgedb based +object types. + +## Example + +Here is a quick example of what an API using this plugin might look like. There is a more thorough +breakdown of what the methods and options used in the example below. + +```typescript +// Create an object type based on a edgedb model +// without providing any custom type information +builder.edgeDBObject('User', { + fields: (t) => ({ + // expose fields from the database + id: t.exposeID('id'), + email: t.exposeString('email'), + name: t.exposeString('name', { nullable: true }), + // Multi Link field for Users Posts + posts: t.link("posts"), + }), +}); + +builder.queryType({ + fields: (t) => ({ + // Define a field that issues an optimized edgedb query + me: t.edgeDBField({ + type: 'User', + resolve: async (query, root, args, ctx, info) => { + const db_query = e + .select(e.User, (user) => { + // the `query` argument will add in the `select`s fields to + // resolve as much of the request in a single query as possible + ...query, + filter: e.op(user.id, '=', ctx.user.id), + }); + + return await db.run(db_query); + } + }), + }), +}); +``` + +Given this schema, you would be able to resolve a query like the following with a single edgedb +query. + +```graphql +query { + me { + email + posts { + title + author { + id + } + } + } +} +``` + +## EdgeDB Expressions + +The `e` variable provides everything you need to build an edgedb query. All EdgeQL commands, +standard library functions, and types are available as properties on e. + +[Source](https://www.edgedb.com/docs/clients/js/querybuilder#expressions) + +```ts +import e from './dbschema/edgeql-js'; + +// commands +e.select; +e.insert; +e.update; +e.delete; + +// types +e.str; +e.bool; +e.cal.local_date; +e.User; +e.Post; +e....; + +// functions +e.str_upper; +e.len; +e.count; +e.math.stddev; +``` diff --git a/packages/plugin-edgedb/dbschema/default.esdl b/packages/plugin-edgedb/dbschema/default.esdl new file mode 100644 index 000000000..e62103918 --- /dev/null +++ b/packages/plugin-edgedb/dbschema/default.esdl @@ -0,0 +1,64 @@ +module default { + type Post { + required property big_int_id -> bigint { + constraint exclusive; + }; + required property created_at -> datetime { + default := datetime_current(); + constraint exclusive; + readonly := true; + }; + required property updated_at -> datetime; + + required property title -> str; + property content -> str; + required property published -> bool; + + link author -> User; + } + + type Media { + required property url -> str; + multi link posts -> PostMedia; + link uploaded_by -> User; + } + + type PostMedia { + required link post -> Post; + required link media -> Media; + } + + type Comment { + required property created_at -> datetime { + default := datetime_current(); + constraint exclusive; + readonly := true; + }; + required property content -> str; + required link author -> User; + required link post -> Post; + } + + type Profile { + property bio -> str; + required link user -> User; + } + + type User { + required property email -> str { + constraint exclusive; + }; + property name -> str; + multi link posts -> Post; + multi link comments -> Comment; + link profile -> Profile; + multi link followers -> Follow; + multi link following -> Follow; + multi link media -> Media; + } + + type Follow { + required link from_user -> User; + required link to_user -> User; + } +} diff --git a/packages/plugin-edgedb/dbschema/migrations/00001.edgeql b/packages/plugin-edgedb/dbschema/migrations/00001.edgeql new file mode 100644 index 000000000..f4d0bdc60 --- /dev/null +++ b/packages/plugin-edgedb/dbschema/migrations/00001.edgeql @@ -0,0 +1,18 @@ +CREATE MIGRATION m1co7oxdqc52kxete3rozfhbkb67kofxnqspppu7ek6pe327a5mtyq + ONTO initial +{ + CREATE TYPE default::Post { + CREATE REQUIRED PROPERTY big_int_id -> std::bigint { + CREATE CONSTRAINT std::exclusive; + }; + CREATE PROPERTY content -> std::str; + CREATE REQUIRED PROPERTY created_at -> std::datetime { + SET default := (std::datetime_current()); + SET readonly := true; + CREATE CONSTRAINT std::exclusive; + }; + CREATE REQUIRED PROPERTY published -> std::bool; + CREATE REQUIRED PROPERTY title -> std::str; + CREATE REQUIRED PROPERTY updated_at -> std::datetime; + }; +}; diff --git a/packages/plugin-edgedb/dbschema/migrations/00002.edgeql b/packages/plugin-edgedb/dbschema/migrations/00002.edgeql new file mode 100644 index 000000000..0a0c90dd2 --- /dev/null +++ b/packages/plugin-edgedb/dbschema/migrations/00002.edgeql @@ -0,0 +1,56 @@ +CREATE MIGRATION m1goni6inys6wt4wokvqvemtqiiambmkfxudnf6rsculrh645bozla + ONTO m1co7oxdqc52kxete3rozfhbkb67kofxnqspppu7ek6pe327a5mtyq +{ + CREATE TYPE default::Comment { + CREATE REQUIRED LINK post -> default::Post; + CREATE REQUIRED PROPERTY content -> std::str; + CREATE REQUIRED PROPERTY created_at -> std::datetime { + SET default := (std::datetime_current()); + SET readonly := true; + CREATE CONSTRAINT std::exclusive; + }; + }; + CREATE TYPE default::User { + CREATE MULTI LINK comments -> default::Comment; + CREATE MULTI LINK posts -> default::Post; + CREATE REQUIRED PROPERTY email -> std::str { + CREATE CONSTRAINT std::exclusive; + }; + CREATE PROPERTY name -> std::str; + }; + ALTER TYPE default::Comment { + CREATE REQUIRED LINK author -> default::User; + }; + CREATE TYPE default::Follow { + CREATE REQUIRED LINK from_user -> default::User; + CREATE REQUIRED LINK to_user -> default::User; + }; + ALTER TYPE default::User { + CREATE MULTI LINK followers -> default::Follow; + CREATE MULTI LINK following -> default::Follow; + }; + CREATE TYPE default::Media { + CREATE LINK uploaded_by -> default::User; + CREATE REQUIRED PROPERTY url -> std::str; + }; + CREATE TYPE default::PostMedia { + CREATE REQUIRED LINK media -> default::Media; + CREATE REQUIRED LINK post -> default::Post; + }; + ALTER TYPE default::Media { + CREATE MULTI LINK posts -> default::PostMedia; + }; + ALTER TYPE default::User { + CREATE MULTI LINK media -> default::Media; + }; + ALTER TYPE default::Post { + CREATE LINK author -> default::User; + }; + CREATE TYPE default::Profile { + CREATE REQUIRED LINK user -> default::User; + CREATE PROPERTY bio -> std::str; + }; + ALTER TYPE default::User { + CREATE LINK profile -> default::Profile; + }; +}; diff --git a/packages/plugin-edgedb/edgedb.toml b/packages/plugin-edgedb/edgedb.toml new file mode 100644 index 000000000..ddcda65a9 --- /dev/null +++ b/packages/plugin-edgedb/edgedb.toml @@ -0,0 +1,2 @@ +[edgedb] +server-version = "2.1" diff --git a/packages/plugin-edgedb/esm/.gitignore b/packages/plugin-edgedb/esm/.gitignore new file mode 100755 index 000000000..440de5113 --- /dev/null +++ b/packages/plugin-edgedb/esm/.gitignore @@ -0,0 +1,4 @@ +* +!.gitignore +!.npmignore +!package.json \ No newline at end of file diff --git a/packages/plugin-edgedb/esm/.npmignore b/packages/plugin-edgedb/esm/.npmignore new file mode 100755 index 000000000..e69de29bb diff --git a/packages/plugin-edgedb/esm/package.json b/packages/plugin-edgedb/esm/package.json new file mode 100755 index 000000000..3dbc1ca59 --- /dev/null +++ b/packages/plugin-edgedb/esm/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/packages/plugin-edgedb/jest.config.js b/packages/plugin-edgedb/jest.config.js new file mode 100644 index 000000000..7d6620a47 --- /dev/null +++ b/packages/plugin-edgedb/jest.config.js @@ -0,0 +1,6 @@ +module.exports = { + transformIgnorePatterns: [`node_modules`], + transform: { + '^.+\\.(t|j)sx?$': ['@swc/jest'], + }, +}; diff --git a/packages/plugin-edgedb/package.json b/packages/plugin-edgedb/package.json new file mode 100644 index 000000000..b6d04d06f --- /dev/null +++ b/packages/plugin-edgedb/package.json @@ -0,0 +1,57 @@ +{ + "name": "@pothos/plugin-edgedb", + "version": "3.4.0", + "description": "An edgedb plugin for Pothos", + "main": "./lib/index.js", + "types": "./dts/index.d.ts", + "module": "./esm/index.js", + "exports": { + "types": "./dts/index.d.ts", + "import": "./esm/index.js", + "require": "./lib/index.js" + }, + "scripts": { + "generate": "edgeql-js --output-dir tests/client/", + "type": "tsc --project tsconfig.type.json", + "build": "pnpm build:clean && pnpm build:cjs && pnpm build:esm && pnpm build:dts", + "build:clean": "git clean -dfX esm lib", + "build:cjs": "swc src -d lib --config-file ../../.swcrc -C module.type=commonjs", + "build:esm": "swc src -d esm --config-file ../../.swcrc -C module.type=es6 && pnpm esm:extensions", + "build:dts": "tsc", + "esm:extensions": "TS_NODE_PROJECT=../../tsconfig.json node -r @swc-node/register ../../.config/esm-transformer.ts", + "test": "jest --runInBand" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/hayes/pothos.git" + }, + "author": "Michael Hayes", + "license": "ISC", + "keywords": [ + "giraphql", + "pothos", + "graphql", + "schema", + "edgedb", + "plugin" + ], + "publishConfig": { + "access": "public" + }, + "peerDependencies": { + "@pothos/core": "*", + "graphql": ">=15.1.0" + }, + "devDependencies": { + "@pothos/core": "workspace:*", + "@pothos/plugin-complexity": "workspace:*", + "@pothos/plugin-errors": "workspace:*", + "@pothos/plugin-relay": "workspace:*", + "@pothos/plugin-simple-objects": "workspace:*", + "@pothos/test-utils": "workspace:*", + "edgedb": "^0.21.3", + "graphql": "16.5.0", + "graphql-tag": "^2.12.6" + }, + "gitHead": "9dfe52f1975f41a111e01bf96a20033a914e2acc" +} diff --git a/packages/plugin-edgedb/src/edgedb-field-builder.ts b/packages/plugin-edgedb/src/edgedb-field-builder.ts new file mode 100644 index 000000000..83413db78 --- /dev/null +++ b/packages/plugin-edgedb/src/edgedb-field-builder.ts @@ -0,0 +1,182 @@ +import { GraphQLResolveInfo } from 'graphql'; +import { + CompatibleTypes, + FieldKind, + FieldRef, + InputFieldMap, + NormalizeArgs, + RootFieldBuilder, + SchemaTypes, + ShapeFromTypeParam, + TypeParam, +} from '@pothos/core'; +import { FieldMap } from './util/relation-map'; +import { getLink, getRefFromModel } from './util/datamodel'; +import { extractTargetTypeName } from './util/target'; +import { isMultiLink } from './util/links'; +import { EdgeDBModelTypes, RelatedFieldOptions } from './types'; + +// Workaround for FieldKind not being extended on Builder classes +const RootBuilder: { + // eslint-disable-next-line @typescript-eslint/prefer-function-type + new ( + name: string, + builder: PothosSchemaTypes.SchemaBuilder, + kind: FieldKind, + graphqlKind: PothosSchemaTypes.PothosKindToGraphQLType[FieldKind], + ): PothosSchemaTypes.RootFieldBuilder; +} = RootFieldBuilder as never; + +export class EdgeDBObjectFieldBuilder< + Types extends SchemaTypes, + Model extends EdgeDBModelTypes, + Shape extends object = Model['Shape'], +> extends RootBuilder { + model: string; + edgeDBFieldMap: FieldMap; + + exposeBoolean = this.createExpose('Boolean'); + exposeFloat = this.createExpose('Float'); + exposeInt = this.createExpose('Int'); + exposeID = this.createExpose('ID'); + exposeString = this.createExpose('String'); + exposeBooleanList = this.createExpose(['Boolean']); + exposeFloatList = this.createExpose(['Float']); + exposeIntList = this.createExpose(['Int']); + exposeIDList = this.createExpose(['ID']); + exposeStringList = this.createExpose(['String']); + + constructor( + name: string, + builder: PothosSchemaTypes.SchemaBuilder, + model: string, + fieldMap: FieldMap, + ) { + super(name, builder, 'EdgeDBObject', 'Object'); + + this.model = model; + this.edgeDBFieldMap = fieldMap; + } + + link< + Field extends Model['LinkName'], + Nullable extends boolean, + Args extends InputFieldMap, + ResolveReturnShape, + >( + ...allArgs: NormalizeArgs< + [ + name: Field, + options?: RelatedFieldOptions< + Types, + Model, + Field, + Nullable, + Args, + ResolveReturnShape, + Shape + >, + ] + > + ): FieldRef { + const [name, { ...options } = {} as never] = allArgs; + const relationField = getLink(this.model, this.builder, name); + const ref = getRefFromModel(extractTargetTypeName(relationField.target), this.builder); + + const { resolve, extensions, ...rest } = options; + + const isList = isMultiLink(relationField); + + console.log('[debug] link() -> name: ', name); + console.log('[debug] link() -> relationField: ', relationField, relationField.target); + console.log('[debug] link() -> ref: ', ref); + console.log('[debug] link() -> isList: ', isList); + + return this.field({ + ...(rest as {}), + type: isList ? [ref] : ref, + // TODO: Descriptions + // description: getFieldDescription(this.model, this.builder, name, description), + extensions: { + ...extensions, + pothosEdgeDBFallback: + resolve && + ((q: {}, parent: Shape, args: {}, context: {}, info: GraphQLResolveInfo) => + resolve({ ...q } as never, parent, args as never, context, info)), + }, + resolve: (parent) => (parent as Record)[name], + }) as FieldRef; + } + + expose< + Type extends TypeParam, + Nullable extends boolean, + ResolveReturnShape, + Name extends CompatibleTypes, + >( + ...args: NormalizeArgs< + [ + name: Name, + options?: Omit< + PothosSchemaTypes.ObjectFieldOptions< + Types, + Shape, + Type, + Nullable, + {}, + ResolveReturnShape + >, + 'resolve' | 'select' + >, + ] + > + ) { + const [name, options = {} as never] = args; + + const typeConfig = this.builder.configStore.getTypeConfig(this.typename, 'Object'); + const usingSelect = false; + + return this.exposeField(name as never, { + ...options, + }); + } + + private createExpose>(type: Type) { + return < + Nullable extends boolean, + ResolveReturnShape, + Name extends CompatibleTypes, + >( + ...args: NormalizeArgs< + [ + name: Name, + options?: Omit< + PothosSchemaTypes.ObjectFieldOptions< + Types, + Shape, + Type, + Nullable, + {}, + ResolveReturnShape + >, + 'resolve' | 'type' | 'select' | 'description' + > & { description?: string | false }, + ] + > + ): FieldRef, 'EdgeDBObject'> => { + const [name, { description, ...options } = {} as never] = args; + + return this.expose(name as never, { + ...options, + // TODO: Descriptions + // description: getFieldDescription( + // this.model, + // this.builder, + // name as string, + // description, + // ) as never, + type, + }); + }; + } +} diff --git a/packages/plugin-edgedb/src/field-builder.ts b/packages/plugin-edgedb/src/field-builder.ts new file mode 100644 index 000000000..0cb185c1d --- /dev/null +++ b/packages/plugin-edgedb/src/field-builder.ts @@ -0,0 +1,30 @@ +import { FieldKind, ObjectRef, RootFieldBuilder, SchemaTypes } from '@pothos/core'; +import { GraphQLResolveInfo } from 'graphql'; +import { getRefFromModel } from './util/datamodel'; + +export * from './edgedb-field-builder'; + +const fieldBuilderProto = RootFieldBuilder.prototype as PothosSchemaTypes.RootFieldBuilder< + SchemaTypes, + unknown, + FieldKind +>; + +fieldBuilderProto.edgeDBField = function edgeDBField({ type, resolve, ...options }) { + const modelOrRef = Array.isArray(type) ? type[0] : type; + const typeRef = + typeof modelOrRef === 'string' + ? getRefFromModel(modelOrRef, this.builder) + : (modelOrRef as ObjectRef); + const typeParam = Array.isArray(type) ? ([typeRef] as [ObjectRef]) : typeRef; + + return this.field({ + ...(options as {}), + type: typeParam, + resolve: (parent: unknown, args: unknown, ctx: {}, info: GraphQLResolveInfo) => { + const query = null; + + return resolve(query as never, parent as never, args as never, ctx, info) as never; + }, + }) as never; +}; diff --git a/packages/plugin-edgedb/src/global-types.ts b/packages/plugin-edgedb/src/global-types.ts new file mode 100644 index 000000000..60e3e0211 --- /dev/null +++ b/packages/plugin-edgedb/src/global-types.ts @@ -0,0 +1,153 @@ +/* eslint-disable @typescript-eslint/no-unud-vars */ +import { + FieldKind, + FieldNullability, + FieldRef, + InputFieldMap, + InterfaceParam, + PluginName, + SchemaTypes, + ShapeFromTypeParam, + TypeParam, +} from '@pothos/core'; +import { + EdgeDBDriver, + EdgeDBFieldOptions, + EdgeDBSchemaTypeKeys, + EdgeDBModelShape, + EdgeDBModelTypes, + EdgeDBObjectFieldOptions, + EdgeDBObjectTypeOptions, + EdgeDBQueryBuilder, +} from './types'; +import type { EdgeDBPlugin } from '.'; +import { EdgeDBObjectFieldBuilder as InternalEdgeDBObjectFieldBuilder } from './edgedb-field-builder'; +import { edgeDBModelKey, EdgeDBObjectRef } from './object-ref'; + +declare global { + export namespace PothosSchemaTypes { + export interface Plugins { + edgedb: EdgeDBPlugin; + } + + export interface UserSchemaTypes { + EdgeDBTypes: { default: { [key: string]: unknown } }; + } + + export interface ExtendDefaultTypes> { + EdgeDBTypes: PartialTypes['EdgeDBTypes'] & {}; + } + + export interface PothosKindToGraphQLType { + EdgeDBObject: 'Object'; + } + + export interface SchemaBuilderOptions { + edgeDB: { + qb: EdgeDBQueryBuilder; + client: EdgeDBDriver; + extensions?: {}; + exposeDescriptions?: + | boolean + | { + types?: boolean; + fields?: boolean; + }; + }; + } + + export interface BuildSchemaOptions { + customBuildTimeOptions?: boolean; + } + + export interface ObjectTypeOptions { + optionOnObject?: boolean; + } + + export interface FieldOptionsByKind< + Types extends SchemaTypes, + ParentShape, + Type extends TypeParam, + Nullable extends FieldNullability, + Args extends InputFieldMap, + ResolveShape, + ResolveReturnShape, + > { + EdgeDBObject: EdgeDBObjectFieldOptions< + Types, + ParentShape, + Type, + Nullable, + Args, + ResolveShape, + ResolveReturnShape + >; + } + + export interface RootFieldBuilder< + Types extends SchemaTypes, + ParentShape, + Kind extends FieldKind = FieldKind, + > { + edgeDBField: < + Args extends InputFieldMap, + TypeParam extends + | EdgeDBObjectRef + | keyof Types['EdgeDBTypes']['default'] + | [keyof Types['EdgeDBTypes']['default']] + | [EdgeDBObjectRef], + Nullable extends FieldNullability, + ResolveShape, + ResolveReturnShape, + Type extends TypeParam extends [unknown] + ? [ObjectRef] + : ObjectRef, + Model extends EdgeDBModelTypes = EdgeDBModelShape< + Types, + // @ts-expect-error -> string | number | symbol not assignable to .. + TypeParam extends [keyof Types['EdgeDBTypes']['default']] + ? TypeParam[0] + : TypeParam extends [EdgeDBObjectRef] + ? TypeParam[0][typeof edgeDBModelKey]['Name'] + : TypeParam extends EdgeDBObjectRef + ? TypeParam[typeof edgeDBModelKey]['Name'] + : TypeParam extends keyof Types['EdgeDBTypes']['default'] + ? TypeParam + : never + >, + >( + options: EdgeDBFieldOptions< + Types, + ParentShape, + TypeParam, + Model, + Type, + Args, + Nullable, + ResolveShape, + ResolveReturnShape, + Kind + >, + ) => FieldRef>; + } + + export interface SchemaBuilder { + edgeDBObject: < + Name extends EdgeDBSchemaTypeKeys, + Interfaces extends InterfaceParam[], + Model extends EdgeDBModelShape & Types['EdgeDBTypes']['default'][Name], + Shape extends object = Model['Shape'], + >( + name: Name, + options: EdgeDBObjectTypeOptions, + ) => EdgeDBObjectRef; + } + + export interface EdgeDBObjectFieldBuilder< + Types extends SchemaTypes, + Model extends EdgeDBModelTypes, + Shape extends object = Model['Shape'], + > extends InternalEdgeDBObjectFieldBuilder, + RootFieldBuilder {} + } +} diff --git a/packages/plugin-edgedb/src/index.ts b/packages/plugin-edgedb/src/index.ts new file mode 100644 index 000000000..ec38ad8b5 --- /dev/null +++ b/packages/plugin-edgedb/src/index.ts @@ -0,0 +1,72 @@ +/* eslint-disable no-console */ +import './global-types'; +import './schema-builder'; +import { GraphQLFieldResolver, GraphQLTypeResolver } from 'graphql'; +import SchemaBuilder, { + BasePlugin, + BuildCache, + PothosInterfaceTypeConfig, + PothosOutputFieldConfig, + PothosUnionTypeConfig, + SchemaTypes, +} from '@pothos/core'; +import { EdgeDBObjectFieldBuilder as InternalEdgeDBObjectFieldBuilder } from './field-builder'; +import { EdgeDBModelTypes } from './types'; + +export * from './types'; + +const pluginName = 'edgedb' as const; + +export default pluginName; + +export type EdgeDBObjectFieldBuilder< + Types extends SchemaTypes, + ParentShape, +> = PothosSchemaTypes.ObjectFieldBuilder; + +export const ObjectFieldBuilder = InternalEdgeDBObjectFieldBuilder as new < + Types extends SchemaTypes, + Model extends EdgeDBModelTypes, + Shape extends object = Model['Shape'], +>( + name: string, + builder: PothosSchemaTypes.SchemaBuilder, +) => PothosSchemaTypes.EdgeDBObjectFieldBuilder; + +export class EdgeDBPlugin extends BasePlugin { + constructor(cache: BuildCache) { + super(cache, pluginName); + } + + override onOutputFieldConfig(fieldConfig: PothosOutputFieldConfig) { + if (fieldConfig.kind === 'EdgeDBObject') { + console.log('[plugin-edgedb] Received object of type `EdgeDBObject` '); + } + + return fieldConfig; + } + + override wrapResolve( + resolver: GraphQLFieldResolver, + fieldConfig: PothosOutputFieldConfig, + ): GraphQLFieldResolver { + if (fieldConfig.kind !== 'EdgeDBObject') { + return resolver; + } + + return (parent, args, context, info) => { + console.log(`Resolving ${info.parentType}.${info.fieldName}`); + + return resolver(parent, args, context, info); + }; + } + + override wrapResolveType( + resolveType: GraphQLTypeResolver, + typeConfig: PothosInterfaceTypeConfig | PothosUnionTypeConfig, + ) { + return resolveType; + } +} + +SchemaBuilder.registerPlugin(pluginName, EdgeDBPlugin); diff --git a/packages/plugin-edgedb/src/object-ref.ts b/packages/plugin-edgedb/src/object-ref.ts new file mode 100644 index 000000000..047de53fb --- /dev/null +++ b/packages/plugin-edgedb/src/object-ref.ts @@ -0,0 +1,8 @@ +import { ObjectRef } from '@pothos/core'; +import type { EdgeDBModelTypes } from './types'; + +export const edgeDBModelKey = Symbol.for('Pothos.edgeDBModelKey'); + +export class EdgeDBObjectRef extends ObjectRef { + [edgeDBModelKey]!: Model; +} diff --git a/packages/plugin-edgedb/src/schema-builder.ts b/packages/plugin-edgedb/src/schema-builder.ts new file mode 100644 index 000000000..480ce2124 --- /dev/null +++ b/packages/plugin-edgedb/src/schema-builder.ts @@ -0,0 +1,41 @@ +import './global-types'; +import SchemaBuilder, { SchemaTypes } from '@pothos/core'; +import { EdgeDBObjectFieldBuilder } from './edgedb-field-builder'; +import { getRefFromModel } from './util/datamodel'; +import { getModelDescription } from './util/description'; +import { getRelationMap } from './util/relation-map'; +import { getObjectsTypes } from './util/get-client'; + +const schemaBuilderProto = SchemaBuilder.prototype as PothosSchemaTypes.SchemaBuilder; + +schemaBuilderProto.edgeDBObject = function edgeDBObject(type, { fields, description, ...options }) { + const ref = getRefFromModel(type as string, this); + const name: string = type as string; + const fieldMap = getRelationMap(getObjectsTypes(this)).get(type as string)!; + + ref.name = name; + + this.objectType(ref, { + ...(options as {}), + description: getModelDescription(type, this, description), + extensions: { + ...options.extensions, + pothosEdgeDBModel: type, + pothosEdgeDBFieldMap: fieldMap, + }, + name, + fields: fields + ? () => + fields( + new EdgeDBObjectFieldBuilder( + name, + this, + type, + getRelationMap(getObjectsTypes(this)).get(type)!, + ), + ) + : undefined, + }); + + return ref as never; +}; diff --git a/packages/plugin-edgedb/src/types.ts b/packages/plugin-edgedb/src/types.ts new file mode 100644 index 000000000..7d2ef094f --- /dev/null +++ b/packages/plugin-edgedb/src/types.ts @@ -0,0 +1,469 @@ +import type { Client } from 'edgedb'; +import { + FieldKind, + FieldMap, + FieldNullability, + FieldOptionsFromKind, + InputFieldMap, + InputShapeFromFields, + InterfaceParam, + ListResolveValue, + MaybePromise, + Normalize, + ObjectRef, + SchemaTypes, + ShapeFromTypeParam, + ShapeWithNullability, + TypeParam, +} from '@pothos/core'; +import type { EdgeDBObjectFieldBuilder } from './edgedb-field-builder'; +import { GraphQLResolveInfo } from 'graphql'; +import { EdgeDBObjectRef } from './object-ref'; + +export interface EdgeDBQueryBuilder { + default: unknown; +} +export interface EdgeDBDriver extends Client {} + +export type EdgeDBSchemaTypeKeys = + keyof Types['EdgeDBTypes']['default'] extends infer Key + ? Key extends string + ? Key + : never + : never; + +export type EdgeDBDefaultExportKeyTypes = { + [K in keyof DefaultExports]-?: K extends string + ? string + : K extends number + ? number + : K extends symbol + ? symbol + : never; +}[keyof DefaultExports]; + +type EdgeDBModelKeyAsString< + DefaultExports, + KeyType extends string | number | symbol = EdgeDBDefaultExportKeyTypes, +> = Extract; + +export type EdgeDBSchemaTypes = + Types['EdgeDBTypes']['default'] extends infer ObjectTypeMap + ? ObjectTypeMap extends object + ? ObjectTypeMap + : never + : never; + +export interface EdgeDBModelTypes { + Name: string; + Shape: {}; + ReturnShape: {}; + MultiLink: string; + LinkName: string; + Links: Record< + string, + { + Shape: unknown; + Types: EdgeDBModelTypes; + } + >; +} + +export type DeepPartial = T extends Promise + ? Promise> + : T extends object + ? { [P in keyof T]?: DeepPartial } + : T extends (infer U)[] + ? DeepPartial[] + : T; + +export type EdgeDBModelShape< + Types extends SchemaTypes, + Name extends EdgeDBSchemaTypeKeys, +> = EdgeDBSchemaTypes[Name] extends infer ModelProperties + ? ModelProperties extends BaseObject + ? { + Name: Name; + Shape: ModelProperties; + ReturnShape: DeepPartial; + MultiLink: extractMultiLinks extends infer Link + ? Link extends string + ? SplitLT + : never + : never; + LinkName: extractLinks extends infer Link + ? Link extends string + ? SplitLT + : never + : never; + } extends infer ModelTypesWithoutLinks + ? ModelTypesWithoutLinks extends { + LinkName: string; + Shape: Record; + } + ? ModelTypesWithoutLinks & { + Links: { + [Key in ModelTypesWithoutLinks['LinkName']]: { + Shape: ModelTypesWithoutLinks['Shape'][Key]; + Types: EdgeDBModelShape< + Types, + ModelTypesWithoutLinks['Shape'][Key] extends infer Base + ? Base extends TypeSet + ? Base['__element__']['__name__'] extends infer ModelName + ? ModelName extends string + ? SplitDefault extends EdgeDBSchemaTypeKeys + ? SplitDefault + : never + : never + : never + : never + : never + >; + }; + }; + } + : ModelTypesWithoutLinks & { Links: {} } + : never + : never + : never; + +export type SelectedKeys = { [K in keyof T]: T[K] extends false ? never : K }[keyof T]; + +// -- +// EdgeDB Types +// -- +export enum Cardinality { + AtMostOne = 'AtMostOne', + One = 'One', + Many = 'Many', + AtLeastOne = 'AtLeastOne', + Empty = 'Empty', +} + +export enum TypeKind { + scalar = 'scalar', + // castonlyscalar = "castonlyscalar", + enum = 'enum', + object = 'object', + namedtuple = 'namedtuple', + tuple = 'tuple', + array = 'array', + range = 'range', +} +export type tupleOf = [T, ...T[]] | []; + +export type CardinalityAssignable = { + Empty: Cardinality.Empty; + One: Cardinality.One; + AtLeastOne: Cardinality.One | Cardinality.Many | Cardinality.AtLeastOne; + AtMostOne: Cardinality.One | Cardinality.AtMostOne | Cardinality.Empty; + Many: Cardinality.Many; +}[Card]; + +export interface BaseObject { + id: any; + __type__: any; +} +export interface BaseType { + __kind__: TypeKind; + __name__: string; +} +export type BaseTypeSet = { + __element__: BaseType; + __cardinality__: Cardinality; +}; +export type BaseTypeTuple = tupleOf; + +export interface ScalarType< + Name extends string = string, + TsType extends any = any, + TsConstType extends TsType = TsType, +> extends BaseType { + __kind__: TypeKind.scalar; + __tstype__: TsType; + __tsconsttype__: TsConstType; + __name__: Name; +} + +export interface TypeSet { + __element__: T; + __cardinality__: Card; +} + +export type PropertyShape = { + [k: string]: PropertyDesc; +}; + +export interface PropertyDesc< + Type extends BaseType = BaseType, + Card extends Cardinality = Cardinality, + Exclusive extends boolean = boolean, + Computed extends boolean = boolean, + Readonly extends boolean = boolean, + HasDefault extends boolean = boolean, +> { + __kind__: 'property'; + target: Type; + cardinality: Card; + exclusive: Exclusive; + computed: Computed; + readonly: Readonly; + hasDefault: HasDefault; +} + +export interface LinkDesc< + Type extends ObjectType = any, + Card extends Cardinality = Cardinality, + LinkProps extends PropertyShape = any, + Exclusive extends boolean = boolean, + Computed extends boolean = boolean, + Readonly extends boolean = boolean, + HasDefault extends boolean = boolean, +> { + __kind__: 'link'; + target: Type; + cardinality: Card; + properties: LinkProps; + exclusive: Exclusive; + computed: Computed; + readonly: Readonly; + hasDefault: HasDefault; +} + +export type ObjectTypeSet = TypeSet; +export type ObjectTypeExpression = TypeSet; + +export interface ObjectType< + Name extends string = string, + Pointers extends ObjectTypePointers = ObjectTypePointers, + Shape extends object | null = any, + // Polys extends Poly[] = any[] +> extends BaseType { + __kind__: TypeKind.object; + __name__: Name; + __pointers__: Pointers; + __shape__: Shape; +} + +export type ObjectTypePointers = { + [k: string]: PropertyDesc | LinkDesc; +}; + +export interface PathParent { + type: Parent; + linkName: string; +} + +export namespace EdgeDB { + export interface Datamodel { + [key: string]: TypeSet; + } +} + +type PointersToObjectType

= ObjectType; + +export type SelectModifiers = { + filter?: TypeSet, Cardinality>; + order_by?: any; + offset?: any | number; + limit?: any | number; +}; + +// --- + +// object types -> pointers +// pointers -> links +// links -> target object type +// links -> link properties +export type extractObjectShapeToSelectShape = Partial<{ + [k in keyof TObject['__pointers__']]: TObject['__pointers__'][k] extends PropertyDesc + ? + | boolean + | TypeSet< + TObject['__pointers__'][k]['target'], + CardinalityAssignable + > + : TObject['__pointers__'][k] extends LinkDesc + ? {} // as link, currently no type + : any; +}> & { [key: string]: unknown }; + +type extractLinksToPartial = Shape extends infer T + ? T extends object + ? { + [Key in keyof Shape]: Shape[Key] extends infer Link + ? Link extends BaseObject | Array + ? boolean + : never + : never; + } + : never + : never; + +type extractMultiLinksToPartial = Shape extends infer T + ? T extends object + ? { + [Key in keyof Shape]: Shape[Key] extends infer Link + ? Link extends Array + ? boolean + : never + : never; + } + : never + : never; + +// Filter out links from model type +export type extractLinks< + Model extends object, + PartialLinks extends extractLinksToPartial = extractLinksToPartial, +> = PartialLinks extends infer Links + ? { + [K in keyof Links]: [boolean] extends [Links[K]] ? K : never; + }[keyof Links] + : null; +export type extractMultiLinks< + Model extends object, + PartialLinks extends extractMultiLinksToPartial = extractMultiLinksToPartial, +> = PartialLinks extends infer Links + ? { + [K in keyof Links]: [boolean] extends [Links[K]] ? K : never; + }[keyof Links] + : null; + +type Split = S extends `${infer T}${D}${infer U}` + ? never + : [S][0]; + +// For removing backlinks from `link` fields +// eg. SplitLT<"posts" | "comments" | " -> "posts" | "comments" +export type SplitLT = Split; +// Only way to get models name is to split it on its BaseObject __name__ +// eg. SplitDefault<"default::Post"> -> "Post" +export type SplitDefault = Split; + +export type EdgeDBObjectFieldOptions< + Types extends SchemaTypes, + ParentShape, + Type extends TypeParam, + Nullable extends FieldNullability, + Args extends InputFieldMap, + Select, + ResolveReturnShape, +> = PothosSchemaTypes.ObjectFieldOptions< + Types, + ParentShape, + Type, + Nullable, + Args, + ResolveReturnShape +>; + +type EdgeDBObjectFieldsShape< + Types extends SchemaTypes, + Model extends EdgeDBModelTypes, + Shape extends object, +> = (t: EdgeDBObjectFieldBuilder) => FieldMap; + +export type EdgeDBObjectTypeOptions< + Types extends SchemaTypes, + Interfaces extends InterfaceParam[], + Model extends EdgeDBModelTypes, + Shape extends object, +> = Omit< + | PothosSchemaTypes.ObjectTypeOptions + | PothosSchemaTypes.ObjectTypeWithInterfaceOptions, + 'fields' | 'description' +> & { + description?: string | false; + fields?: EdgeDBObjectFieldsShape; +}; + +type RefForLink< + Model extends EdgeDBModelTypes, + Field extends keyof Model['Links'], +> = Model['Links'][Field] extends unknown[] + ? [ObjectRef] + : ObjectRef; + +export type RelatedFieldOptions< + Types extends SchemaTypes, + Model extends EdgeDBModelTypes, + Field extends keyof Model['Links'], + Nullable extends boolean, + Args extends InputFieldMap, + ResolveReturnShape, + Shape, +> = Omit< + PothosSchemaTypes.ObjectFieldOptions< + Types, + Shape, + RefForLink, + Nullable, + Args, + ResolveReturnShape + >, + 'resolve' | 'type' | 'description' +> & { + resolve?: ( + query: never, + parent: Shape, + args: InputShapeFromFields, + context: Types['Context'], + info: GraphQLResolveInfo, + ) => MaybePromise> & { + type?: EdgeDBObjectRef; + }; +}; + +export type EdgeDBFieldResolver< + Types extends SchemaTypes, + Model extends EdgeDBModelTypes, + Parent, + Param extends TypeParam, + Args extends InputFieldMap, + Nullable extends FieldNullability, + ResolveReturnShape, +> = ( + query: {}, + parent: Parent, + args: InputShapeFromFields, + context: Types['Context'], + info: GraphQLResolveInfo, +) => ShapeFromTypeParam extends infer Shape + ? [Shape] extends [[readonly (infer Item)[] | null | undefined]] + ? ListResolveValue + : MaybePromise + : never; + +export type EdgeDBFieldOptions< + Types extends SchemaTypes, + ParentShape, + Type extends + | EdgeDBObjectRef + | keyof Types['EdgeDBTypes']['default'] + | [keyof Types['EdgeDBTypes']['default']] + | [EdgeDBObjectRef], + Model extends EdgeDBModelTypes, + Param extends TypeParam, + Args extends InputFieldMap, + Nullable extends FieldNullability, + ResolveShape, + ResolveReturnShape, + Kind extends FieldKind = FieldKind, +> = FieldOptionsFromKind< + Types, + ParentShape, + Param, + Nullable, + Args, + Kind, + ResolveShape, + ResolveReturnShape +> extends infer FieldOptions + ? Omit & { + type: Type; + resolve: FieldOptions extends { resolve?: (parent: infer Parent, ...args: any[]) => unknown } + ? EdgeDBFieldResolver + : EdgeDBFieldResolver; + } + : never; diff --git a/packages/plugin-edgedb/src/util/datamodel.ts b/packages/plugin-edgedb/src/util/datamodel.ts new file mode 100644 index 000000000..0c6bd2ec9 --- /dev/null +++ b/packages/plugin-edgedb/src/util/datamodel.ts @@ -0,0 +1,65 @@ +import { SchemaTypes } from '@pothos/core'; +import { EdgeDBObjectRef } from '../object-ref'; +import { EdgeDBModelTypes } from '../types'; +import { getObjectsTypes } from './get-client'; + +export const refMap = new WeakMap>>(); + +export function getRefFromModel( + name: string, + builder: PothosSchemaTypes.SchemaBuilder, +): EdgeDBObjectRef { + if (!refMap.has(builder)) { + refMap.set(builder, new Map()); + } + const cache = refMap.get(builder)!; + + if (!cache.has(name)) { + cache.set(name, new EdgeDBObjectRef(name)); + } + + return cache.get(name)!; +} + +export function getLink( + name: string, + builder: PothosSchemaTypes.SchemaBuilder, + link: string, +) { + const fieldData = getFieldData(name, builder, link); + + if (fieldData.__kind__ !== 'link') { + throw new Error(`Field ${link} of model '${name}' is not a link (${fieldData.__kind__})`); + } + + return fieldData; +} + +export function getFieldData( + name: string, + builder: PothosSchemaTypes.SchemaBuilder, + fieldName: string, +) { + const modelData = getModel(name, builder); + const fieldData = modelData.__element__.__pointers__[fieldName]; + + if (!fieldData) { + throw new Error(`Field '${fieldName}' not found in model '${name}'`); + } + + return fieldData; +} + +export function getModel( + name: string, + builder: PothosSchemaTypes.SchemaBuilder, +) { + const typeFields = getObjectsTypes(builder); + + const typeData = typeFields[name]; + if (!typeData) { + throw new Error(`Type '${name}' not found in schema's default exports.`); + } + + return typeData; +} diff --git a/packages/plugin-edgedb/src/util/description.ts b/packages/plugin-edgedb/src/util/description.ts new file mode 100644 index 000000000..e6b288670 --- /dev/null +++ b/packages/plugin-edgedb/src/util/description.ts @@ -0,0 +1,17 @@ +import { SchemaTypes } from '@pothos/core'; +import { getModel } from './datamodel'; + +export function getModelDescription( + model: string, + builder: PothosSchemaTypes.SchemaBuilder, + description?: string | false, +) { + const { exposeDescriptions } = builder.options.edgeDB; + const useEdgeDBDescription = exposeDescriptions === true; + // || (typeof exposeDescriptions === 'object' && exposeDescriptions?.models === true); + + return ( + // (useEdgeDBDescription ? description ?? getModel(model, builder).documentation : description) || + undefined + ); +} diff --git a/packages/plugin-edgedb/src/util/get-client.ts b/packages/plugin-edgedb/src/util/get-client.ts new file mode 100644 index 000000000..502d5ecc9 --- /dev/null +++ b/packages/plugin-edgedb/src/util/get-client.ts @@ -0,0 +1,23 @@ +import { SchemaTypes } from '@pothos/core'; +import { EdgeDB, EdgeDBDriver } from '../types'; + +export function getDriver( + builder: PothosSchemaTypes.SchemaBuilder, + context: Types['Context'], +): EdgeDBDriver { + if (typeof builder.options.edgeDB.client === 'function') { + console.warn('Not implemented yet'); + } + + return builder.options.edgeDB.client; +} + +export function getObjectsTypes( + builder: PothosSchemaTypes.SchemaBuilder, +): EdgeDB.Datamodel { + if (!builder.options.edgeDB.qb.default) { + console.warn(`Missing EdgeDB Query Builder.`); + } + + return builder.options.edgeDB.qb.default as EdgeDB.Datamodel; +} diff --git a/packages/plugin-edgedb/src/util/links.ts b/packages/plugin-edgedb/src/util/links.ts new file mode 100644 index 000000000..1cf0adcf1 --- /dev/null +++ b/packages/plugin-edgedb/src/util/links.ts @@ -0,0 +1,5 @@ +import { Cardinality, LinkDesc } from '../types'; + +export function isMultiLink(field: LinkDesc): boolean { + return field.cardinality === Cardinality.Many || field.cardinality === Cardinality.AtLeastOne; +} diff --git a/packages/plugin-edgedb/src/util/relation-map.ts b/packages/plugin-edgedb/src/util/relation-map.ts new file mode 100644 index 000000000..0d937e86f --- /dev/null +++ b/packages/plugin-edgedb/src/util/relation-map.ts @@ -0,0 +1,41 @@ +import { createContextCache } from '@pothos/core'; +import { EdgeDB } from '../types'; + +export interface FieldMap { + type: string; + links: Map; +} +export type RelationMap = Map; + +export const getRelationMap = createContextCache((edgeDBDefaultExports: unknown) => + createRelationMap(edgeDBDefaultExports as EdgeDB.Datamodel), +); + +export function createRelationMap(edgeDBDefault: EdgeDB.Datamodel) { + const relationMap: RelationMap = new Map(); + + Object.entries(edgeDBDefault).forEach(([type]) => { + relationMap.set(type, { + type, + links: new Map([]), + }); + }); + + Object.entries(edgeDBDefault).forEach(([type, { __element__ }]) => { + const map = relationMap.get(type)!.links; + + Object.entries(__element__.__pointers__) + .filter( + ([key, entry]) => + !String(key).startsWith('<') && + String(key) !== '__type__' && + String(entry.__kind__) === 'link', + ) + .forEach(([linkName, entry]) => { + const typeName = entry.target.__name__.replace('default::', ''); + map.set(linkName, relationMap.get(typeName)!); + }); + }); + + return relationMap; +} diff --git a/packages/plugin-edgedb/src/util/target.ts b/packages/plugin-edgedb/src/util/target.ts new file mode 100644 index 000000000..1b9378590 --- /dev/null +++ b/packages/plugin-edgedb/src/util/target.ts @@ -0,0 +1,17 @@ +import { ObjectType } from '../types'; + +// To get the type name of an object type, extract the `target`s `__name__` prop. +// eg. { target: { __name__: "default::User" } } -> "default::User" -> "User" +export function extractTargetTypeName(target: ObjectType): string { + const targetName = target.__name__; + if (!targetName || typeof targetName !== 'string') { + throw new Error(`Target type has no name: ${target}.`); + } + + let prefix: string = ''; + if (targetName.startsWith('default::')) { + prefix = 'default::'; + } + + return targetName.replace(prefix, ''); +} diff --git a/packages/plugin-edgedb/tests/.gitignore b/packages/plugin-edgedb/tests/.gitignore new file mode 100644 index 000000000..859ae3bfb --- /dev/null +++ b/packages/plugin-edgedb/tests/.gitignore @@ -0,0 +1 @@ +client/* \ No newline at end of file diff --git a/packages/plugin-edgedb/tests/__snapshots__/index.test.ts.snap b/packages/plugin-edgedb/tests/__snapshots__/index.test.ts.snap new file mode 100644 index 000000000..83a4c5e41 --- /dev/null +++ b/packages/plugin-edgedb/tests/__snapshots__/index.test.ts.snap @@ -0,0 +1,37 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`edgedb generates schema 1`] = ` +"scalar DateTime + +type ExplicitEdgeDBUser { + email: String! + id: ID! + name: String +} + +type Post { + author: User! + content: String + createdAt: DateTime! + id: ID! + published: Boolean! + title: String! +} + +type PostPreview { + id: ID! + preview: String +} + +type Query { + me: ExplicitEdgeDBUser + users: [User!] +} + +type User { + email: String! + id: ID! + name: String + posts: [Post!]! +}" +`; diff --git a/packages/plugin-edgedb/tests/example/builder.ts b/packages/plugin-edgedb/tests/example/builder.ts new file mode 100644 index 000000000..0cdd6e157 --- /dev/null +++ b/packages/plugin-edgedb/tests/example/builder.ts @@ -0,0 +1,35 @@ +import SchemaBuilder from '@pothos/core'; +import SimpleObjects from '@pothos/plugin-simple-objects'; +import EdgeDBPlugin from '../../src'; +import { db as edgeDBDriver } from './db'; +import edgeDBQB, { types as EdgeDBTypes } from '../../dbschema/edgeql-js'; + +const builder = new SchemaBuilder<{ + Context: { + user: { id: string }; + }; + Scalars: { + ID: { Input: string; Output: string | number }; + DateTime: { Input: Date; Output: Date }; + }; + EdgeDBTypes: EdgeDBTypes; +}>({ + plugins: [EdgeDBPlugin, SimpleObjects], + edgeDB: { + qb: edgeDBQB, + client: edgeDBDriver, + }, +}); + +builder.scalarType('DateTime', { + serialize: (date) => date.toISOString(), + parseValue: (date) => { + if (typeof date !== 'string') { + throw new Error('Unknown date value.'); + } + + return new Date(date); + }, +}); + +export default builder; diff --git a/packages/plugin-edgedb/tests/example/db.ts b/packages/plugin-edgedb/tests/example/db.ts new file mode 100644 index 000000000..f8aaa078b --- /dev/null +++ b/packages/plugin-edgedb/tests/example/db.ts @@ -0,0 +1,14 @@ +import createClient, { Client } from 'edgedb'; + +let db: Client; + +declare global { + var __db__: Client | undefined; +} + +if (process.env.NODE_ENV !== 'production') { + if (!global.__db__) global.__db__ = createClient({ logging: true }); + db = global.__db__; +} + +export { db }; diff --git a/packages/plugin-edgedb/tests/example/schema/index.ts b/packages/plugin-edgedb/tests/example/schema/index.ts new file mode 100644 index 000000000..648f9dd17 --- /dev/null +++ b/packages/plugin-edgedb/tests/example/schema/index.ts @@ -0,0 +1,98 @@ +import e, { Post, User } from '../../client'; +import { db } from '../db'; +import builder from '../builder'; + +const PostPreview = builder.objectRef('PostPreview'); +PostPreview.implement({ + fields: (t) => ({ + id: t.exposeID('id'), + preview: t.string({ + nullable: true, + resolve: (post) => post.content?.slice(10), + }), + }), +}); + +const UserRef = builder.objectRef('ExplicitEdgeDBUser'); +UserRef.implement({ + fields: (t) => ({ + id: t.exposeID('id'), + email: t.exposeString('email'), + name: t.exposeString('name', { nullable: true }), + }), +}); + +builder.edgeDBObject('Post', { + fields: (t) => ({ + id: t.exposeID('id'), + title: t.exposeString('title'), + content: t.exposeString('content', { nullable: true }), + published: t.exposeBoolean('published'), + createdAt: t.expose('created_at', { type: 'DateTime' }), + author: t.link('author'), + }), +}); + +builder.edgeDBObject('User', { + fields: (t) => ({ + id: t.exposeID('id'), + email: t.exposeString('email'), + name: t.exposeString('name', { nullable: true }), + posts: t.link('posts'), + }), +}); + +builder.queryType({ + fields: (t) => ({ + me: t.field({ + type: UserRef, + nullable: true, + // Temporarily since `User` doesnt have the links defined yet. + // @ts-ignore + resolve: async (root, args, ctx, info) => { + const user = await e + .select(e.User, (user) => ({ + id: true, + email: true, + name: true, + filter: e.op(user.id, '=', e.uuid(ctx.user.id)), + })) + .run(db); + + return user; + }, + }), + user: t.edgeDBField({ + type: 'User', + nullable: true, + resolve: async (_query, _parent, _args, ctx) => { + const user = await e + .select(e.User, (user) => ({ + ...e.User['*'], + filter: e.op(user.id, '=', e.uuid(ctx.user.id)), + })) + .run(db); + return user; + // ^? + }, + }), + users: t.edgeDBField({ + type: ['User'], + nullable: true, + resolve: async (_query, _parent, _args, ctx) => { + const users = await e + .select(e.User, (user) => ({ + id: true, + email: true, + name: true, + })) + .run(db); + + return users; + // ^? + }, + }), + }), +}); + +export default builder.toSchema({}); diff --git a/packages/plugin-edgedb/tests/example/server.ts b/packages/plugin-edgedb/tests/example/server.ts new file mode 100644 index 000000000..15ef95266 --- /dev/null +++ b/packages/plugin-edgedb/tests/example/server.ts @@ -0,0 +1,12 @@ +import { createTestServer } from '@pothos/test-utils'; +import { db } from './db'; +import schema from './schema'; + +const server = createTestServer({ + schema, + contextFactory: () => ({ user: { id: 'a04bf8b8-1bfd-11ed-93f8-836b78753212' } }), +}); + +server.listen(3000, () => { + console.log('🚀 Server started at http://127.0.0.1:3000'); +}); diff --git a/packages/plugin-edgedb/tests/index.test.ts b/packages/plugin-edgedb/tests/index.test.ts new file mode 100644 index 000000000..8eb561c56 --- /dev/null +++ b/packages/plugin-edgedb/tests/index.test.ts @@ -0,0 +1,43 @@ +import schema from './example/schema'; +import { db as edgedb } from './example/db'; +import { execute, printSchema } from 'graphql'; +import { gql } from 'graphql-tag'; + +let queries = []; + +describe('edgedb', () => { + afterEach(() => { + queries = []; + }); + + it('generates schema', () => { + const graphqlSchema = printSchema(schema); + expect(graphqlSchema).toMatchSnapshot(); + }); + + it('queries for single item', async () => { + const query = gql` + query { + me { + id + } + } + `; + + const result = await execute({ + schema, + document: query, + contextValue: { user: { id: 'a04bf8b8-1bfd-11ed-93f8-836b78753212' } }, + }); + + expect(result).toMatchInlineSnapshot(` + Object { + "data": Object { + "me": Object { + "id": "a04bf8b8-1bfd-11ed-93f8-836b78753212", + }, + }, + } + `); + }); +}); diff --git a/packages/plugin-edgedb/tsconfig.json b/packages/plugin-edgedb/tsconfig.json new file mode 100644 index 000000000..49e0952e0 --- /dev/null +++ b/packages/plugin-edgedb/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "noEmit": false, + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "dts", + "rootDir": "src" + }, + "include": [ + "src/**/*", + "dbschema/**/*" + ], + "extends": "../../tsconfig.options.json" +} diff --git a/packages/plugin-edgedb/tsconfig.type.json b/packages/plugin-edgedb/tsconfig.type.json new file mode 100644 index 000000000..31e222cc4 --- /dev/null +++ b/packages/plugin-edgedb/tsconfig.type.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "skipLibCheck": false + }, + "extends": "../../tsconfig.options.json", + "include": [ + "src/**/*", + "tests/**/*" + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 028bd8442..1dba6d36e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -223,6 +223,9 @@ importers: graphql: 16.5.0 prisma: 4.1.1 + examples/prisma-federation/prisma/client: + specifiers: {} + examples/prisma-relay: specifiers: '@faker-js/faker': ^7.4.0 @@ -243,6 +246,12 @@ importers: graphql: 16.5.0 prisma: 4.1.1 + examples/prisma-relay/prisma/client: + specifiers: {} + + examples/prisma/prisma/client: + specifiers: {} + examples/relay-windowed-pagination: specifiers: '@faker-js/faker': ^7.4.0 @@ -400,6 +409,31 @@ importers: packages/plugin-directives/esm: specifiers: {} + packages/plugin-edgedb: + specifiers: + '@pothos/core': workspace:* + '@pothos/plugin-complexity': workspace:* + '@pothos/plugin-errors': workspace:* + '@pothos/plugin-relay': workspace:* + '@pothos/plugin-simple-objects': workspace:* + '@pothos/test-utils': workspace:* + edgedb: ^0.21.3 + graphql: 16.5.0 + graphql-tag: ^2.12.6 + devDependencies: + '@pothos/core': link:../core + '@pothos/plugin-complexity': link:../plugin-complexity + '@pothos/plugin-errors': link:../plugin-errors + '@pothos/plugin-relay': link:../plugin-relay + '@pothos/plugin-simple-objects': link:../plugin-simple-objects + '@pothos/test-utils': link:../test-utils + edgedb: 0.21.3 + graphql: 16.5.0 + graphql-tag: 2.12.6_graphql@16.5.0 + + packages/plugin-edgedb/esm: + specifiers: {} + packages/plugin-errors: specifiers: '@pothos/core': workspace:* @@ -508,6 +542,9 @@ importers: packages/plugin-prisma/esm: specifiers: {} + packages/plugin-prisma/tests/client: + specifiers: {} + packages/plugin-relay: specifiers: '@pothos/core': workspace:* @@ -548,6 +585,9 @@ importers: packages/plugin-scope-auth/esm: specifiers: {} + packages/plugin-scope-auth/prisma/client: + specifiers: {} + packages/plugin-simple-objects: specifiers: '@pothos/core': workspace:* @@ -8473,6 +8513,12 @@ packages: safe-buffer: 5.2.1 dev: true + /edgedb/0.21.3: + resolution: {integrity: sha512-iFSvVRFpVVRaiMCFo3Cslol7C6xLD3mGM8MUWQYd8nSFvR2nsnyA2KjVmZw5MY0qZjdlm+8aJiCZffhjdtnLDw==} + engines: {node: '>= 10.0.0'} + hasBin: true + dev: true + /ee-first/1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}