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

feat: Add owner field support #11

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
3 changes: 1 addition & 2 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:prettier/recommended",
"prettier/@typescript-eslint"
"plugin:prettier/recommended"
],
"plugins": [
"@typescript-eslint"
Expand Down
30 changes: 16 additions & 14 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,24 +32,26 @@
},
"dependencies": {
"graphql": "^14.5.8",
"graphql-transformer-common": "^4.12.0",
"graphql-transformer-core": "^6.12.0"
"graphql-transformer-common": "^4.19.3",
"graphql-transformer-core": "^6.28.6"
},
"devDependencies": {
"@types/deep-diff": "^1.0.0",
"@types/graphql": "^14.5.0",
"@types/jest": "^24.9.0",
"@types/jest": "^26.0.23",
"@types/node": "^13.1.7",
"@typescript-eslint/eslint-plugin": "^2.16.0",
"@typescript-eslint/parser": "^2.16.0",
"eslint": "^6.8.0",
"eslint-config-prettier": "^6.9.0",
"eslint-plugin-prettier": "^3.1.2",
"graphql-dynamodb-transformer": "^6.12.0",
"graphql-key-transformer": "^2.17.1",
"jest": "^24.9.0",
"prettier": "^1.19.1",
"ts-jest": "^24.3.0",
"typescript": "^3.7.5"
"@typescript-eslint/eslint-plugin": "^4.23.0",
"@typescript-eslint/parser": "^4.23.0",
"eslint": "^7.26.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^3.4.0",
"graphql-auth-transformer": "^6.24.6",
"graphql-dynamodb-transformer": "^6.22.6",
"graphql-key-transformer": "^2.23.6",
"jest": "^26.6.3",
"prettier": "^2.3.0",
"ts-jest": "^26.5.6",
"typescript": "^4.2.4"
},
"jest": {
"transform": {
Expand Down
279 changes: 144 additions & 135 deletions src/AutoTransformer.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,4 @@
import {
valueFromASTUntyped,
ObjectTypeDefinitionNode,
DirectiveNode,
ArgumentNode,
InterfaceTypeDefinitionNode,
FieldDefinitionNode,
Kind,
} from 'graphql'
import {ObjectTypeDefinitionNode, DirectiveNode, InterfaceTypeDefinitionNode, FieldDefinitionNode, Kind} from 'graphql'
import {
Transformer,
TransformerContext,
Expand All @@ -22,8 +14,14 @@ import {
unwrapNonNull,
} from 'graphql-transformer-common'
import {printBlock, compoundExpression, qref} from 'graphql-mapping-template'
import {findDirective, getArgValueFromDirective, isEmpty, removeUndefinedValue} from './utils'
import {Expression} from 'graphql-mapping-template/lib/ast'

export class AutoTransformer extends Transformer {
private templateParts: {
[parentTypeName: string]: {updatedAt?: Expression; owner?: Expression; __typename?: Expression}
} = {}

constructor() {
super(
'AutoTransformer',
Expand All @@ -35,139 +33,156 @@ export class AutoTransformer extends Transformer {

public field = (
parent: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode,
definition: FieldDefinitionNode,
fieldDefinition: FieldDefinitionNode,
directive: DirectiveNode,
ctx: TransformerContext
) => {
): void => {
if (parent.kind === Kind.INTERFACE_TYPE_DEFINITION) {
throw new InvalidDirectiveError(
`The @auto directive cannot be placed on an interface's field. See ${parent.name.value}${definition.name.value}`
`The @auto directive cannot be placed on an interface's field. See ${parent.name.value}${fieldDefinition.name.value}`
)
}

const modelDirective = parent.directives?.find((dir) => dir.name.value === 'model')
const modelDirective = findDirective(parent, 'model')
if (!modelDirective) {
throw new InvalidDirectiveError('Types annotated with @auto must also be annotated with @model.')
}

if (!isNonNullType(definition.type)) {
if (!isNonNullType(fieldDefinition.type)) {
throw new TransformerContractError(`@auto directive can only be used on non-nullable type fields`)
}

const isArg = (s: string) => (arg: ArgumentNode) => arg.name.value === s
const getArg = (directive: DirectiveNode, arg: string, dflt?: any): any => {
const argument = directive.arguments?.find(isArg(arg))
return argument ? valueFromASTUntyped(argument.value) : dflt
}

const typeName = parent.name.value
const objectTypeName = parent.name.value

const creatable = getArg(directive, 'creatable', false)
const updatable = getArg(directive, 'updatable', false)
const creatable = getArgValueFromDirective(directive, 'creatable', false)
const updatable = getArgValueFromDirective(directive, 'updatable', false)

this.updateCreateInput(ctx, typeName, definition, creatable)
this.updateUpdateInput(ctx, typeName, definition, updatable)
this.updateCreateInput(ctx, objectTypeName, fieldDefinition, creatable)
this.updateUpdateInput(ctx, objectTypeName, fieldDefinition, updatable)

// @key directive generates VTL code before @model does.
// There're three cases @key trie to use automatic variables before they're defined.
// There are three cases @key trie to use automatic variables before they're defined.
// 1. An automatic generated variable is a part of composite primary key:
// @key(fields: ["hash", "createdAt"]
// 2. An automatic generated variable is a part of range key:
// @key(name: "byThing", fields: ["hash", "sender", "createdAt"]
// 3. An automatic generated variable satisfy 1 and 2.
// To handle this problem, We generate automatic variables by ourselves.

const keyDirectives = parent.directives?.filter((dir) => dir.name.value === 'key')
if (keyDirectives) {
let useCreatedAtField = false
let useUpdatedAtField = false
let useTypeName = false

let createdAtField: string | null = null
let updatedAtField: string | null = null
const timestamps = getArg(modelDirective, 'timestamps')
switch (typeof timestamps) {
case 'object':
if (timestamps !== null) {
createdAtField = timestamps['createdAt']
updatedAtField = timestamps['updatedAt']
if (createdAtField === undefined) {
createdAtField = 'createdAt'
}
if (updatedAtField === undefined) {
updatedAtField = 'updatedAt'
}
}
break
case 'undefined':
createdAtField = 'createdAt'
updatedAtField = 'updatedAt'
break
default:
throw new Error('unreachable')
const keyDirectives = parent.directives?.filter((directive) => directive.name.value === 'key')

if (!keyDirectives) {
return
}

let useUpdatedAtField = false
let useTypeName = false
let useOwner = false
let ownerField = 'owner'

let ownerFields: string[] = []
const authDirective = findDirective(parent, 'auth')
if (authDirective) {
const rules = getArgValueFromDirective(authDirective, 'rules') as {ownerField?: string}[]
ownerFields = rules.map((rule) => rule.ownerField || 'owner') as string[]
}

const defaultTimestampConfig = {
updatedAt: 'updatedAt',
}
const timestampConfig = {
...defaultTimestampConfig,
...removeUndefinedValue(getArgValueFromDirective(modelDirective, 'timestamps', {})),
} as typeof defaultTimestampConfig
const currentFieldName = fieldDefinition.name.value

for (const keyDirective of keyDirectives) {
const keyFields = getArgValueFromDirective(keyDirective, 'fields') as string[] | undefined
const isPrimaryIndex = !getArgValueFromDirective(keyDirective, 'name')

if (!keyFields || !isPrimaryIndex || !keyFields.includes(currentFieldName)) {
continue
}

for (const kd of keyDirectives) {
const fields = getArg(kd, 'fields')
if (fields) {
if (fields.includes(createdAtField)) {
useCreatedAtField = true
}
if (fields.includes(updatedAtField)) {
useUpdatedAtField = true
}
if (fields.includes('__typename')) {
useTypeName = true
}
}
if (currentFieldName === timestampConfig.updatedAt) {
useUpdatedAtField = true
continue
}

// Update create and update mutations
const createResolverResourceId = ResolverResourceIDs.DynamoDBCreateResolverResourceID(typeName)
this.updateResolver(
ctx,
createResolverResourceId,
printBlock(`Prepare DynamoDB PutItem Request for @auto`)(
compoundExpression([
...(useCreatedAtField && createdAtField
? [
qref(
`$context.args.input.put("${createdAtField}", $util.defaultIfNull($ctx.args.input.${createdAtField}, $util.time.nowISO8601()))`
),
]
: []),
...(useUpdatedAtField && updatedAtField
? [
qref(
`$context.args.input.put("${updatedAtField}", $util.defaultIfNull($ctx.args.input.${updatedAtField}, $util.time.nowISO8601()))`
),
]
: []),
...(useTypeName ? [qref(`$context.args.input.put("__typename", "${typeName}")`)] : []),
])
)
const ownerFieldFound = keyFields.find((field) => ownerFields.includes(field))
if (currentFieldName === ownerFieldFound) {
ownerField = ownerFieldFound
useOwner = true
continue
}

if (currentFieldName === '__typename') {
useTypeName = true
}
}

if (!useUpdatedAtField && !useTypeName && !useOwner) {
return
}

if (!this.templateParts[objectTypeName]) {
this.templateParts[objectTypeName] = {}
}

if (useUpdatedAtField) {
this.templateParts[objectTypeName].updatedAt = qref(
`$context.args.input.put("${timestampConfig.updatedAt}", $util.defaultIfNull($ctx.args.input.${timestampConfig.updatedAt}, $util.time.nowISO8601()))`
)
return
}

const updateResolverResourceId = ResolverResourceIDs.DynamoDBUpdateResolverResourceID(typeName)
this.updateResolver(
ctx,
updateResolverResourceId,
printBlock(`Prepare DynamoDB UpdateItem Request for @auto`)(
compoundExpression([
...(useUpdatedAtField && updatedAtField
? [
qref(
`$context.args.input.put("${updatedAtField}", $util.defaultIfNull($ctx.args.input.${updatedAtField}, $util.time.nowISO8601()))`
),
]
: []),
...(useTypeName ? [qref(`$context.args.input.put("__typename", "${typeName}")`)] : []),
])
)
if (useTypeName) {
this.templateParts[objectTypeName].__typename = qref(`$context.args.input.put("__typename", "${objectTypeName}")`)
}

if (useOwner) {
const identityValue = `$util.defaultIfNull($ctx.identity.claims.get("username"), $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), "___xamznone____"))`
this.templateParts[objectTypeName].owner = qref(
`$context.args.input.put("${ownerField}", $util.defaultIfNull($ctx.args.input.get("${ownerField}"), ${identityValue}))`
)
}
}

after = (ctx: TransformerContext): void => {
const templatePartsForObjectTypes = this.templateParts

if (isEmpty(templatePartsForObjectTypes)) {
return
}

for (const objectTypeName in templatePartsForObjectTypes) {
const templateParts = templatePartsForObjectTypes[objectTypeName]

const templateForCreateResolver = [templateParts.owner, templateParts.__typename].filter(Boolean) as Expression[]
const templateForUpdateResolver = [templateParts.owner, templateParts.updatedAt, templateParts.__typename].filter(
Boolean
) as Expression[]

if (templateForCreateResolver.length) {
const createResolverResourceId = ResolverResourceIDs.DynamoDBCreateResolverResourceID(objectTypeName)
this.updateResolver(
ctx,
createResolverResourceId,
printBlock(`[@auto] Prepare DynamoDB PutItem Request`)(compoundExpression(templateForCreateResolver))
)
}

if (templateForUpdateResolver.length) {
const updateResolverResourceId = ResolverResourceIDs.DynamoDBUpdateResolverResourceID(objectTypeName)
this.updateResolver(
ctx,
updateResolverResourceId,
printBlock(`[@auto] Prepare DynamoDB UpdateItem Request`)(compoundExpression(templateForUpdateResolver))
)
}
}
}

private updateCreateInput(
ctx: TransformerContext,
typeName: string,
Expand All @@ -194,36 +209,30 @@ export class AutoTransformer extends Transformer {
nullable: boolean
) {
const input = ctx.getType(inputName)
if (input && input.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION) {
if (input.fields) {
if (nullable) {
// make autoField nullable
ctx.putType({
...input,
fields: input.fields.map((f) => {
if (f.name.value === autoField.name.value) {
return makeInputValueDefinition(autoField.name.value, unwrapNonNull(autoField.type))
}
return f
}),
})
} else {
// or strip autoField
const updatedFields = input.fields.filter((f) => f.name.value !== autoField.name.value)
if (updatedFields.length === 0) {
throw new InvalidDirectiveError(
`After stripping away version field "${autoField.name.value}", \
the create input for type "${typeName}" cannot be created \
with 0 fields. Add another field to type "${typeName}" to continue.`
)
}
ctx.putType({
...input,
fields: updatedFields,
})
}

if (!(input && input.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION && input.fields)) {
return
}

const fieldsWithoutAutoField = input.fields.filter((field) => field.name.value !== autoField.name.value)
const fieldsWithNullableAutoField = input.fields.map((field) => {
if (field.name.value === autoField.name.value) {
return makeInputValueDefinition(autoField.name.value, unwrapNonNull(autoField.type))
}
return field
})
const updatedFields = nullable ? fieldsWithNullableAutoField : fieldsWithoutAutoField

if (updatedFields.length === 0 && !nullable) {
throw new InvalidDirectiveError(
`After stripping away version field "${autoField.name.value}", the create input for type "${typeName}" cannot be created with 0 fields. Add another field to type "${typeName}" to continue.`
)
}

ctx.putType({
...input,
fields: updatedFields,
})
}

private updateResolver = (ctx: TransformerContext, resolverResourceId: string, code: string) => {
Expand Down
Loading