Skip to content

Instantly share code, notes, and snippets.

@hos
Created March 25, 2021 15:32
Show Gist options
  • Save hos/20593b62a9c49bc5a8a6b5629855ee0b to your computer and use it in GitHub Desktop.
Save hos/20593b62a9c49bc5a8a6b5629855ee0b to your computer and use it in GitHub Desktop.
import { Plugin } from "graphile-build";
import {
PgAttribute,
PgClass,
PgIntrospectionResultsByKind,
} from "graphile-build-pg";
import {
embed,
gql,
makeExtendSchemaPlugin,
makePluginByCombiningPlugins,
} from "graphile-utils";
interface FindResult {
translatableTables: PgClass[];
translatableColumns: PgAttribute[];
}
const ResourceEnumName = "TranslatableResourceType";
const translationsTableName = "translations";
const TranslationPlugin: Plugin = function TranslationPlugin(builder) {
builder.hook("inflection", (inflection) => {
return {
...inflection,
translationTypeName(table: PgClass) {
return `${table.name}-translation`;
},
};
});
builder.hook("build", (build) => {
const { inflection, graphql } = build;
const introspection = build.pgIntrospectionResultsByKind as PgIntrospectionResultsByKind;
const { GraphQLObjectType } = graphql;
const findResult: FindResult = introspection.attribute.reduce<FindResult>(
(data, attribute) => {
if (attribute.tags.localize) {
data.translatableColumns.push(attribute);
const table = attribute.class;
if (!data.translatableTables.includes(table)) {
data.translatableTables.push(table);
build.newWithHooks(
GraphQLObjectType,
{
name: inflection.translationTypeName(table),
description: build.wrapDescription(
`Translation type for \`"${inflection.tableType(table)}"\``,
"type"
),
fields: () => {},
},
{ pgIntrospection: table, isTranslation: true }
);
}
}
return data;
},
{ translatableColumns: [], translatableTables: [] }
);
const { translatableColumns, translatableTables } = findResult;
build.newWithHooks(
build.graphql.GraphQLEnumType,
{
name: ResourceEnumName,
values: {
...translatableTables.reduce((all, table) => {
const name = inflection.enumName(
inflection.singularize(table.name)
);
all[name] = { value: table.name };
return all;
}, {}),
},
},
{
isResourceTypeEnum: true,
}
);
// const findIntrospectionByGraphql
return build.extend(build, {
translatableColumns,
translatableTables,
});
});
// Update resource types on types
builder.hook("GraphQLObjectType:fields", (typeConfig, build, context) => {
const { scope } = context;
const { graphql } = build;
const { isNonNullType, GraphQLNonNull } = graphql;
const introspection = scope.pgIntrospection;
if (
!introspection ||
!scope.isPgRowType ||
introspection.name !== translationsTableName
) {
return typeConfig;
}
const enumType = build.getTypeByName(ResourceEnumName);
const isNotNull = isNonNullType(typeConfig.resourceType.type);
const newType = isNotNull ? new GraphQLNonNull(enumType) : enumType;
return {
...typeConfig,
resourceType: { ...typeConfig.resourceType, type: newType },
};
});
// Update resource types on inputs
builder.hook(
"GraphQLInputObjectType:fields",
(typeConfig, build, context) => {
const { scope } = context;
const { graphql } = build;
const { GraphQLNonNull, isNonNullType } = graphql;
const introspection = scope.pgIntrospection;
if (
!introspection ||
introspection.name !== translationsTableName ||
!typeConfig.resourceType
) {
return typeConfig;
}
const enumType = build.getTypeByName(ResourceEnumName);
const isNotNull = isNonNullType(typeConfig.resourceType.type);
const newType = isNotNull ? new GraphQLNonNull(enumType) : enumType;
return {
...typeConfig,
resourceType: {
...typeConfig.resourceType,
type: newType,
},
};
}
);
};
TranslationPlugin.displayName = "TranslationPlugin";
const TranslatableSchemas = makeExtendSchemaPlugin((build) => {
const { inflection, pgSql: sql } = build;
const translatableTables = build.translatableTables as FindResult["translatableTables"];
const typeDefs = translatableTables.map(
(tb) => gql`
extend type ${inflection.tableType(tb)} {
translations: [Translation!]! @pgQuery(
source: ${embed(
sql.fragment`app_public.${sql.identifier(translationsTableName)}`
)}
withQueryBuilder: ${embed((queryBuilder) => {
queryBuilder.where(
sql.fragment`${queryBuilder.getTableAlias()}.resource_id = ${queryBuilder.parentQueryBuilder.getTableAlias()}.id`
);
queryBuilder.where(
sql.fragment`${queryBuilder.getTableAlias()}.resource_type = ${sql.literal(
tb.name
)}`
);
})}
)
}`
);
return { typeDefs };
}, "TranslatableSchemas");
interface ITranslationInput {
locale: string;
key: string;
value: string;
translatableContentDigest: string;
}
const TranslationsRegisterPlugin = makeExtendSchemaPlugin((build) => {
const translatableTables = build.translatableTables as FindResult["translatableTables"];
return {
typeDefs: gql`
type TranslationsRegisterPayload {
translations: [Translation!]
}
input ATranslationInput {
# The locale of the translation.
locale: String!
# The key of the translation.
key: String!
# Translation value.
value: String!
}
extend type Mutation {
translationsRegister(
# The node ID of the resource.
resourceId: ID!
translations: [ATranslationInput!]!
): TranslationsRegisterPayload!
}
`,
resolvers: {
Mutation: {
async translationsRegister(_source, args, context, info) {
const { graphile } = info;
const { pgClient } = context;
const {
getTypeAndIdentifiersFromNodeId,
inflection,
} = graphile.build;
const { resourceId, translations } = args as {
resourceId: string;
translations: ITranslationInput[];
};
const params: any[] = [];
let query = translations.reduce((_query, trans, index, arr) => {
const { Type, identifiers } = getTypeAndIdentifiersFromNodeId(
resourceId
);
if (identifiers.length !== 1) {
throw new Error(
`The type "${Type.name}" is not supported for translations, as it have multiple identifiers.`
);
}
const table = translatableTables.find((table) => {
return inflection.tableType(table) === Type.name;
});
if (!table) {
throw new Error(
`Invalid type "${Type.name}" for translations, please make sure it have translatable resources.`
);
}
params.push(identifiers[0], Type.name, table.name, trans.value);
return (
_query +
`($${index + 1}, $${index + 2}, $${index + 3}, $${index + 4})` +
(index !== arr.length - 1 ? ",\n" : "")
);
}, `insert into app_public.${translationsTableName}(resource_id, resource_type, key, value) values `);
query +=
" on conflict (resource_id, resource_type, key) do update set value = EXCLUDED.value";
query += " returning *";
const { rows } = await pgClient.query(query, params);
debugger;
return { translations: rows, query: build.$$isQuery };
},
},
},
};
}, "TranslationsRegister");
const TranslationsRemovePlugin = makeExtendSchemaPlugin((_build) => {
return {
typeDefs: gql`
type TranslationsRemovePayload {
translations: [Translation!]!
}
extend type Mutation {
translationsRemove(
resourceId: ID!
# ID of a translatable resource.
translationKeys: [String!]!
# List of translation keys.
locales: [String!]! # List of translation locales.
): TranslationsRemovePayload!
}
`,
resolvers: {},
};
}, "TranslationsRemove");
export default makePluginByCombiningPlugins(
TranslationPlugin,
TranslatableSchemas,
TranslationsRegisterPlugin,
TranslationsRemovePlugin
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment