diff --git a/packages/generate/dbschema/default.esdl b/packages/generate/dbschema/default.esdl index f7e0889bb..81a1b97ea 100644 --- a/packages/generate/dbschema/default.esdl +++ b/packages/generate/dbschema/default.esdl @@ -20,7 +20,13 @@ module default { scalar type global_seq extending sequence; global seq_global -> global_seq; + abstract type Power { + property name -> str; + } + type GoodPower extending Power {} + + type EvilPower extending Power {} abstract link movie_character { property character_name -> str; @@ -31,16 +37,21 @@ module default { constraint exclusive; }; property height -> decimal; + multi link powers -> Power; } - type Villain extending Person { + abstract type MainCharacter extending Person {} + + type Villain extending MainCharacter { link nemesis -> Hero; + overloaded multi link powers -> EvilPower; } - type Hero extending Person { + type Hero extending MainCharacter { property secret_identity -> str; property number_of_movies -> int64; multi link villains := . GoodPower; } scalar type year extending int16 { diff --git a/packages/generate/dbschema/migrations/00021.edgeql b/packages/generate/dbschema/migrations/00021.edgeql new file mode 100644 index 000000000..32aca7f9d --- /dev/null +++ b/packages/generate/dbschema/migrations/00021.edgeql @@ -0,0 +1,35 @@ +CREATE MIGRATION m1u4jn5jcn65vrccu2a2ynlio44tm67exuplgdmmu4m2q2ej5u2fxa + ONTO m1sxhoqfjqn7vtpmatzanmwydtxndf3jlf33npkblmya42fx3bcdoa +{ + CREATE ABSTRACT TYPE default::Power { + CREATE PROPERTY name -> std::str; + }; + CREATE TYPE default::EvilPower EXTENDING default::Power; + ALTER TYPE default::Person { + CREATE MULTI LINK powers -> default::Power; + }; + CREATE ABSTRACT TYPE default::MainCharacter EXTENDING default::Person; + ALTER TYPE default::Villain { + DROP EXTENDING default::Person; + EXTENDING default::MainCharacter LAST; + }; + ALTER TYPE default::Villain { + ALTER LINK powers { + SET MULTI; + SET OWNED; + SET TYPE default::EvilPower USING ({}); + }; + }; + CREATE TYPE default::GoodPower EXTENDING default::Power; + ALTER TYPE default::Hero { + DROP EXTENDING default::Person; + EXTENDING default::MainCharacter LAST; + }; + ALTER TYPE default::Hero { + ALTER LINK powers { + SET MULTI; + SET OWNED; + SET TYPE default::GoodPower USING ({}); + }; + }; +}; diff --git a/packages/generate/jest.config.js b/packages/generate/jest.config.js index 14bb9c484..0d1006dcf 100644 --- a/packages/generate/jest.config.js +++ b/packages/generate/jest.config.js @@ -3,10 +3,8 @@ module.exports = { testEnvironment: "node", testPathIgnorePatterns: ["./dist", "./esm", "./mts", "./cjs", "./deno"], globalSetup: "./test/globalSetup.ts", - transform: {}, - globals: { - "ts-jest": { - tsconfig: "tsconfig.json" - } - } + transform: { + "^.+\\.tsx?$": "ts-jest", + }, + globals: {}, }; diff --git a/packages/generate/src/edgeql-js/generateObjectTypes.ts b/packages/generate/src/edgeql-js/generateObjectTypes.ts index 5ec96b0ec..303505dc0 100644 --- a/packages/generate/src/edgeql-js/generateObjectTypes.ts +++ b/packages/generate/src/edgeql-js/generateObjectTypes.ts @@ -1,4 +1,4 @@ -import { CodeFragment, dts, r, t, ts } from "../builders"; +import { CodeFragment, IdentRef, dts, r, t, ts } from "../builders"; import type { GeneratorParams } from "../genutil"; import type { $ } from "../genutil"; import { @@ -381,12 +381,21 @@ export const generateObjectTypes = (params: GeneratorParams) => { const baseTypesUnion = type.bases.length ? frag`${joinFrags( type.bases.map((base) => { + function getRecursiveLinks(b: { + id: string; + }): ($.introspect.Pointer | $.introspect.Backlink)[] { + const { pointers, backlinks, backlink_stubs, bases } = types.get( + b.id + ) as $.introspect.ObjectType; + return [ + ...pointers, + ...backlinks, + ...backlink_stubs, + ...(bases.length ? bases.flatMap(getRecursiveLinks) : []), + ]; + } const baseType = types.get(base.id) as $.introspect.ObjectType; - const overloadedFields = [ - ...baseType.pointers, - ...baseType.backlinks, - ...baseType.backlink_stubs, - ] + const overloadedFields = getRecursiveLinks(base) .filter((field) => fieldNames.has(field.name)) .map((field) => quote(field.name)); const baseRef = getRef(baseType.name); @@ -444,34 +453,90 @@ export const generateObjectTypes = (params: GeneratorParams) => { // instantiate ObjectType subtype from shape body.writeln([ dts`declare `, - t`type ${ref} = $.ObjectType<${quote(type.name)}, ${ref}λShape, null, [`, + // type Foo = $.ObjectType< + t`type ${ref} = $.ObjectType<`, ]); - - const bases = type.bases - .map((b) => types.get(b.id)) - .map((b) => getRef(b.name)); body.indented(() => { - for (const b of bases) { - body.writeln([t`...${b}['__exclusives__'],`]); + // Name + body.writeln([t`${quote(type.name)},`]); + // Pointers + body.writeln([t`${ref}λShape, `]); + // Shape + body.writeln([t`null, `]); + // Exclusives + body.writeln([t`[`]); + + // Base types exclusives + const bases = (function findRecursiveBases( + bs: readonly { id: string }[] + ): IdentRef[] { + return [ + ...bs + .map((b) => types.get(b.id) as $.introspect.ObjectType) + .flatMap((b) => { + if (b.bases.length) { + return [...findRecursiveBases(b.bases), b]; + } + return [b]; + }) + .map((b) => getRef(b.name)), + ]; + })(type.bases); + + body.indented(() => { + for (const b of bases) { + body.writeln([t`...${b}['__exclusives__'],`]); + } + }); + + // This type exclusives + body.indented(() => { + for (const ex of type.exclusives) { + body.writeln([ + t`{`, + ...Object.keys(ex).map((key) => { + const target = types.get(ex[key].target_id); + const { staticType } = getStringRepresentation(target, { types }); + const card = `$.Cardinality.One | $.Cardinality.AtMostOne `; + return t`${key}: {__element__: ${staticType}, __cardinality__: ${card}},`; + }), + t`},`, + ]); + } + }); + + // Exclusives, End + body.writeln([t`],`]); + + // Subtypes + body.writeln([t`| ${quote(ref.name)}`]); + if ( + type.is_abstract && + !["std", "sys", "schema"].includes(splitName(type.name).mod) + ) { + const subtypes = (function findRecursiveSubtypes(sub: { + id: string; + }): $.introspect.ObjectType[] { + const subs: $.introspect.ObjectType[] = []; + for (const candidate of types.values()) { + if ( + candidate.kind === "object" && + candidate.bases.find((b) => b.id === sub.id) + ) { + subs.push(candidate); + subs.push(...findRecursiveSubtypes(candidate)); + } + } + return subs; + })(type); + + for (const sub of subtypes) { + body.writeln([t`| ${quote(getRef(sub.name).name)}`]); + } } }); - // const ref = getRef(type.name); - for (const ex of type.exclusives) { - body.writeln([ - t` {`, - ...Object.keys(ex).map((key) => { - const target = types.get(ex[key].target_id); - const { staticType } = getStringRepresentation(target, { types }); - const card = `$.Cardinality.One | $.Cardinality.AtMostOne `; - return t`${key}: {__element__: ${staticType}, __cardinality__: ${card}},`; - }), - t`},`, - ]); - // body.writeln([t`\n {${lines.join(", ")}}`]); - } - - body.writeln([t`]>;`]); + body.writeln([t`>;`]); if (type.name === "std::Object") { body.writeln([t`export `, dts`declare `, t`type $Object = ${ref}`]); diff --git a/packages/generate/src/syntax/casting.ts b/packages/generate/src/syntax/casting.ts index a6b9c6ad6..914926a94 100644 --- a/packages/generate/src/syntax/casting.ts +++ b/packages/generate/src/syntax/casting.ts @@ -15,6 +15,8 @@ import type { TupleType, TypeSet, RangeType, + ExclusiveTuple, + ObjectTypePointers, } from "./typesystem"; import type { cardutil } from "./cardinality"; @@ -73,22 +75,25 @@ export type pointerToAssignmentExpression< export type setToAssignmentExpression< Set extends TypeSet, IsSetModifier extends boolean -> = [Set] extends [PrimitiveTypeSet] +> = Set extends PrimitiveTypeSet ? | TypeSet< assignableBy, cardutil.assignable< - // Set["__cardinality__"] cardutil.overrideLowerBound > > | getAssignmentLiteral - : [Set] extends [ObjectTypeSet] + : Set extends ObjectTypeSet ? TypeSet< ObjectType< // anonymize the object type string, - Set["__element__"]["__pointers__"] + ObjectTypePointers, + any, + ExclusiveTuple, + // Allow expressions that are assignable to a supertype + Set["__element__"]["__subNames__"] >, cardutil.assignable< // Allow expressions with AtMostOne or Many cardinality in diff --git a/packages/generate/src/syntax/select.ts b/packages/generate/src/syntax/select.ts index c813db4f0..d3db90099 100644 --- a/packages/generate/src/syntax/select.ts +++ b/packages/generate/src/syntax/select.ts @@ -111,57 +111,12 @@ export type exclusivesToFilterSingle = : orLiteralValue; }; }[number]; + export type SelectModifiers = { - // export type SelectModifiers = { filter?: SelectFilterExpression; - filter_single?: // | Partial< - // typeutil.stripNever<{ - // [k in keyof T["__pointers__"]]: T["__pointers__"][k] - // extends PropertyDesc - // ? orScalarLiteral<{ - // __element__: T["__pointers__"][k]["target"]; - // __cardinality__: T["__pointers__"][k]["cardinality"]; - // }> - // : never; - // }> - // > - - // | (ObjectType extends T - // ? unknown - // : typeutil.stripNever<{ - // [k in keyof T["__pointers__"]]: T["__pointers__"][k] - // extends PropertyDesc< - // infer T, - // infer C, - // infer E - // > - // ? E extends true - // ? orScalarLiteral<{ - // __element__: T; - // __cardinality__: C; - // }> - // : never - // : never; - // }>) - exclusivesToFilterSingle | SelectFilterExpression; - - // | (ObjectType extends T - // ? unknown - // : typeutil.stripNever<{ - // [k in keyof T["__pointers__"]]: T["__pointers__"][k] - // extends PropertyDesc< - // infer T, - // infer C, - // infer E - // > - // ? E extends true - // ? orScalarLiteral<{ - // __element__: T; - // __cardinality__: C; - // }> - // : never - // : never; - // }>); + filter_single?: + | exclusivesToFilterSingle + | SelectFilterExpression; order_by?: OrderByExpression; offset?: OffsetExpression | number; limit?: LimitExpression | number; @@ -872,7 +827,9 @@ export function select( __element__: ObjectType< `${Expr["__element__"]["__name__"]}`, // _shape Expr["__element__"]["__pointers__"], - Expr["__element__"]["__shape__"] // {id: true} + Expr["__element__"]["__shape__"], // {id: true} + Expr["__element__"]["__exclusives__"], + Expr["__element__"]["__subNames__"] >; __cardinality__: Expr["__cardinality__"]; }>; diff --git a/packages/generate/src/syntax/typesystem.ts b/packages/generate/src/syntax/typesystem.ts index 7f2f0d318..f9b4a752e 100644 --- a/packages/generate/src/syntax/typesystem.ts +++ b/packages/generate/src/syntax/typesystem.ts @@ -217,14 +217,15 @@ export interface ObjectType< Name extends string = string, Pointers extends ObjectTypePointers = ObjectTypePointers, Shape extends object | null = any, - Exclusives extends ExclusiveTuple = ExclusiveTuple - // Polys extends Poly[] = any[] + Exclusives extends ExclusiveTuple = ExclusiveTuple, + SubtypeNames extends string = Name > extends BaseType { __kind__: TypeKind.object; __name__: Name; __pointers__: Pointers; __shape__: Shape; __exclusives__: Exclusives; + __subNames__: SubtypeNames; } export type PropertyTypes = diff --git a/packages/generate/test/insert.test.ts b/packages/generate/test/insert.test.ts index 31826d21d..b3af3aa1f 100644 --- a/packages/generate/test/insert.test.ts +++ b/packages/generate/test/insert.test.ts @@ -144,14 +144,22 @@ describe("insert", () => { test("nested insert", async () => { const q1 = e.insert(e.Villain, { name: e.str("villain"), + powers: e.insert(e.EvilPower, { name: "genius" }), + nemesis: e.insert(e.Hero, { name: "hero", + powers: e.for(e.set(e.str("flight"), e.str("super strength")), (name) => + e.insert(e.GoodPower, { name }) + ), }), }); const q2 = e.select(q1, () => ({ name: true, - nemesis: { name: true }, + powers: { + name: true, + }, + nemesis: { name: true, powers: { name: true } }, })); const result = await q2.run(client); @@ -159,9 +167,11 @@ describe("insert", () => { assert.deepEqual(result, { ...result, name: "villain", + powers: [{ name: "genius" }], nemesis: { ...result.nemesis, name: "hero", + powers: [{ name: "flight" }, { name: "super strength" }], }, }); diff --git a/packages/generate/test/interfaces.test.ts b/packages/generate/test/interfaces.test.ts index fee4b5884..d3f058374 100644 --- a/packages/generate/test/interfaces.test.ts +++ b/packages/generate/test/interfaces.test.ts @@ -1,6 +1,6 @@ import * as tc from "conditional-type-checks"; -import type { Movie, X, Y, Z } from "../dbschema/interfaces"; +import type { Power, Movie, X, Y, Z } from "../dbschema/interfaces"; export type Genre = | "Horror" @@ -15,6 +15,7 @@ export interface BaseObject { export interface test_Person extends BaseObject { name: string; height?: string | null; + powers: Power[]; } export interface test_Movie extends BaseObject { characters: test_Person[];