Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/ws2 2949 locking add graphql support #205

Merged
merged 17 commits into from
Feb 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 25 additions & 8 deletions packages/devtools/src/formats/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ type ExtractSchemaParams = {
ownName?: string
required?: boolean
localRef?: boolean
immutable?: boolean
}

type RuntimeModelDefinition = {
Expand All @@ -76,6 +77,7 @@ export class RuntimeModelBuilder {
#objects: Record<string, RuntimeObjectFields> = {}
#enums: Record<string, Array<string>> = {}
#unions: Record<string, Array<string>> = {}
#immutableFields: Array<string> = []

constructor(params: RuntimeModelBuilderParams) {
this.#commonEmbeds = params.commonEmbeds ?? []
Expand All @@ -85,6 +87,8 @@ export class RuntimeModelBuilder {
this.#modelSchema = params.definition.schema
this.#modelViews = params.views
this.#modelIndices = params.indices
this.#immutableFields =
params.definition.version === '1.0' ? [] : params.definition.immutableFields ?? []
}

build(): RuntimeModelDefinition {
Expand Down Expand Up @@ -128,6 +132,7 @@ export class RuntimeModelBuilder {
ownName: propKey,
parentName: ownName,
required: requiredProps.includes(propKey),
immutable: this.#immutableFields.includes(propKey),
})
}
return fields
Expand All @@ -153,10 +158,16 @@ export class RuntimeModelBuilder {
}

const required = params.required ?? false
const immutable = params.immutable ?? false
const items = schema.items as AnySchema

if (items.$ref != null) {
return { type: 'list', required, item: this._buildListReference(items.$ref, params) }
return {
type: 'list',
required,
immutable,
item: this._buildListReference(items.$ref, params),
}
}
if (items.type == null) {
throw new Error('Missing schema $ref or type for array items')
Expand All @@ -173,7 +184,7 @@ export class RuntimeModelBuilder {
item = this._buildScalar(items as ScalarSchema, params)
break
}
return { type: 'list', required, item }
return { type: 'list', required, immutable, item }
}

_buildListReference(
Expand Down Expand Up @@ -208,6 +219,7 @@ export class RuntimeModelBuilder {
refType: 'object',
refName: ownName,
required: params.required ?? false,
immutable: params.immutable ?? false,
}
}

Expand All @@ -224,6 +236,7 @@ export class RuntimeModelBuilder {
refType: 'enum',
refName: ownName,
required: params.required ?? false,
immutable: params.immutable ?? false,
}
}

Expand All @@ -249,17 +262,19 @@ export class RuntimeModelBuilder {
}

const required = params.required ?? false
const immutable = params.immutable ?? false

switch (schema.type) {
case 'boolean':
case 'integer':
return { type: schema.type, required }
return { type: schema.type, required, immutable }
case 'number':
return { type: 'float', required }
return { type: 'float', required, immutable }
case 'string':
return {
type: getStringScalarType(schema),
required,
immutable,
}
}
}
Expand Down Expand Up @@ -326,19 +341,21 @@ export function createRuntimeDefinition(

for (const [modelID, modelDefinition] of Object.entries(definition.models)) {
const modelName = definition.aliases?.[modelID] ?? modelDefinition.name
const interfaceDefinition =
modelDefinition.version === '1.0'
? { interface: false, implements: [] }
: { interface: modelDefinition.interface, implements: modelDefinition.implements }
const isV1 = modelDefinition.version === '1.0'
const interfaceDefinition = isV1
? { interface: false, implements: [] }
: { interface: modelDefinition.interface, implements: modelDefinition.implements }
// Add name to model metadata mapping
runtime.models[modelName] = {
...interfaceDefinition,
id: modelID,
accountRelation: modelDefinition.accountRelation,
}

// Extract objects, enums, relations and views from model schema
const modelViews = modelDefinition.views ?? {}
const compositeModelViews = definition.views?.models?.[modelID] ?? {}

const modelBuilder = new RuntimeModelBuilder({
commonEmbeds: definition.commonEmbeds,
name: modelName,
Expand Down
69 changes: 57 additions & 12 deletions packages/devtools/src/schema/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,11 @@ export class SchemaParser {
return type
}
const directives = getDirectives(this.#schema, type)
const object = this._parseObject(type)
const object = this._parseObject(type, directives)
this.#def.objects[type.name] = object
const model = this._parseModelDirective(type, directives, object)
if (model == null) {
throw new Error(`Missing @createModel or @loadModel directive for interface ${type.name}`)
// this.#def.interfaces[type.name] = this._parseObject(type)
} else {
this.#def.models[type.name] = model
}
Expand All @@ -106,7 +105,7 @@ export class SchemaParser {
[MapperKind.OBJECT_TYPE]: (type: GraphQLObjectType) => {
const directives = getDirectives(this.#schema, type)
const indices = this._parseIndices(directives)
const object = this._parseObject(type)
const object = this._parseObject(type, directives)
object.indices = indices
this.#def.objects[type.name] = object
const model = this._parseModelDirective(type, directives, object)
Expand Down Expand Up @@ -268,19 +267,41 @@ export class SchemaParser {
)
}

const inheritedImmutableFields: Array<string> = type
PaulLeCam marked this conversation as resolved.
Show resolved Hide resolved
.getInterfaces()
.flatMap((interfaceObj) => {
const fields = interfaceObj.getFields()
return Object.values(fields)
.filter((field) => {
const { directives } = field.astNode as unknown as {
directives: Array<{ name: { value: string } }>
}
return directives.some((directive) => directive.name.value === 'immutable')
})
.map((field) => field.name)
})

return {
action: 'create',
interface: isInterfaceType(type),
implements: type.getInterfaces().map((i) => i.name),
immutableFields: Array.from(new Set(
Object.keys(object.properties)
.filter((key) => object.properties[key].immutable === true)
.concat(inheritedImmutableFields)
)),
description: args.description,
accountRelation: accountRelationValue,
relations: object.relations,
}
}
}

_parseObject(type: GraphQLInterfaceType | GraphQLObjectType): ObjectDefinition {
const { definition, references, relations } = this._parseObjectFields(type)
_parseObject(
type: GraphQLInterfaceType | GraphQLObjectType,
directives: Array<DirectiveAnnotation>,
): ObjectDefinition {
const { definition, references, relations } = this._parseObjectFields(type, directives)
return {
// implements: type.getInterfaces().map((i) => i.name),
properties: definition,
Expand All @@ -290,11 +311,15 @@ export class SchemaParser {
}
}

_parseObjectFields(type: GraphQLInterfaceType | GraphQLObjectType): IntermediaryObjectDefinition {
_parseObjectFields(
type: GraphQLInterfaceType | GraphQLObjectType,
directives: Array<DirectiveAnnotation>,
): IntermediaryObjectDefinition {
const objectFields = type.getFields()
const fields: ObjectFieldsDefinition = {}
let references: Array<string> = []
const relations: ModelRelationsDefinition = {}
const hasCreateModel = directives.some((directive) => directive.name === 'createModel') ?? false

for (const [key, value] of Object.entries(objectFields)) {
const directives = getDirectives(this.#schema, value)
Expand All @@ -312,15 +337,23 @@ export class SchemaParser {
if (view != null) {
fields[key] = view
} else if (isListType(innerType)) {
const list = this._parseListType(type.name, key, innerType, required, directives)
const list = this._parseListType(
type.name,
key,
innerType,
required,
directives,
hasCreateModel,
)
fields[key] = list.definition
references = [...references, ...list.references]
} else {
const listDirective = directives.find((d) => d.name === 'list')
if (listDirective != null) {
throw new Error(`Unexpected @list directive on field ${key} of object ${type.name}`)
}
const item = this._parseItemType(type.name, key, value.type, directives)

const item = this._parseItemType(type.name, key, value.type, directives, hasCreateModel)
fields[key] = item.definition
references = [...references, ...item.references]
}
Expand Down Expand Up @@ -370,14 +403,14 @@ export class SchemaParser {
`Unsupported @documentAccount directive on field ${fieldName} of object ${objectName}, @documentAccount can only be set on a DID scalar`,
)
}
return { type: 'view', required: true, viewType: 'documentAccount' }
return { type: 'view', required: true, immutable: false, viewType: 'documentAccount' }
case 'documentVersion':
if (!isScalarType(type) || type.name !== 'CommitID') {
throw new Error(
`Unsupported @documentVersion directive on field ${fieldName} of object ${objectName}, @documentVersion can only be set on a CommitID scalar`,
)
}
return { type: 'view', required: true, viewType: 'documentVersion' }
return { type: 'view', required: true, immutable: false, viewType: 'documentVersion' }
case 'relationDocument': {
if (!isInterfaceType(type) && !isObjectType(type)) {
throw new Error(
Expand All @@ -398,6 +431,7 @@ export class SchemaParser {
return {
type: 'view',
required: false,
immutable: false,
viewType: 'relation',
relation: {
source: 'document',
Expand All @@ -422,6 +456,7 @@ export class SchemaParser {
return {
type: 'view',
required: true,
immutable: false,
viewType: 'relation',
relation: { source: 'queryConnection', model, property },
}
Expand All @@ -447,6 +482,7 @@ export class SchemaParser {
return {
type: 'view',
required: true,
immutable: false,
viewType: 'relation',
relation: { source: 'queryCount', model, property },
}
Expand Down Expand Up @@ -480,6 +516,7 @@ export class SchemaParser {
type: GraphQLList<GraphQLType>,
required: boolean,
directives: Array<DirectiveAnnotation>,
hasCreateModel: boolean,
): DefinitionWithReferences<ListFieldDefinition> {
const list = directives.find((d) => d.name === 'list')
if (list == null) {
Expand All @@ -491,7 +528,7 @@ export class SchemaParser {
)
}

const item = this._parseItemType(objectName, fieldName, type.ofType, directives)
const item = this._parseItemType(objectName, fieldName, type.ofType, directives, hasCreateModel)
const definition: ListFieldDefinition = {
type: 'list',
required,
Expand All @@ -509,8 +546,15 @@ export class SchemaParser {
fieldName: string,
type: GraphQLType,
directives: Array<DirectiveAnnotation>,
hasCreateModel: boolean,
): DefinitionWithReferences<ItemDefinition> {
const required = isNonNullType(type)
const immutable = directives.some((item) => item.name === 'immutable')
JulissaDantes marked this conversation as resolved.
Show resolved Hide resolved
if (immutable && !hasCreateModel) {
throw new Error(
`Unsupported immutable directive for ${fieldName} on nested object ${objectName}`,
)
}
const innerType = required ? type.ofType : type
if (isListType(innerType)) {
throw new Error(`Unsupported nested list on field ${fieldName} of object ${objectName}`)
Expand All @@ -519,7 +563,7 @@ export class SchemaParser {
const referenceType = this._getReferenceFieldType(innerType)
if (referenceType != null) {
return {
definition: { type: referenceType, required, name: innerType.name },
definition: { type: referenceType, required, immutable, name: innerType.name },
references: [innerType.name],
}
}
Expand All @@ -529,6 +573,7 @@ export class SchemaParser {
definition: {
type: 'scalar',
required,
immutable,
schema: this._parseScalarSchema(objectName, fieldName, innerType, directives),
},
references: [],
Expand Down
2 changes: 2 additions & 0 deletions packages/devtools/src/schema/resolution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ function executeCreateFactory(
relationsPromise,
promiseMap(viewsPromises, (viewPromise) => viewPromise),
])

// Always convert to a v2 definition
const newDefinition: ModelDefinitionV2 = {
version: '2.0',
Expand All @@ -128,6 +129,7 @@ function executeCreateFactory(
interface: isV1 ? false : sourceDefinition.interface,
implements: implementIDs,
schema: sourceDefinition.schema,
immutableFields: isV1 ? [] : sourceDefinition.immutableFields,
relations,
views,
}
Expand Down
5 changes: 3 additions & 2 deletions packages/devtools/src/schema/type-definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import { DOC_ID_FIELD, NODE_INTERFACE_NAME } from '../constants.js'

import { extraScalars } from './scalars.js'

const scarlarDefinitions = Object.keys(extraScalars)
const scalarDefinitions = Object.keys(extraScalars)
.map((name) => `scalar ${name}`)
.join('\n')

export const typeDefinitions = `
# Added scalars

${scarlarDefinitions}
${scalarDefinitions}

# Field validation and configuration

Expand All @@ -26,6 +26,7 @@ directive @list(minLength: Int, maxLength: Int!) on FIELD_DEFINITION

directive @documentAccount on FIELD_DEFINITION
directive @documentVersion on FIELD_DEFINITION
directive @immutable on FIELD_DEFINITION

# Relation definitions

Expand Down
2 changes: 2 additions & 0 deletions packages/devtools/src/schema/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export { type Field } from '@ceramicnetwork/common'

export type FieldCommonDefinition = {
required: boolean
immutable?: boolean
}

export type EnumFieldDefinition = FieldCommonDefinition & {
Expand Down Expand Up @@ -62,6 +63,7 @@ export type ParsedCreateModelDefinition = {
description: string
accountRelation: ModelAccountRelationV2
relations: ModelRelationsDefinitionV2
immutableFields: Array<string>
}

export type ParsedLoadModelDefinition = {
Expand Down
Loading
Loading