From ceb769755a9531dc47ecb26e1320a6edc3af928f Mon Sep 17 00:00:00 2001 From: Adam Sparks Date: Tue, 2 Aug 2022 23:54:04 -0500 Subject: [PATCH 01/22] wip --- .../src/generators/collectionMutations.ts | 36 ++++++ .../core/src/generators/collectionQueries.ts | 92 ++++++++++++++ packages/core/src/generators/schema.ts | 119 ++++-------------- packages/transformer-yaml/src/index.ts | 12 +- 4 files changed, 161 insertions(+), 98 deletions(-) create mode 100644 packages/core/src/generators/collectionMutations.ts create mode 100644 packages/core/src/generators/collectionQueries.ts diff --git a/packages/core/src/generators/collectionMutations.ts b/packages/core/src/generators/collectionMutations.ts new file mode 100644 index 00000000..a20ac161 --- /dev/null +++ b/packages/core/src/generators/collectionMutations.ts @@ -0,0 +1,36 @@ +import { ObjectTypeComposer, SchemaComposer } from 'graphql-compose'; +import { LoadedFlatbreadConfig } from '../types'; + +export interface AddCollectionMutationsArgs { + name: string; + pluralName: string; + config: LoadedFlatbreadConfig; + objectComposer: ObjectTypeComposer; + schemaComposer: SchemaComposer; + allContentNodesJSON: Record; +} + +export default function addCollectionMutations( + args: AddCollectionMutationsArgs +) { + const { + name, + pluralName, + config, + objectComposer, + schemaComposer, + allContentNodesJSON, + } = args; + + schemaComposer.Mutation.addFields({ + [`upsert${name}`]: { + type: objectComposer, + args: { [name]: objectComposer.getInputType() }, + description: `Update or create a ${name}`, + async resolve(_, payload) { + console.dir({ payload }, { depth: Infinity }); + return payload; + }, + }, + }); +} diff --git a/packages/core/src/generators/collectionQueries.ts b/packages/core/src/generators/collectionQueries.ts new file mode 100644 index 00000000..98ddc1ce --- /dev/null +++ b/packages/core/src/generators/collectionQueries.ts @@ -0,0 +1,92 @@ +import { ObjectTypeComposer, SchemaComposer } from 'graphql-compose'; +import resolveQueryArgs from '../resolvers/arguments'; + +import { + generateArgsForAllItemQuery, + generateArgsForManyItemQuery, + generateArgsForSingleItemQuery, +} from '../generators/arguments'; +import { cloneDeep } from 'lodash-es'; +import { EntryNode, LoadedFlatbreadConfig } from '../types'; + +export interface AddCollectionQueriesArgs { + name: string; + pluralName: string; + config: LoadedFlatbreadConfig; + objectComposer: ObjectTypeComposer; + schemaComposer: SchemaComposer; + allContentNodesJSON: Record; +} + +export default function addCollectionQueries(args: AddCollectionQueriesArgs) { + const { + name, + pluralName, + config, + objectComposer, + schemaComposer, + allContentNodesJSON, + } = args; + + const pluralTypeQueryName = 'all' + pluralName; + + objectComposer.addResolver({ + name: 'findById', + type: () => objectComposer, + description: `Find one ${name} by its ID`, + args: generateArgsForSingleItemQuery(), + resolve: (rp: Record) => + cloneDeep(allContentNodesJSON[name]).find( + (node: EntryNode) => node.id === rp.args.id + ), + }); + + objectComposer.addResolver({ + name: 'findMany', + type: () => [objectComposer], + description: `Find many ${pluralName} by their IDs`, + args: generateArgsForManyItemQuery(pluralName), + resolve: (rp: Record) => { + const idsToFind = rp.args.ids ?? []; + const matches = + cloneDeep(allContentNodesJSON[name])?.filter((node: EntryNode) => + idsToFind?.includes(node.id) + ) ?? []; + return resolveQueryArgs(matches, rp.args, config, { + type: { + name: name, + pluralName: pluralName, + pluralQueryName: pluralTypeQueryName, + }, + }); + }, + }); + + objectComposer.addResolver({ + name: 'all', + args: generateArgsForAllItemQuery(pluralName), + type: () => [objectComposer], + description: `Return a set of ${pluralName}`, + resolve: (rp: Record) => { + const nodes = cloneDeep(allContentNodesJSON[name]); + return resolveQueryArgs(nodes, rp.args, config, { + type: { + name: name, + pluralName: pluralName, + pluralQueryName: pluralTypeQueryName, + }, + }); + }, + }); + + schemaComposer.Query.addFields({ + /** + * Add find by ID to each content type + */ + [name]: objectComposer.getResolver('findById'), + /** + * Add find 'many' to each content type + */ + [pluralTypeQueryName]: objectComposer.getResolver('all'), + }); +} diff --git a/packages/core/src/generators/schema.ts b/packages/core/src/generators/schema.ts index d8f24283..2bf98494 100644 --- a/packages/core/src/generators/schema.ts +++ b/packages/core/src/generators/schema.ts @@ -1,28 +1,15 @@ import { schemaComposer } from 'graphql-compose'; import { composeWithJson } from 'graphql-compose-json'; -import { cloneDeep, defaultsDeep, merge } from 'lodash-es'; +import { defaultsDeep, merge } from 'lodash-es'; import plur from 'plur'; import { VFile } from 'vfile'; -import { - generateArgsForAllItemQuery, - generateArgsForManyItemQuery, - generateArgsForSingleItemQuery, -} from '../generators/arguments'; -import resolveQueryArgs from '../resolvers/arguments'; + import { cacheSchema, checkCacheForSchema } from '../cache/cache'; -import { - ConfigResult, - EntryNode, - LoadedFlatbreadConfig, - Transformer, -} from '../types'; -import { map } from '../utils/map'; +import { ConfigResult, LoadedFlatbreadConfig, Transformer } from '../types'; import { getFieldOverrides } from '../utils/fieldOverrides'; - -interface RootQueries { - maybeReturnsSingleItem: string[]; - maybeReturnsList: string[]; -} +import { map } from '../utils/map'; +import addCollectionMutations from './collectionMutations'; +import addCollectionQueries from './collectionQueries'; /** * Generates a GraphQL schema from content nodes. @@ -79,28 +66,19 @@ export async function generateSchema( ]) ); - /** - * @todo potentially able to remove this - **/ - let queries: RootQueries = { - maybeReturnsSingleItem: [], - maybeReturnsList: [], - }; - // Main builder loop - iterate through each content type and generate query resolvers + relationships for it - for (const [type, schema] of Object.entries(schemaArray)) { - const pluralType = plur(type, 2); - const pluralTypeQueryName = 'all' + pluralType; + for (const [name, objectComposer] of Object.entries(schemaArray)) { + const pluralName = plur(name, 2); // /// Global meta fields // - schema.addFields({ + objectComposer.addFields({ _collection: { type: 'String', description: 'The collection name', - resolve: () => type, + resolve: () => name, }, }); @@ -108,72 +86,23 @@ export async function generateSchema( /// Query resolvers // - schema.addResolver({ - name: 'findById', - type: () => schema, - description: `Find one ${type} by its ID`, - args: generateArgsForSingleItemQuery(), - resolve: (rp: Record) => - cloneDeep(allContentNodesJSON[type]).find( - (node: EntryNode) => node.id === rp.args.id - ), - }); - - schema.addResolver({ - name: 'findMany', - type: () => [schema], - description: `Find many ${pluralType} by their IDs`, - args: generateArgsForManyItemQuery(pluralType), - resolve: (rp: Record) => { - const idsToFind = rp.args.ids ?? []; - const matches = - cloneDeep(allContentNodesJSON[type])?.filter((node: EntryNode) => - idsToFind?.includes(node.id) - ) ?? []; - return resolveQueryArgs(matches, rp.args, config, { - type: { - name: type, - pluralName: pluralType, - pluralQueryName: pluralTypeQueryName, - }, - }); - }, - }); - - schema.addResolver({ - name: 'all', - args: generateArgsForAllItemQuery(pluralType), - type: () => [schema], - description: `Return a set of ${pluralType}`, - resolve: (rp: Record) => { - const nodes = cloneDeep(allContentNodesJSON[type]); - return resolveQueryArgs(nodes, rp.args, config, { - type: { - name: type, - pluralName: pluralType, - pluralQueryName: pluralTypeQueryName, - }, - }); - }, + addCollectionQueries({ + name, + pluralName, + objectComposer, + schemaComposer, + allContentNodesJSON, + config, }); - schemaComposer.Query.addFields({ - /** - * Add find by ID to each content type - */ - [type]: schema.getResolver('findById'), - /** - * Add find 'many' to each content type - */ - [pluralTypeQueryName]: schema.getResolver('all'), + addCollectionMutations({ + name, + pluralName, + objectComposer, + schemaComposer, + allContentNodesJSON, + config, }); - - /** - * Separate the queries by return type for later use when wrapping the query resolvers - * @todo potentially able to remove this - **/ - queries.maybeReturnsSingleItem.push(type); - queries.maybeReturnsList.push(pluralTypeQueryName); } // Create map of references on each content node diff --git a/packages/transformer-yaml/src/index.ts b/packages/transformer-yaml/src/index.ts index 859d8888..11cd1dc1 100644 --- a/packages/transformer-yaml/src/index.ts +++ b/packages/transformer-yaml/src/index.ts @@ -2,7 +2,7 @@ import yaml from 'js-yaml'; import type { YAMLException } from 'js-yaml'; import slugify from '@sindresorhus/slugify'; import type { EntryNode, TransformerPlugin } from '@flatbread/core'; -import type { VFile } from 'vfile'; +import { VFile } from 'vfile'; /** * Transforms a yaml file (content node) to JSON. @@ -32,15 +32,21 @@ export const parse = (input: VFile): EntryNode => { ); }; +function serialize(node: EntryNode): VFile { + const doc = yaml.dump(node); + return new VFile(doc); +} + /** - * Converts markdown files to meaningful data. + * Converts yaml files to meaningful data. * - * @returns Markdown parser, preknown GraphQL schema fragments, and an EntryNode inspector function. + * @returns yaml parser, preknown GraphQL schema fragments, and an EntryNode inspector function. */ const transformer: TransformerPlugin = () => { return { parse: (input: VFile): EntryNode => parse(input), inspect: (input: EntryNode) => String(input), + serialize, extensions: ['.yaml', '.yml'], }; }; From 9339af9d25a334787c846f3bb0e3ade849749ad5 Mon Sep 17 00:00:00 2001 From: Adam Sparks Date: Thu, 4 Aug 2022 01:28:54 -0500 Subject: [PATCH 02/22] refactor collection metadata --- packages/core/src/generators/schema.ts | 22 +++++++---- packages/core/src/types.ts | 15 ++++---- packages/core/src/utils/initializeConfig.ts | 7 ++++ packages/source-filesystem/package.json | 1 + packages/source-filesystem/src/index.ts | 36 ++++++++++++------ packages/transformer-markdown/package.json | 1 - packages/transformer-markdown/src/index.ts | 8 +--- packages/transformer-yaml/package.json | 1 - packages/transformer-yaml/src/index.ts | 8 +--- .../src/tests/snapshots/index.test.ts.md | 3 -- .../src/tests/snapshots/index.test.ts.snap | Bin 539 -> 511 bytes playground/src/routes/index.svelte | 8 ++-- pnpm-lock.yaml | 18 ++++----- 13 files changed, 73 insertions(+), 55 deletions(-) diff --git a/packages/core/src/generators/schema.ts b/packages/core/src/generators/schema.ts index 2bf98494..5fb11ddb 100644 --- a/packages/core/src/generators/schema.ts +++ b/packages/core/src/generators/schema.ts @@ -1,6 +1,6 @@ import { schemaComposer } from 'graphql-compose'; import { composeWithJson } from 'graphql-compose-json'; -import { defaultsDeep, merge } from 'lodash-es'; +import { defaultsDeep, get, merge } from 'lodash-es'; import plur from 'plur'; import { VFile } from 'vfile'; @@ -59,7 +59,17 @@ export async function generateSchema( defaultsDeep( {}, getFieldOverrides(collection, config), - ...nodes.map((node) => merge({}, node, preknownSchemaFragments)) + ...nodes.map((node) => + merge( + { + _flatbread: { + reference: get(node, node?._flatbread?.referenceField), + }, + }, + node, + preknownSchemaFragments + ) + ) ), { schemaComposer } ), @@ -170,11 +180,9 @@ const fetchPreknownSchemaFragments = ( function getTransformerExtensionMap(transformer: Transformer[]) { const transformerMap = new Map(); - transformer.forEach((t) => { - t.extensions.forEach((extension) => { - transformerMap.set(extension, t); - }); - }); + transformer.forEach((t) => + t.extensions.forEach((extension) => transformerMap.set(extension, t)) + ); return transformerMap; } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 82645f8f..43f74df3 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -23,13 +23,13 @@ export type ContentNode = BaseContentNode & { export interface FlatbreadConfig { source: Source; transformer?: Transformer | Transformer[]; - content: Content; + content: Partial[]; } export interface LoadedFlatbreadConfig { source: Source; transformer: Transformer[]; - content: Content; + content: CollectionEntry[]; loaded: { extensions: string[]; }; @@ -69,9 +69,8 @@ export type EntryNode = Record; */ export interface Source { initialize?: (flatbreadConfig: LoadedFlatbreadConfig) => void; - fetchByType?: (path: string) => Promise; fetch: ( - allContentTypes: Record[] + allContentTypes: CollectionEntry[] ) => Promise>; } @@ -93,12 +92,14 @@ export interface Override { } /** - * An array of content descriptions which can be used to retrieve content nodes. + * A collection entry which can be used to retrieve content nodes. * * This is paired with a `Source` (and, *optionally*, a `Transformer`) plugin. */ -export type Content = { +export interface CollectionEntry { collection: string; overrides?: Override[]; + referenceField: string; + [key: string]: any; -}[]; +} diff --git a/packages/core/src/utils/initializeConfig.ts b/packages/core/src/utils/initializeConfig.ts index c8540aed..bea2bd98 100644 --- a/packages/core/src/utils/initializeConfig.ts +++ b/packages/core/src/utils/initializeConfig.ts @@ -1,3 +1,5 @@ +import { defaultsDeep } from 'lodash-es'; +import { CollectionEntry } from '../../dist'; import { LoadedFlatbreadConfig, Transformer } from '../types'; export function initializeConfig(config: any): LoadedFlatbreadConfig { @@ -10,5 +12,10 @@ export function initializeConfig(config: any): LoadedFlatbreadConfig { .map((transformer: Transformer) => transformer.extensions || []) .flat(), }; + + config.content = config.content?.map((content: CollectionEntry) => + defaultsDeep(content, { referenceField: 'id' }) + ); + return config; } diff --git a/packages/source-filesystem/package.json b/packages/source-filesystem/package.json index 58b8031a..4c5c7a1f 100644 --- a/packages/source-filesystem/package.json +++ b/packages/source-filesystem/package.json @@ -41,6 +41,7 @@ }, "devDependencies": { "@flatbread/core": "workspace:*", + "@sindresorhus/slugify": "^2.1.0", "@types/lodash-es": "4.17.6", "@types/node": "16.11.47", "tsup": "6.2.1", diff --git a/packages/source-filesystem/src/index.ts b/packages/source-filesystem/src/index.ts index 34a67f4c..0127aef0 100644 --- a/packages/source-filesystem/src/index.ts +++ b/packages/source-filesystem/src/index.ts @@ -1,7 +1,12 @@ -import { defaultsDeep } from 'lodash-es'; +import slugify from '@sindresorhus/slugify'; +import { defaultsDeep, merge } from 'lodash-es'; import { read } from 'to-vfile'; -import type { LoadedFlatbreadConfig, SourcePlugin } from '@flatbread/core'; +import type { + CollectionEntry, + LoadedFlatbreadConfig, + SourcePlugin, +} from '@flatbread/core'; import type { VFile } from 'vfile'; import type { FileNode, @@ -18,16 +23,27 @@ import gatherFileNodes from './utils/gatherFileNodes'; * @returns An array of content nodes */ async function getNodesFromDirectory( - path: string, + collectionEntry: CollectionEntry, config: InitializedSourceFilesystemConfig ): Promise { const { extensions } = config; - const nodes: FileNode[] = await gatherFileNodes(path, { extensions }); + const nodes: FileNode[] = await gatherFileNodes(collectionEntry.path, { + extensions, + }); return Promise.all( nodes.map(async (node: FileNode): Promise => { const file = await read(node.path); - file.data = node.data; + file.data = merge(node.data, { + _flatbread: { + referenceField: collectionEntry.referenceField, + collection: collectionEntry.collection, + filename: file.basename, + path: file.path, + slug: slugify(file.stem ?? ''), + }, + }); + return file; }) ); @@ -40,16 +56,16 @@ async function getNodesFromDirectory( * @returns */ async function getAllNodes( - allContentTypes: Record[], + allCollectionEntries: CollectionEntry[], config: InitializedSourceFilesystemConfig ): Promise> { const nodeEntries = await Promise.all( - allContentTypes.map( + allCollectionEntries.map( async (contentType): Promise> => new Promise(async (res) => res([ contentType.collection, - await getNodesFromDirectory(contentType.path, config), + await getNodesFromDirectory(contentType, config), ]) ) ) @@ -76,9 +92,7 @@ const source: SourcePlugin = (sourceConfig?: sourceFilesystemConfig) => { const { extensions } = flatbreadConfig.loaded; config = defaultsDeep(sourceConfig ?? {}, { extensions }); }, - fetchByType: (path: string) => getNodesFromDirectory(path, config), - fetch: (allContentTypes: Record[]) => - getAllNodes(allContentTypes, config), + fetch: (content: CollectionEntry[]) => getAllNodes(content, config), }; }; diff --git a/packages/transformer-markdown/package.json b/packages/transformer-markdown/package.json index 7cbbfd2c..995dc85a 100644 --- a/packages/transformer-markdown/package.json +++ b/packages/transformer-markdown/package.json @@ -35,7 +35,6 @@ "node": "^14.13.1 || >=16.0.0" }, "dependencies": { - "@sindresorhus/slugify": "^2.1.0", "graphql": "16.5.0", "gray-matter": "^4.0.3", "lodash-es": "^4.17.21", diff --git a/packages/transformer-markdown/src/index.ts b/packages/transformer-markdown/src/index.ts index e37ac9b9..03c9cbf9 100644 --- a/packages/transformer-markdown/src/index.ts +++ b/packages/transformer-markdown/src/index.ts @@ -1,10 +1,9 @@ import matter from 'gray-matter'; -import slugify from '@sindresorhus/slugify'; -import { html, excerpt, timeToRead } from './graphql/schema-helpers'; +import { excerpt, html, timeToRead } from './graphql/schema-helpers'; -import type { MarkdownTransformerConfig } from './types'; import type { EntryNode, TransformerPlugin } from '@flatbread/core'; import type { VFile } from 'vfile'; +import type { MarkdownTransformerConfig } from './types'; export * from './types'; @@ -20,9 +19,6 @@ export const parse = ( ): EntryNode => { const { data, content } = matter(String(input), config.grayMatter); return { - _filename: input.basename, - _path: input.path, - _slug: slugify(input.stem ?? ''), ...input.data, ...data, _content: { diff --git a/packages/transformer-yaml/package.json b/packages/transformer-yaml/package.json index 28a1dbb0..e9d5c70b 100644 --- a/packages/transformer-yaml/package.json +++ b/packages/transformer-yaml/package.json @@ -35,7 +35,6 @@ "node": "^14.13.1 || >=16.0.0" }, "dependencies": { - "@sindresorhus/slugify": "^2.1.0", "js-yaml": "^4.1.0" }, "devDependencies": { diff --git a/packages/transformer-yaml/src/index.ts b/packages/transformer-yaml/src/index.ts index 11cd1dc1..5d998d3f 100644 --- a/packages/transformer-yaml/src/index.ts +++ b/packages/transformer-yaml/src/index.ts @@ -1,7 +1,6 @@ -import yaml from 'js-yaml'; -import type { YAMLException } from 'js-yaml'; -import slugify from '@sindresorhus/slugify'; import type { EntryNode, TransformerPlugin } from '@flatbread/core'; +import type { YAMLException } from 'js-yaml'; +import yaml from 'js-yaml'; import { VFile } from 'vfile'; /** @@ -18,9 +17,6 @@ export const parse = (input: VFile): EntryNode => { if (typeof doc === 'object') { return { - _filename: input.basename, - _path: input.path, - _slug: slugify(input.stem ?? ''), ...input.data, ...doc, }; diff --git a/packages/transformer-yaml/src/tests/snapshots/index.test.ts.md b/packages/transformer-yaml/src/tests/snapshots/index.test.ts.md index 4174cf44..ee331630 100644 --- a/packages/transformer-yaml/src/tests/snapshots/index.test.ts.md +++ b/packages/transformer-yaml/src/tests/snapshots/index.test.ts.md @@ -9,9 +9,6 @@ Generated by [AVA](https://avajs.dev). > Snapshot 1 { - _filename: undefined, - _path: undefined, - _slug: '', date_joined: Date 2021-02-25 16:41:59 558ms UTC {}, enjoys: [ 'cats', diff --git a/packages/transformer-yaml/src/tests/snapshots/index.test.ts.snap b/packages/transformer-yaml/src/tests/snapshots/index.test.ts.snap index 8de1e400eadd826f0a4a14048d10672ce39d8d11..b68787a83d21e4d8818404a52cbd67211bfffa73 100644 GIT binary patch literal 511 zcmVKas?O6;o zWxIBR79WcU00000000ABQZaASP!N7+J4tD2lN6+Os{999C2m8Z8;F68rArwaUF?_k z68qWsUZiGV=*C}Qf->;~*x8UO^(QbeG4TVK5EnOgsV#l-y}R$-clX&><58T3Q@;GH zE!kLK?2z?CG=7FTYajg)GKKyelh_|qtceX~e4bjqyow^Uc;b9_RZA@%Sb^3HdA|~9 z!8^ex!8gHA!6RsPq4|0=(V+#X!cM27U|m5|!3Mw|AwC9IL8A+6BA|zpl#GswVju2` zO>HLWoE5aCu~TKoIcXD>NAIA=`!$L~cjW;0b*=0fOmuZPPR+cKKK);srxeE`y(Jgj zP8YU=UeMp|1-rrExc}tnpno)Ywl^3&eN(LLy_7Y;Sq8O zEx{?7GvA&Qm*41~Sr10JMyZ;!BIC8dJKXCX)>ti39A;>5InIwpy^Fpew%UZ1@2gD7 zEJjPIsc|=BdOM?N7^a42Nw#3zwuPv!%4BUdnRBD+zY1q*Zh3k8?>`HjV`~Zn0014c B@>>7^ literal 539 zcmV+$0_6QcRzVVR@Qmm zzR(X$OCO5}00000000AZQq6ADKoH)s*LIr{k^mBLK|XLpjS{z^&>Iq*kU&VefP|ur zy=gXCuZ?#jnq%L96P(MPr|1pw2%Na{3Ke75Hf1F&ee&#l-_GxzFXl3f*ZS-$H^NAM z`aqa4W-82Cp?S!{InyEzw=9+6LP##OV!2*sMxUK02{Y`i`^Y5$DL>eO&MW*+5eD%I z@eT1E@eAPrArBER5Vye}fd6{F;;{i}&3?a6pi5wvKo2#}9dHSF1K6FF$rVd^%mjC0l+?>=Wtnwznd#s?L_pl2-TV+D=q#{3GBYmz`*;ZQ-{Y4?TN}3}*9O(l~ z;)uU=#16Pc)3%lJFwNAqN_}xXwO$J;G4)-%==KM2FB(PT{ZX_ZO{U|=$A{zN$=zP=16.9'} hasBin: true requiresBuild: true @@ -5425,7 +5423,7 @@ packages: /lodash.deburr/4.1.0: resolution: {integrity: sha512-m/M1U1f3ddMCs6Hq2tAsYThTBDaAKFDX3dwDo97GEYzamXi9SqUpjWi/Rrj/gf3X2n8ktwgZrlP1z6E3v/IExQ==} - dev: false + dev: true /lodash.merge/4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} From 941031053b46ff2cd8adc9ed1aa5e57575af1e3a Mon Sep 17 00:00:00 2001 From: Adam Sparks Date: Thu, 4 Aug 2022 22:50:41 -0500 Subject: [PATCH 03/22] add more metadata to collection --- examples/sveltekit/src/routes/index.svelte | 10 ++++++---- packages/core/src/generators/schema.ts | 18 ++++++------------ packages/core/src/types.ts | 2 ++ packages/core/src/utils/initializeConfig.ts | 7 ++++++- packages/source-filesystem/src/index.ts | 6 ++++-- packages/transformer-markdown/src/index.ts | 2 ++ packages/transformer-yaml/src/index.ts | 2 ++ 7 files changed, 28 insertions(+), 19 deletions(-) diff --git a/examples/sveltekit/src/routes/index.svelte b/examples/sveltekit/src/routes/index.svelte index e9f7bdcb..2d894932 100644 --- a/examples/sveltekit/src/routes/index.svelte +++ b/examples/sveltekit/src/routes/index.svelte @@ -4,9 +4,9 @@ query PostCategory { allPostCategories (sortBy: "title", order: DESC) { _flatbread { - _filename - _collection - _slug + filename + collection + slug } id title @@ -20,7 +20,9 @@ timeToRead } authors { - _slug + _flatbread { + slug + } id name entity diff --git a/packages/core/src/generators/schema.ts b/packages/core/src/generators/schema.ts index 5fb11ddb..57014ad3 100644 --- a/packages/core/src/generators/schema.ts +++ b/packages/core/src/generators/schema.ts @@ -59,17 +59,7 @@ export async function generateSchema( defaultsDeep( {}, getFieldOverrides(collection, config), - ...nodes.map((node) => - merge( - { - _flatbread: { - reference: get(node, node?._flatbread?.referenceField), - }, - }, - node, - preknownSchemaFragments - ) - ) + ...nodes.map((node) => merge({}, node, preknownSchemaFragments)) ), { schemaComposer } ), @@ -213,7 +203,11 @@ const optionallyTransformContentNodes = ( if (!transformer?.parse) { throw new Error(`no transformer found for ${node.path}`); } - return transformer.parse(node); + console.log({ transformer }); + const doc = transformer.parse(node); + doc._flatbread.transformedBy = transformer.id; + doc._flatbread.reference = get(doc, doc._flatbread.referenceField); + return doc; }); } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 43f74df3..7eab39fe 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -51,6 +51,7 @@ export interface Transformer { * @param input Node to transform */ parse?: (input: VFile) => EntryNode; + id?: string; preknownSchemaFragments?: () => Record; inspect: (input: EntryNode) => string; extensions: string[]; @@ -69,6 +70,7 @@ export type EntryNode = Record; */ export interface Source { initialize?: (flatbreadConfig: LoadedFlatbreadConfig) => void; + id?: string; fetch: ( allContentTypes: CollectionEntry[] ) => Promise>; diff --git a/packages/core/src/utils/initializeConfig.ts b/packages/core/src/utils/initializeConfig.ts index bf46b09e..de9ea718 100644 --- a/packages/core/src/utils/initializeConfig.ts +++ b/packages/core/src/utils/initializeConfig.ts @@ -2,6 +2,8 @@ import { cloneDeep, defaultsDeep } from 'lodash-es'; import { CollectionEntry } from '../types'; import { FlatbreadConfig, LoadedFlatbreadConfig, Transformer } from '../types'; import { toArray } from './arrayUtils'; +import { createHash } from 'crypto'; +import { anyToString } from './stringUtils'; /** * Processes a config object and returns a normalized version of it. @@ -10,7 +12,10 @@ export function initializeConfig( rawConfig: FlatbreadConfig ): LoadedFlatbreadConfig { const config = cloneDeep(rawConfig); - const transformer = toArray(config.transformer ?? []); + const transformer = toArray(config.transformer ?? []).map((t) => { + t.id = t.id ?? createHash('sha256').update(anyToString(t)).digest('hex'); + return t; + }); return { ...config, diff --git a/packages/source-filesystem/src/index.ts b/packages/source-filesystem/src/index.ts index 838d4e28..b167fc1a 100644 --- a/packages/source-filesystem/src/index.ts +++ b/packages/source-filesystem/src/index.ts @@ -1,12 +1,13 @@ import slugify from '@sindresorhus/slugify'; import { defaultsDeep, merge } from 'lodash-es'; import { read } from 'to-vfile'; - +import ownPackage from '../package.json'; import type { CollectionEntry, LoadedFlatbreadConfig, SourcePlugin, } from '@flatbread/core'; +import { relative } from 'path'; import type { VFile } from 'vfile'; import type { FileNode, @@ -39,8 +40,9 @@ async function getNodesFromDirectory( referenceField: collectionEntry.referenceField, collection: collectionEntry.collection, filename: file.basename, - path: file.path, + path: relative(process.cwd(), file.path), slug: slugify(file.stem ?? ''), + sourcedBy: ownPackage.name, }, }); diff --git a/packages/transformer-markdown/src/index.ts b/packages/transformer-markdown/src/index.ts index 590a3a7d..8db226a6 100644 --- a/packages/transformer-markdown/src/index.ts +++ b/packages/transformer-markdown/src/index.ts @@ -1,5 +1,6 @@ import matter from 'gray-matter'; import { excerpt, html, timeToRead } from './graphql/schema-helpers'; +import ownPackage from '../package.json'; import type { EntryNode, TransformerPlugin } from '@flatbread/core'; import type { VFile } from 'vfile'; @@ -41,6 +42,7 @@ export const transformer: TransformerPlugin = ( ); return { parse: (input: VFile): EntryNode => parse(input, config), + id: ownPackage.name, preknownSchemaFragments: () => ({ _content: { html: html(config), diff --git a/packages/transformer-yaml/src/index.ts b/packages/transformer-yaml/src/index.ts index 2ad8c1b0..4e897e5f 100644 --- a/packages/transformer-yaml/src/index.ts +++ b/packages/transformer-yaml/src/index.ts @@ -2,6 +2,7 @@ import type { EntryNode, TransformerPlugin } from '@flatbread/core'; import type { YAMLException } from 'js-yaml'; import yaml from 'js-yaml'; import { VFile } from 'vfile'; +import ownPackage from '../package.json'; /** * Transforms a yaml file (content node) to JSON. @@ -42,6 +43,7 @@ export const transformer: TransformerPlugin = () => { return { parse: (input: VFile): EntryNode => parse(input), inspect: (input: EntryNode) => String(input), + id: ownPackage.name, serialize, extensions: ['.yaml', '.yml'], }; From df774c0a36fe1582f6b316d35252a4f897713200 Mon Sep 17 00:00:00 2001 From: Adam Sparks Date: Thu, 4 Aug 2022 22:57:11 -0500 Subject: [PATCH 04/22] add json assertsions --- packages/core/src/generators/schema.ts | 1 - packages/source-filesystem/src/index.ts | 2 +- packages/transformer-markdown/src/index.ts | 2 +- packages/transformer-yaml/src/index.ts | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/core/src/generators/schema.ts b/packages/core/src/generators/schema.ts index 57014ad3..7d5ccdb1 100644 --- a/packages/core/src/generators/schema.ts +++ b/packages/core/src/generators/schema.ts @@ -203,7 +203,6 @@ const optionallyTransformContentNodes = ( if (!transformer?.parse) { throw new Error(`no transformer found for ${node.path}`); } - console.log({ transformer }); const doc = transformer.parse(node); doc._flatbread.transformedBy = transformer.id; doc._flatbread.reference = get(doc, doc._flatbread.referenceField); diff --git a/packages/source-filesystem/src/index.ts b/packages/source-filesystem/src/index.ts index b167fc1a..1e092a5f 100644 --- a/packages/source-filesystem/src/index.ts +++ b/packages/source-filesystem/src/index.ts @@ -1,7 +1,7 @@ import slugify from '@sindresorhus/slugify'; import { defaultsDeep, merge } from 'lodash-es'; import { read } from 'to-vfile'; -import ownPackage from '../package.json'; +import ownPackage from '../package.json' assert { type: 'json' }; import type { CollectionEntry, LoadedFlatbreadConfig, diff --git a/packages/transformer-markdown/src/index.ts b/packages/transformer-markdown/src/index.ts index 8db226a6..c81f8fe6 100644 --- a/packages/transformer-markdown/src/index.ts +++ b/packages/transformer-markdown/src/index.ts @@ -1,6 +1,6 @@ import matter from 'gray-matter'; import { excerpt, html, timeToRead } from './graphql/schema-helpers'; -import ownPackage from '../package.json'; +import ownPackage from '../package.json' assert { type: 'json' }; import type { EntryNode, TransformerPlugin } from '@flatbread/core'; import type { VFile } from 'vfile'; diff --git a/packages/transformer-yaml/src/index.ts b/packages/transformer-yaml/src/index.ts index 4e897e5f..570637d8 100644 --- a/packages/transformer-yaml/src/index.ts +++ b/packages/transformer-yaml/src/index.ts @@ -2,7 +2,7 @@ import type { EntryNode, TransformerPlugin } from '@flatbread/core'; import type { YAMLException } from 'js-yaml'; import yaml from 'js-yaml'; import { VFile } from 'vfile'; -import ownPackage from '../package.json'; +import ownPackage from '../package.json' assert { type: 'json' }; /** * Transforms a yaml file (content node) to JSON. From fdefe159644912433f53411529ed3936d07ffb78 Mon Sep 17 00:00:00 2001 From: Adam Sparks Date: Fri, 5 Aug 2022 23:50:29 -0500 Subject: [PATCH 05/22] update mutations update file and memory :D, breaks ava watch --- .../src/generators/collectionMutations.ts | 34 ++++++++++++---- .../core/src/generators/collectionQueries.ts | 3 +- packages/core/src/generators/schema.ts | 40 +++++++++++++++++-- packages/core/src/providers/test/base.test.ts | 35 ++++++++++++++++ packages/core/src/types.ts | 13 ++++++ packages/flatbread/content/authors/me.md | 2 +- packages/source-filesystem/src/index.ts | 17 +++++++- packages/transformer-markdown/src/index.ts | 21 +++++++++- packages/transformer-yaml/src/index.ts | 8 +++- 9 files changed, 155 insertions(+), 18 deletions(-) diff --git a/packages/core/src/generators/collectionMutations.ts b/packages/core/src/generators/collectionMutations.ts index a20ac161..f320b629 100644 --- a/packages/core/src/generators/collectionMutations.ts +++ b/packages/core/src/generators/collectionMutations.ts @@ -1,5 +1,11 @@ import { ObjectTypeComposer, SchemaComposer } from 'graphql-compose'; -import { LoadedFlatbreadConfig } from '../types'; +import { merge } from 'lodash-es'; +import { + CollectionContext, + EntryNode, + LoadedFlatbreadConfig, + Transformer, +} from '../types'; export interface AddCollectionMutationsArgs { name: string; @@ -7,7 +13,9 @@ export interface AddCollectionMutationsArgs { config: LoadedFlatbreadConfig; objectComposer: ObjectTypeComposer; schemaComposer: SchemaComposer; - allContentNodesJSON: Record; + updateCollectionRecord: ( + entry: EntryNode & { _flatbread: CollectionContext } + ) => Promise; } export default function addCollectionMutations( @@ -19,17 +27,29 @@ export default function addCollectionMutations( config, objectComposer, schemaComposer, - allContentNodesJSON, + updateCollectionRecord, } = args; schemaComposer.Mutation.addFields({ - [`upsert${name}`]: { + [`update${name}`]: { type: objectComposer, args: { [name]: objectComposer.getInputType() }, description: `Update or create a ${name}`, - async resolve(_, payload) { - console.dir({ payload }, { depth: Infinity }); - return payload; + async resolve(source, payload) { + // remove _flatbread to prevent injection + const { _flatbread, ...update } = source.author; + + const targetRecord = objectComposer + .getResolver('findById') + .resolve({ args: update }); + + // remove supplied key (might not be required) + delete update[targetRecord._flatbread.referenceField]; + const newRecord = merge(targetRecord, update); + + await updateCollectionRecord(newRecord); + + return newRecord; }, }, }); diff --git a/packages/core/src/generators/collectionQueries.ts b/packages/core/src/generators/collectionQueries.ts index 98ddc1ce..7fc94bd0 100644 --- a/packages/core/src/generators/collectionQueries.ts +++ b/packages/core/src/generators/collectionQueries.ts @@ -7,7 +7,7 @@ import { generateArgsForSingleItemQuery, } from '../generators/arguments'; import { cloneDeep } from 'lodash-es'; -import { EntryNode, LoadedFlatbreadConfig } from '../types'; +import { EntryNode, LoadedFlatbreadConfig, Transformer } from '../types'; export interface AddCollectionQueriesArgs { name: string; @@ -16,6 +16,7 @@ export interface AddCollectionQueriesArgs { objectComposer: ObjectTypeComposer; schemaComposer: SchemaComposer; allContentNodesJSON: Record; + transformersById: Record; } export default function addCollectionQueries(args: AddCollectionQueriesArgs) { diff --git a/packages/core/src/generators/schema.ts b/packages/core/src/generators/schema.ts index 7d5ccdb1..61a582b4 100644 --- a/packages/core/src/generators/schema.ts +++ b/packages/core/src/generators/schema.ts @@ -1,11 +1,17 @@ import { schemaComposer } from 'graphql-compose'; import { composeWithJson } from 'graphql-compose-json'; -import { defaultsDeep, get, merge } from 'lodash-es'; +import { defaultsDeep, get, keyBy, merge } from 'lodash-es'; import plur from 'plur'; import { VFile } from 'vfile'; import { cacheSchema, checkCacheForSchema } from '../cache/cache'; -import { ConfigResult, LoadedFlatbreadConfig, Transformer } from '../types'; +import { + CollectionContext, + ConfigResult, + EntryNode, + LoadedFlatbreadConfig, + Transformer, +} from '../types'; import { getFieldOverrides } from '../utils/fieldOverrides'; import { map } from '../utils/map'; import addCollectionMutations from './collectionMutations'; @@ -66,6 +72,33 @@ export async function generateSchema( ]) ); + const transformersById = { + ...Object.fromEntries( + config.transformer.map((transformer) => [transformer.id, transformer]) + ), + // this will be the default for collections that aren't already `transformedBy` anything + undefined: config.transformer[0], + }; + + async function updateCollectionRecord( + entry: EntryNode & { _flatbread: CollectionContext } + ) { + const { _flatbread: ctx, ...record } = entry; + const file = await transformersById[ctx.transformedBy].serialize( + record, + ctx + ); + + await config?.source.put(file, ctx); + const index = allContentNodesJSON[ctx.collection].findIndex( + (c) => get(c, ctx.referenceField) === ctx.reference + ); + + // replace in memory representation of record + allContentNodesJSON[ctx.collection][index] = entry; + return entry; + } + // Main builder loop - iterate through each content type and generate query resolvers + relationships for it for (const [name, objectComposer] of Object.entries(schemaArray)) { const pluralName = plur(name, 2); @@ -91,6 +124,7 @@ export async function generateSchema( pluralName, objectComposer, schemaComposer, + transformersById, allContentNodesJSON, config, }); @@ -100,7 +134,7 @@ export async function generateSchema( pluralName, objectComposer, schemaComposer, - allContentNodesJSON, + updateCollectionRecord, config, }); } diff --git a/packages/core/src/providers/test/base.test.ts b/packages/core/src/providers/test/base.test.ts index f003d247..dc267fd7 100644 --- a/packages/core/src/providers/test/base.test.ts +++ b/packages/core/src/providers/test/base.test.ts @@ -58,3 +58,38 @@ test('relational filter query', async (t) => { t.snapshot(result); }); + +test('update collection record', async (t) => { + const flatbread = basicProject(); + const sitting = (Math.random() * 100) | 0; + const result: any = await flatbread.query({ + rootValue: { author: { id: '2a3e', skills: { sitting } } }, + source: ` + mutation UpdateAuthor($author: AuthorInput){ + updateAuthor(Author: $author) { + id + skills { + sitting + } + } + } + `, + }); + + t.is(result.data.updateAuthor.skills.sitting, sitting); + + const updated: any = await flatbread.query({ + source: ` + query { + Author(id: "2a3e") { + id + skills { + sitting + } + } + } + `, + }); + + t.is(updated.data.Author.skills.sitting, sitting); +}); diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 7eab39fe..ebf3aa9f 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -54,6 +54,7 @@ export interface Transformer { id?: string; preknownSchemaFragments?: () => Record; inspect: (input: EntryNode) => string; + serialize: (input: EntryNode, ctx: CollectionContext) => VFile; extensions: string[]; } @@ -71,6 +72,7 @@ export type EntryNode = Record; export interface Source { initialize?: (flatbreadConfig: LoadedFlatbreadConfig) => void; id?: string; + put: (source: VFile, ctx: CollectionContext) => Promise; fetch: ( allContentTypes: CollectionEntry[] ) => Promise>; @@ -93,6 +95,17 @@ export interface Override { ) => any; } +export interface CollectionContext { + referenceField: string; + collection: string; + filename: string; + path: string; + slug: string; + sourcedBy: string; + transformedBy: string; + reference: string; +} + /** * A collection entry which can be used to retrieve content nodes. * diff --git a/packages/flatbread/content/authors/me.md b/packages/flatbread/content/authors/me.md index 408dc67d..14448c1b 100644 --- a/packages/flatbread/content/authors/me.md +++ b/packages/flatbread/content/authors/me.md @@ -8,7 +8,7 @@ enjoys: - making this date_joined: 2021-02-25T16:41:59.558Z skills: - sitting: 204 + sitting: 23 breathing: 7.07 liquid_consumption: 100 existence: simulation diff --git a/packages/source-filesystem/src/index.ts b/packages/source-filesystem/src/index.ts index 1e092a5f..d5732c97 100644 --- a/packages/source-filesystem/src/index.ts +++ b/packages/source-filesystem/src/index.ts @@ -1,13 +1,14 @@ import slugify from '@sindresorhus/slugify'; import { defaultsDeep, merge } from 'lodash-es'; -import { read } from 'to-vfile'; +import { read, write } from 'to-vfile'; import ownPackage from '../package.json' assert { type: 'json' }; import type { + CollectionContext, CollectionEntry, LoadedFlatbreadConfig, SourcePlugin, } from '@flatbread/core'; -import { relative } from 'path'; +import { relative, resolve } from 'path'; import type { VFile } from 'vfile'; import type { FileNode, @@ -80,6 +81,17 @@ async function getAllNodes( return nodes; } +// TODO: _flatbread data should be extracted from plugins +// plugin should return a context object and be given the same context object back when saving, +// this context object will be saved internally under _flatbread[collectionId] + +async function put(source: VFile, ctx: CollectionContext) { + (source.basename = ctx.filename), + (source.path = resolve(process.cwd(), ctx.path)); + + await write(source); +} + /** * Source filesystem plugin for fetching flat-file content nodes from directories on disk. * @@ -95,6 +107,7 @@ export const source: SourcePlugin = (sourceConfig?: sourceFilesystemConfig) => { config = defaultsDeep(sourceConfig ?? {}, { extensions }); }, fetch: (content: CollectionEntry[]) => getAllNodes(content, config), + put, }; }; diff --git a/packages/transformer-markdown/src/index.ts b/packages/transformer-markdown/src/index.ts index c81f8fe6..bfd18cdb 100644 --- a/packages/transformer-markdown/src/index.ts +++ b/packages/transformer-markdown/src/index.ts @@ -2,8 +2,12 @@ import matter from 'gray-matter'; import { excerpt, html, timeToRead } from './graphql/schema-helpers'; import ownPackage from '../package.json' assert { type: 'json' }; -import type { EntryNode, TransformerPlugin } from '@flatbread/core'; -import type { VFile } from 'vfile'; +import type { + CollectionContext, + EntryNode, + TransformerPlugin, +} from '@flatbread/core'; +import { VFile } from 'vfile'; import type { MarkdownTransformerConfig } from './types'; export * from './types'; @@ -28,6 +32,17 @@ export const parse = ( }; }; +function serialize( + data: EntryNode, + ctx: CollectionContext, + config: MarkdownTransformerConfig +) { + const { _content, ...rest } = data; + const doc = matter.stringify(_content.raw, rest, config.grayMatter); + + return new VFile(doc); +} + /** * Converts markdown files to meaningful data. * @@ -51,6 +66,8 @@ export const transformer: TransformerPlugin = ( }, }), inspect: (input: EntryNode) => String(input), + serialize: (input: EntryNode, ctx: CollectionContext) => + serialize(input, ctx, config), extensions, }; }; diff --git a/packages/transformer-yaml/src/index.ts b/packages/transformer-yaml/src/index.ts index 570637d8..39dabeb7 100644 --- a/packages/transformer-yaml/src/index.ts +++ b/packages/transformer-yaml/src/index.ts @@ -1,4 +1,8 @@ -import type { EntryNode, TransformerPlugin } from '@flatbread/core'; +import type { + CollectionContext, + EntryNode, + TransformerPlugin, +} from '@flatbread/core'; import type { YAMLException } from 'js-yaml'; import yaml from 'js-yaml'; import { VFile } from 'vfile'; @@ -29,7 +33,7 @@ export const parse = (input: VFile): EntryNode => { ); }; -function serialize(node: EntryNode): VFile { +function serialize(node: EntryNode, ctx: CollectionContext): VFile { const doc = yaml.dump(node); return new VFile(doc); } From 978b1300d755e346d9c4a9e91cc5f6247b2882d7 Mon Sep 17 00:00:00 2001 From: Adam Sparks Date: Sat, 6 Aug 2022 03:17:29 -0500 Subject: [PATCH 06/22] isolate context based on plugin, major breaking changes --- packages/core/src/generators/schema.ts | 43 +++++++++++--- packages/core/src/types.ts | 26 ++++++--- packages/core/src/utils/createHash.ts | 6 ++ packages/core/src/utils/initializeConfig.ts | 6 +- packages/flatbread/content/authors/me.md | 2 +- packages/source-filesystem/src/index.ts | 57 ++++++++++--------- packages/transformer-markdown/src/index.ts | 10 ++-- packages/transformer-yaml/src/index.ts | 6 +- .../transformer-yaml/src/tests/index.test.ts | 4 +- 9 files changed, 107 insertions(+), 53 deletions(-) create mode 100644 packages/core/src/utils/createHash.ts diff --git a/packages/core/src/generators/schema.ts b/packages/core/src/generators/schema.ts index 61a582b4..aec72f73 100644 --- a/packages/core/src/generators/schema.ts +++ b/packages/core/src/generators/schema.ts @@ -7,11 +7,14 @@ import { VFile } from 'vfile'; import { cacheSchema, checkCacheForSchema } from '../cache/cache'; import { CollectionContext, + CollectionEntry, ConfigResult, EntryNode, LoadedFlatbreadConfig, + Source, Transformer, } from '../types'; +import createHash from '../utils/createHash'; import { getFieldOverrides } from '../utils/fieldOverrides'; import { map } from '../utils/map'; import addCollectionMutations from './collectionMutations'; @@ -41,7 +44,27 @@ export async function generateSchema( config.source.initialize?.(config); // Invoke the content source resolver to retrieve the content nodes - const allContentNodes = await config.source.fetch(config.content); + let allContentNodes: Record = {}; + + const addRecord = + (source: Source) => + (collection: CollectionEntry, record: EntryNode, context: Ctx) => { + allContentNodes[collection.collection] = + allContentNodes[collection.collection] ?? []; + allContentNodes[collection.collection].push({ + record, + context: { + sourceContext: context, + sourcedBy: source.id, + collection: collection.collection, + referenceField: collection.referenceField ?? 'id', + }, + }); + }; + + await config.source.fetch(config.content, { + addRecord: addRecord(config.source), + }); // Transform the content nodes to the expected JSON format if needed const allContentNodesJSON = optionallyTransformContentNodes( @@ -81,15 +104,15 @@ export async function generateSchema( }; async function updateCollectionRecord( - entry: EntryNode & { _flatbread: CollectionContext } + entry: EntryNode & { _flatbread: any } ) { const { _flatbread: ctx, ...record } = entry; const file = await transformersById[ctx.transformedBy].serialize( record, - ctx + ctx.transformContext ); - await config?.source.put(file, ctx); + await config?.source.put(file, ctx.sourceContext); const index = allContentNodesJSON[ctx.collection].findIndex( (c) => get(c, ctx.referenceField) === ctx.reference ); @@ -232,14 +255,16 @@ const optionallyTransformContentNodes = ( * @todo if this becomes a performance bottleneck, consider overloading the source plugin API to accept a transform function so we can avoid mapping through the content nodes twice * */ - return map(allContentNodes, (node: VFile) => { - const transformer = transformerMap.get(node.extname); + return map(allContentNodes, (node: { record: VFile; context: any }) => { + const transformer = transformerMap.get(node.record.extname); if (!transformer?.parse) { - throw new Error(`no transformer found for ${node.path}`); + throw new Error(`no transformer found for ${node.record.path}`); } - const doc = transformer.parse(node); + const { record: doc, context } = transformer.parse(node.record); + doc._flatbread = node.context; doc._flatbread.transformedBy = transformer.id; - doc._flatbread.reference = get(doc, doc._flatbread.referenceField); + doc._flatbread.transformContext = context; + doc._flatbread.reference = get(doc, node.context.referenceField); return doc; }); } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index ebf3aa9f..f4234185 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -21,13 +21,13 @@ export type ContentNode = BaseContentNode & { * @todo This needs to be typed more strictly. */ export interface FlatbreadConfig { - source: Source; + source: Source; transformer?: Transformer | Transformer[]; content: Partial[]; } export interface LoadedFlatbreadConfig { - source: Source; + source: Source; transformer: Transformer[]; content: CollectionEntry[]; loaded: { @@ -69,16 +69,28 @@ export type EntryNode = Record; * The result of an invoked `Source` plugin which contains methods on how to retrieve content nodes in * their raw (if coupled with a `Transformer` plugin) or processed form. */ -export interface Source { + +export interface FlatbreadArgs { + addRecord( + collection: CollectionEntry, + record: EntryNode, + context: Context + ): void; +} + +export interface Source { initialize?: (flatbreadConfig: LoadedFlatbreadConfig) => void; id?: string; - put: (source: VFile, ctx: CollectionContext) => Promise; + put: (source: VFile, ctx: Context) => Promise; fetch: ( - allContentTypes: CollectionEntry[] - ) => Promise>; + allContentTypes: CollectionEntry[], + flatbread: FlatbreadArgs + ) => Promise; } -export type SourcePlugin = (sourceConfig?: Record) => Source; +export type SourcePlugin = ( + sourceConfig?: Record +) => Source; /** * An override can be used to declare a custom resolve for a field in content diff --git a/packages/core/src/utils/createHash.ts b/packages/core/src/utils/createHash.ts new file mode 100644 index 00000000..03190add --- /dev/null +++ b/packages/core/src/utils/createHash.ts @@ -0,0 +1,6 @@ +import { anyToString } from './stringUtils'; +import { createHash as createHashRaw } from 'crypto'; + +export default function createHash(content: any) { + return createHashRaw('sha256').update(anyToString(content)).digest('hex'); +} diff --git a/packages/core/src/utils/initializeConfig.ts b/packages/core/src/utils/initializeConfig.ts index de9ea718..6deabd29 100644 --- a/packages/core/src/utils/initializeConfig.ts +++ b/packages/core/src/utils/initializeConfig.ts @@ -2,7 +2,7 @@ import { cloneDeep, defaultsDeep } from 'lodash-es'; import { CollectionEntry } from '../types'; import { FlatbreadConfig, LoadedFlatbreadConfig, Transformer } from '../types'; import { toArray } from './arrayUtils'; -import { createHash } from 'crypto'; +import createHash from './createHash'; import { anyToString } from './stringUtils'; /** @@ -13,10 +13,12 @@ export function initializeConfig( ): LoadedFlatbreadConfig { const config = cloneDeep(rawConfig); const transformer = toArray(config.transformer ?? []).map((t) => { - t.id = t.id ?? createHash('sha256').update(anyToString(t)).digest('hex'); + t.id = t.id ?? createHash(t); return t; }); + config.source.id = config.source.id ?? createHash(config.source); + return { ...config, content: config.content?.map((content: Partial) => diff --git a/packages/flatbread/content/authors/me.md b/packages/flatbread/content/authors/me.md index 14448c1b..3bb4e738 100644 --- a/packages/flatbread/content/authors/me.md +++ b/packages/flatbread/content/authors/me.md @@ -8,7 +8,7 @@ enjoys: - making this date_joined: 2021-02-25T16:41:59.558Z skills: - sitting: 23 + sitting: 69 breathing: 7.07 liquid_consumption: 100 existence: simulation diff --git a/packages/source-filesystem/src/index.ts b/packages/source-filesystem/src/index.ts index d5732c97..60696483 100644 --- a/packages/source-filesystem/src/index.ts +++ b/packages/source-filesystem/src/index.ts @@ -5,6 +5,7 @@ import ownPackage from '../package.json' assert { type: 'json' }; import type { CollectionContext, CollectionEntry, + FlatbreadArgs, LoadedFlatbreadConfig, SourcePlugin, } from '@flatbread/core'; @@ -17,6 +18,12 @@ import type { } from './types'; import gatherFileNodes from './utils/gatherFileNodes'; +interface Context { + filename?: string; + path: string; + slug: string; +} + /** * Get nodes (files) from the directory * @@ -26,28 +33,23 @@ import gatherFileNodes from './utils/gatherFileNodes'; */ async function getNodesFromDirectory( collectionEntry: CollectionEntry, + { addRecord }: FlatbreadArgs, config: InitializedSourceFilesystemConfig -): Promise { +): Promise { const { extensions } = config; const nodes: FileNode[] = await gatherFileNodes(collectionEntry.path, { extensions, }); - return Promise.all( - nodes.map(async (node: FileNode): Promise => { - const file = await read(node.path); - file.data = merge(node.data, { - _flatbread: { - referenceField: collectionEntry.referenceField, - collection: collectionEntry.collection, - filename: file.basename, - path: relative(process.cwd(), file.path), - slug: slugify(file.stem ?? ''), - sourcedBy: ownPackage.name, - }, + await Promise.all( + nodes.map(async (node: FileNode): Promise => { + const doc = await read(node.path); + doc.data = node.data; + addRecord(collectionEntry, doc, { + filename: doc.basename, + path: relative(process.cwd(), doc.path), + slug: slugify(doc.stem ?? ''), }); - - return file; }) ); } @@ -60,15 +62,16 @@ async function getNodesFromDirectory( */ async function getAllNodes( allCollectionEntries: CollectionEntry[], + flatbread: FlatbreadArgs, config: InitializedSourceFilesystemConfig -): Promise> { +): Promise { const nodeEntries = await Promise.all( allCollectionEntries.map( async (contentType): Promise> => new Promise(async (res) => res([ contentType.collection, - await getNodesFromDirectory(contentType, config), + await getNodesFromDirectory(contentType, flatbread, config), ]) ) ) @@ -77,19 +80,19 @@ async function getAllNodes( const nodes = Object.fromEntries( nodeEntries as Iterable ); - - return nodes; } // TODO: _flatbread data should be extracted from plugins // plugin should return a context object and be given the same context object back when saving, // this context object will be saved internally under _flatbread[collectionId] -async function put(source: VFile, ctx: CollectionContext) { - (source.basename = ctx.filename), - (source.path = resolve(process.cwd(), ctx.path)); +async function put(doc: VFile, context: Context) { + doc.basename = context.filename; + doc.path = resolve(process.cwd(), context.path); - await write(source); + await write(doc); + + return { doc, context }; } /** @@ -98,7 +101,8 @@ async function put(source: VFile, ctx: CollectionContext) { * @param sourceConfig content types config * @returns A function that returns functions which fetch lists of nodes */ -export const source: SourcePlugin = (sourceConfig?: sourceFilesystemConfig) => { + +export function source(sourceConfig?: sourceFilesystemConfig) { let config: InitializedSourceFilesystemConfig; return { @@ -106,9 +110,10 @@ export const source: SourcePlugin = (sourceConfig?: sourceFilesystemConfig) => { const { extensions } = flatbreadConfig.loaded; config = defaultsDeep(sourceConfig ?? {}, { extensions }); }, - fetch: (content: CollectionEntry[]) => getAllNodes(content, config), + fetch: (content: CollectionEntry[], flatbread: FlatbreadArgs) => + getAllNodes(content, flatbread, config), put, }; -}; +} export default source; diff --git a/packages/transformer-markdown/src/index.ts b/packages/transformer-markdown/src/index.ts index bfd18cdb..77358696 100644 --- a/packages/transformer-markdown/src/index.ts +++ b/packages/transformer-markdown/src/index.ts @@ -24,10 +24,12 @@ export const parse = ( ): EntryNode => { const { data, content } = matter(String(input), config.grayMatter); return { - ...input.data, - ...data, - _content: { - raw: content, + record: { + ...input.data, + ...data, + _content: { + raw: content, + }, }, }; }; diff --git a/packages/transformer-yaml/src/index.ts b/packages/transformer-yaml/src/index.ts index 39dabeb7..b16cf861 100644 --- a/packages/transformer-yaml/src/index.ts +++ b/packages/transformer-yaml/src/index.ts @@ -22,8 +22,10 @@ export const parse = (input: VFile): EntryNode => { if (typeof doc === 'object') { return { - ...input.data, - ...doc, + record: { + ...input.data, + ...doc, + }, }; } throw new Error( diff --git a/packages/transformer-yaml/src/tests/index.test.ts b/packages/transformer-yaml/src/tests/index.test.ts index ba28b068..5246dee3 100644 --- a/packages/transformer-yaml/src/tests/index.test.ts +++ b/packages/transformer-yaml/src/tests/index.test.ts @@ -26,6 +26,6 @@ const transformer = Transformer(); test('it can parse a basic yaml file', async (t) => { const parse = transformer.parse as (input: VFile) => EntryNode; - const node = parse(testFile); - t.snapshot(node); + const { record } = parse(testFile); + t.snapshot(record); }); From b724de139fe634bfb46828239435d2fcf5b17fa8 Mon Sep 17 00:00:00 2001 From: Adam Sparks Date: Sat, 6 Aug 2022 13:05:24 -0500 Subject: [PATCH 07/22] comments --- examples/nextjs/flatbread.config.ts | 4 +- examples/sveltekit/flatbread.config.js | 14 ++-- examples/sveltekit/src/routes/index.svelte | 14 ++-- packages/core/src/cache/cache.ts | 14 +--- .../src/generators/collectionMutations.ts | 8 +-- .../core/src/generators/collectionQueries.ts | 6 +- packages/core/src/generators/schema.ts | 56 ++++++++-------- packages/core/src/providers/test/base.test.ts | 64 +++++++++---------- packages/core/src/types.ts | 19 ++++-- packages/core/src/utils/createHash.ts | 6 -- packages/core/src/utils/createShaHash.ts | 6 ++ packages/core/src/utils/fieldOverrides.ts | 20 +++--- packages/core/src/utils/initializeConfig.ts | 10 +-- .../src/utils/tests/fieldOverrides.test.ts | 5 +- packages/flatbread/content/authors/me.md | 2 +- packages/source-filesystem/src/index.ts | 14 ++-- 16 files changed, 136 insertions(+), 126 deletions(-) delete mode 100644 packages/core/src/utils/createHash.ts create mode 100644 packages/core/src/utils/createShaHash.ts diff --git a/examples/nextjs/flatbread.config.ts b/examples/nextjs/flatbread.config.ts index 3fc7dff0..0d8e76a5 100644 --- a/examples/nextjs/flatbread.config.ts +++ b/examples/nextjs/flatbread.config.ts @@ -17,14 +17,14 @@ const config = { content: [ { path: '../../packages/flatbread/content/posts', - collection: 'Post', + name: 'Post', refs: { authors: 'Author', }, }, { path: '../../packages/flatbread/content/authors', - collection: 'Author', + name: 'Author', refs: { friend: 'Author', }, diff --git a/examples/sveltekit/flatbread.config.js b/examples/sveltekit/flatbread.config.js index 39c65f27..8d9565db 100644 --- a/examples/sveltekit/flatbread.config.js +++ b/examples/sveltekit/flatbread.config.js @@ -19,28 +19,28 @@ export default defineConfig({ content: [ { path: 'content/markdown/posts', - collection: 'Post', + name: 'Post', refs: { authors: 'Author', }, }, { path: 'content/markdown/posts/[category]/[slug].md', - collection: 'PostCategory', + name: 'PostCategory', refs: { authors: 'Author', }, }, { path: 'content/markdown/posts/**/*.md', - collection: 'PostCategoryBlob', + name: 'PostCategoryBlob', refs: { authors: 'Author', }, }, { path: 'content/markdown/authors', - collection: 'Author', + name: 'Author', refs: { friend: 'Author', }, @@ -54,20 +54,18 @@ export default defineConfig({ }, { path: 'content/yaml/authors', - collection: 'YamlAuthor', + name: 'YamlAuthor', refs: { friend: 'YamlAuthor', }, }, { path: 'content/markdown/deeply-nested', - collection: 'OverrideTest', + name: 'OverrideTest', overrides: [ { field: 'deeply.nested', type: 'String', - test: undefined, - test2: null, resolve: (source) => String(source).toUpperCase(), }, { diff --git a/examples/sveltekit/src/routes/index.svelte b/examples/sveltekit/src/routes/index.svelte index 2d894932..53c0ffa9 100644 --- a/examples/sveltekit/src/routes/index.svelte +++ b/examples/sveltekit/src/routes/index.svelte @@ -3,10 +3,12 @@ const query = ` query PostCategory { allPostCategories (sortBy: "title", order: DESC) { - _flatbread { - filename + _metadata { + sourceContext { + filename + slug + } collection - slug } id title @@ -20,8 +22,10 @@ timeToRead } authors { - _flatbread { - slug + _metadata { + sourceContext { + slug + } } id name diff --git a/packages/core/src/cache/cache.ts b/packages/core/src/cache/cache.ts index 6d40d382..fe8fc54c 100644 --- a/packages/core/src/cache/cache.ts +++ b/packages/core/src/cache/cache.ts @@ -1,8 +1,7 @@ import { GraphQLSchema } from 'graphql'; import LRU from 'lru-cache'; -import { createHash } from 'node:crypto'; import { LoadedFlatbreadConfig } from '../types'; -import { anyToString } from '../utils/stringUtils'; +import createShaHash from '../utils/createShaHash'; type SchemaCacheKey = string; @@ -29,7 +28,7 @@ export function cacheSchema( config: LoadedFlatbreadConfig, schema: GraphQLSchema ) { - const schemaHashKey = getSchemaHash(config); + const schemaHashKey = createShaHash(config); cache.schema.set(schemaHashKey, schema); } @@ -39,13 +38,6 @@ export function cacheSchema( export function checkCacheForSchema( config: LoadedFlatbreadConfig ): GraphQLSchema | undefined { - const schemaHashKey = getSchemaHash(config); + const schemaHashKey = createShaHash(config); return cache.schema.get(schemaHashKey); } - -/** - * Generates a hash key for a given Flatbread config. - */ -export function getSchemaHash(config: LoadedFlatbreadConfig) { - return createHash('md5').update(anyToString(config)).digest('hex'); -} diff --git a/packages/core/src/generators/collectionMutations.ts b/packages/core/src/generators/collectionMutations.ts index f320b629..c3e4421a 100644 --- a/packages/core/src/generators/collectionMutations.ts +++ b/packages/core/src/generators/collectionMutations.ts @@ -14,7 +14,7 @@ export interface AddCollectionMutationsArgs { objectComposer: ObjectTypeComposer; schemaComposer: SchemaComposer; updateCollectionRecord: ( - entry: EntryNode & { _flatbread: CollectionContext } + entry: EntryNode & { _metadata: CollectionContext } ) => Promise; } @@ -36,15 +36,15 @@ export default function addCollectionMutations( args: { [name]: objectComposer.getInputType() }, description: `Update or create a ${name}`, async resolve(source, payload) { - // remove _flatbread to prevent injection - const { _flatbread, ...update } = source.author; + // remove _metadata to prevent injection + const { _metadata, ...update } = source.author; const targetRecord = objectComposer .getResolver('findById') .resolve({ args: update }); // remove supplied key (might not be required) - delete update[targetRecord._flatbread.referenceField]; + delete update[targetRecord._metadata.referenceField]; const newRecord = merge(targetRecord, update); await updateCollectionRecord(newRecord); diff --git a/packages/core/src/generators/collectionQueries.ts b/packages/core/src/generators/collectionQueries.ts index 7fc94bd0..b5cbabd2 100644 --- a/packages/core/src/generators/collectionQueries.ts +++ b/packages/core/src/generators/collectionQueries.ts @@ -37,8 +37,10 @@ export default function addCollectionQueries(args: AddCollectionQueriesArgs) { description: `Find one ${name} by its ID`, args: generateArgsForSingleItemQuery(), resolve: (rp: Record) => - cloneDeep(allContentNodesJSON[name]).find( - (node: EntryNode) => node.id === rp.args.id + cloneDeep( + allContentNodesJSON[name].find( + (node: EntryNode) => node.id === rp.args.id + ) ), }); diff --git a/packages/core/src/generators/schema.ts b/packages/core/src/generators/schema.ts index aec72f73..0da36681 100644 --- a/packages/core/src/generators/schema.ts +++ b/packages/core/src/generators/schema.ts @@ -1,20 +1,18 @@ import { schemaComposer } from 'graphql-compose'; import { composeWithJson } from 'graphql-compose-json'; -import { defaultsDeep, get, keyBy, merge } from 'lodash-es'; +import { defaultsDeep, get, merge } from 'lodash-es'; import plur from 'plur'; import { VFile } from 'vfile'; import { cacheSchema, checkCacheForSchema } from '../cache/cache'; import { - CollectionContext, - CollectionEntry, ConfigResult, EntryNode, + LoadedCollectionEntry, LoadedFlatbreadConfig, Source, Transformer, } from '../types'; -import createHash from '../utils/createHash'; import { getFieldOverrides } from '../utils/fieldOverrides'; import { map } from '../utils/map'; import addCollectionMutations from './collectionMutations'; @@ -48,18 +46,25 @@ export async function generateSchema( const addRecord = (source: Source) => - (collection: CollectionEntry, record: EntryNode, context: Ctx) => { - allContentNodes[collection.collection] = - allContentNodes[collection.collection] ?? []; - allContentNodes[collection.collection].push({ + ( + collection: LoadedCollectionEntry, + record: EntryNode, + context: Ctx + ) => { + allContentNodes[collection.name] = allContentNodes[collection.name] ?? []; + + const newRecord = { record, context: { sourceContext: context, sourcedBy: source.id, - collection: collection.collection, + collection: collection.name, referenceField: collection.referenceField ?? 'id', }, - }); + }; + + allContentNodes[collection.name].push(newRecord); + return newRecord; }; await config.source.fetch(config.content, { @@ -87,7 +92,7 @@ export async function generateSchema( collection, defaultsDeep( {}, - getFieldOverrides(collection, config), + getFieldOverrides(collection, config.content), ...nodes.map((node) => merge({}, node, preknownSchemaFragments)) ), { schemaComposer } @@ -103,14 +108,10 @@ export async function generateSchema( undefined: config.transformer[0], }; - async function updateCollectionRecord( - entry: EntryNode & { _flatbread: any } - ) { - const { _flatbread: ctx, ...record } = entry; - const file = await transformersById[ctx.transformedBy].serialize( - record, - ctx.transformContext - ); + async function updateCollectionRecord(entry: EntryNode & { _metadata: any }) { + const { _metadata: ctx, ...record } = entry; + const { serialize } = transformersById[ctx.transformedBy]; + const file = await serialize(record, ctx.transformContext); await config?.source.put(file, ctx.sourceContext); const index = allContentNodesJSON[ctx.collection].findIndex( @@ -163,8 +164,8 @@ export async function generateSchema( } // Create map of references on each content node - for (const { collection, refs } of config.content) { - const typeTC = schemaComposer.getOTC(collection); + for (const { name, refs } of config.content) { + const typeTC = schemaComposer.getOTC(name); if (!refs) continue; @@ -181,7 +182,7 @@ export async function generateSchema( description: `All ${plur( String(refType), 2 - )} that are referenced by this ${collection}`, + )} that are referenced by this ${name}`, resolver: () => refTypeTC.getResolver('findMany'), prepareArgs: { ids: (source) => source[refField], @@ -191,7 +192,7 @@ export async function generateSchema( } else { // If the reference field has a single node typeTC.addRelation(refField, { - description: `The ${refType} referenced by this ${collection}`, + description: `The ${refType} referenced by this ${name}`, resolver: () => refTypeTC.getResolver('findById'), prepareArgs: { id: (source) => source[refField], @@ -261,13 +262,14 @@ const optionallyTransformContentNodes = ( throw new Error(`no transformer found for ${node.record.path}`); } const { record: doc, context } = transformer.parse(node.record); - doc._flatbread = node.context; - doc._flatbread.transformedBy = transformer.id; - doc._flatbread.transformContext = context; - doc._flatbread.reference = get(doc, node.context.referenceField); + doc._metadata = node.context; + doc._metadata.transformedBy = transformer.id; + doc._metadata.transformContext = context; + doc._metadata.reference = get(doc, node.context.referenceField); return doc; }); } + // TODO: might need to map this to attach metadata here return allContentNodes; }; diff --git a/packages/core/src/providers/test/base.test.ts b/packages/core/src/providers/test/base.test.ts index dc267fd7..3a30499d 100644 --- a/packages/core/src/providers/test/base.test.ts +++ b/packages/core/src/providers/test/base.test.ts @@ -16,7 +16,7 @@ function basicProject() { content: [ { path: 'packages/flatbread/content/authors', - collection: 'Author', + name: 'Author', refs: { friend: 'Author', }, @@ -59,37 +59,37 @@ test('relational filter query', async (t) => { t.snapshot(result); }); -test('update collection record', async (t) => { - const flatbread = basicProject(); - const sitting = (Math.random() * 100) | 0; - const result: any = await flatbread.query({ - rootValue: { author: { id: '2a3e', skills: { sitting } } }, - source: ` - mutation UpdateAuthor($author: AuthorInput){ - updateAuthor(Author: $author) { - id - skills { - sitting - } - } - } - `, - }); +// test('update collection record', async (t) => { +// const flatbread = basicProject(); +// const sitting = (Math.random() * 100) | 0; +// const result: any = await flatbread.query({ +// rootValue: { author: { id: '2a3e', skills: { sitting } } }, +// source: ` +// mutation UpdateAuthor($author: AuthorInput){ +// updateAuthor(Author: $author) { +// id +// skills { +// sitting +// } +// } +// } +// `, +// }); - t.is(result.data.updateAuthor.skills.sitting, sitting); +// t.is(result.data.updateAuthor.skills.sitting, sitting); - const updated: any = await flatbread.query({ - source: ` - query { - Author(id: "2a3e") { - id - skills { - sitting - } - } - } - `, - }); +// const updated: any = await flatbread.query({ +// source: ` +// query { +// Author(id: "2a3e") { +// id +// skills { +// sitting +// } +// } +// } +// `, +// }); - t.is(updated.data.Author.skills.sitting, sitting); -}); +// t.is(updated.data.Author.skills.sitting, sitting); +// }); diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index f4234185..06332f36 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -23,13 +23,13 @@ export type ContentNode = BaseContentNode & { export interface FlatbreadConfig { source: Source; transformer?: Transformer | Transformer[]; - content: Partial[]; + content: CollectionEntry[]; } export interface LoadedFlatbreadConfig { source: Source; transformer: Transformer[]; - content: CollectionEntry[]; + content: LoadedCollectionEntry[]; loaded: { extensions: string[]; }; @@ -72,7 +72,7 @@ export type EntryNode = Record; export interface FlatbreadArgs { addRecord( - collection: CollectionEntry, + collection: LoadedCollectionEntry, record: EntryNode, context: Context ): void; @@ -83,7 +83,7 @@ export interface Source { id?: string; put: (source: VFile, ctx: Context) => Promise; fetch: ( - allContentTypes: CollectionEntry[], + allContentTypes: LoadedCollectionEntry[], flatbread: FlatbreadArgs ) => Promise; } @@ -123,10 +123,15 @@ export interface CollectionContext { * * This is paired with a `Source` (and, *optionally*, a `Transformer`) plugin. */ + export interface CollectionEntry { - collection: string; + name: string; + path: string; overrides?: Override[]; - referenceField: string; + refs?: Record; + referenceField?: string; +} - [key: string]: any; +export interface LoadedCollectionEntry extends CollectionEntry { + referenceField: string; } diff --git a/packages/core/src/utils/createHash.ts b/packages/core/src/utils/createHash.ts deleted file mode 100644 index 03190add..00000000 --- a/packages/core/src/utils/createHash.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { anyToString } from './stringUtils'; -import { createHash as createHashRaw } from 'crypto'; - -export default function createHash(content: any) { - return createHashRaw('sha256').update(anyToString(content)).digest('hex'); -} diff --git a/packages/core/src/utils/createShaHash.ts b/packages/core/src/utils/createShaHash.ts new file mode 100644 index 00000000..a194072a --- /dev/null +++ b/packages/core/src/utils/createShaHash.ts @@ -0,0 +1,6 @@ +import { anyToString } from './stringUtils'; +import { createHash } from 'crypto'; + +export default function createShaHash(content: any) { + return createHash('sha256').update(anyToString(content)).digest('hex'); +} diff --git a/packages/core/src/utils/fieldOverrides.ts b/packages/core/src/utils/fieldOverrides.ts index f3106bb9..5122ea14 100644 --- a/packages/core/src/utils/fieldOverrides.ts +++ b/packages/core/src/utils/fieldOverrides.ts @@ -1,20 +1,24 @@ -import { FlatbreadConfig, Override } from '../types'; import { get, set } from 'lodash-es'; +import { CollectionEntry } from '../../dist'; +import { Override } from '../types'; /** * Get an object containing functions nested in an object structure * aligning to the listed overrides in the config * - * @param collection the collection string referenced in the config - * @param config the flatbread config object + * @param collectionName the collection string referenced in the config + * @param entries the flatbread config object * @returns an object in the shape of the json schema */ -export function getFieldOverrides(collection: string, config: FlatbreadConfig) { - const content = config.content.find( - (content) => content.collection === collection +export function getFieldOverrides( + collectionName: string, + entries: CollectionEntry[] +) { + const collectionEntry = entries.find( + (entry) => entry.name === collectionName ); - if (!content?.overrides) return {}; - const overrides = content.overrides; + if (!collectionEntry?.overrides) return {}; + const overrides = collectionEntry.overrides; return overrides.reduce((fields: any, override: Override) => { const { field, type, ...rest } = override; diff --git a/packages/core/src/utils/initializeConfig.ts b/packages/core/src/utils/initializeConfig.ts index 6deabd29..f1b4f2e5 100644 --- a/packages/core/src/utils/initializeConfig.ts +++ b/packages/core/src/utils/initializeConfig.ts @@ -1,8 +1,8 @@ import { cloneDeep, defaultsDeep } from 'lodash-es'; -import { CollectionEntry } from '../types'; +import { LoadedCollectionEntry } from '../types'; import { FlatbreadConfig, LoadedFlatbreadConfig, Transformer } from '../types'; import { toArray } from './arrayUtils'; -import createHash from './createHash'; +import createShaHash from './createShaHash'; import { anyToString } from './stringUtils'; /** @@ -13,15 +13,15 @@ export function initializeConfig( ): LoadedFlatbreadConfig { const config = cloneDeep(rawConfig); const transformer = toArray(config.transformer ?? []).map((t) => { - t.id = t.id ?? createHash(t); + t.id = t.id ?? createShaHash(t); return t; }); - config.source.id = config.source.id ?? createHash(config.source); + config.source.id = config.source.id ?? createShaHash(config.source); return { ...config, - content: config.content?.map((content: Partial) => + content: config.content?.map((content: Partial) => defaultsDeep(content, { referenceField: 'id' }) ), transformer, diff --git a/packages/core/src/utils/tests/fieldOverrides.test.ts b/packages/core/src/utils/tests/fieldOverrides.test.ts index fca200ee..bb93fdf7 100644 --- a/packages/core/src/utils/tests/fieldOverrides.test.ts +++ b/packages/core/src/utils/tests/fieldOverrides.test.ts @@ -1,8 +1,9 @@ import test from 'ava'; +import { LoadedCollectionEntry } from '../../types'; import { getFieldOverrides } from '../fieldOverrides.js'; -function getProps(overrides: any[]): [string, any] { - return ['t', { content: [{ collection: 't', overrides }] }]; +function getProps(overrides: any[]): [string, LoadedCollectionEntry[]] { + return ['t', [{ name: 't', overrides, referenceField: 'id' }]]; } test('basic override', (t) => { diff --git a/packages/flatbread/content/authors/me.md b/packages/flatbread/content/authors/me.md index 3bb4e738..200f7328 100644 --- a/packages/flatbread/content/authors/me.md +++ b/packages/flatbread/content/authors/me.md @@ -8,7 +8,7 @@ enjoys: - making this date_joined: 2021-02-25T16:41:59.558Z skills: - sitting: 69 + sitting: 71 breathing: 7.07 liquid_consumption: 100 existence: simulation diff --git a/packages/source-filesystem/src/index.ts b/packages/source-filesystem/src/index.ts index 60696483..6f588963 100644 --- a/packages/source-filesystem/src/index.ts +++ b/packages/source-filesystem/src/index.ts @@ -4,7 +4,7 @@ import { read, write } from 'to-vfile'; import ownPackage from '../package.json' assert { type: 'json' }; import type { CollectionContext, - CollectionEntry, + LoadedCollectionEntry, FlatbreadArgs, LoadedFlatbreadConfig, SourcePlugin, @@ -32,7 +32,7 @@ interface Context { * @returns An array of content nodes */ async function getNodesFromDirectory( - collectionEntry: CollectionEntry, + collectionEntry: LoadedCollectionEntry, { addRecord }: FlatbreadArgs, config: InitializedSourceFilesystemConfig ): Promise { @@ -61,7 +61,7 @@ async function getNodesFromDirectory( * @returns */ async function getAllNodes( - allCollectionEntries: CollectionEntry[], + allCollectionEntries: LoadedCollectionEntry[], flatbread: FlatbreadArgs, config: InitializedSourceFilesystemConfig ): Promise { @@ -70,7 +70,7 @@ async function getAllNodes( async (contentType): Promise> => new Promise(async (res) => res([ - contentType.collection, + contentType.name, await getNodesFromDirectory(contentType, flatbread, config), ]) ) @@ -110,8 +110,10 @@ export function source(sourceConfig?: sourceFilesystemConfig) { const { extensions } = flatbreadConfig.loaded; config = defaultsDeep(sourceConfig ?? {}, { extensions }); }, - fetch: (content: CollectionEntry[], flatbread: FlatbreadArgs) => - getAllNodes(content, flatbread, config), + fetch: ( + content: LoadedCollectionEntry[], + flatbread: FlatbreadArgs + ) => getAllNodes(content, flatbread, config), put, }; } From 230970899bbc5308365e6f837070a68e9e9f237e Mon Sep 17 00:00:00 2001 From: Adam Sparks Date: Sat, 6 Aug 2022 13:57:40 -0500 Subject: [PATCH 08/22] added sourceMemory to allow for testing without a filesystem --- packages/core/src/providers/test/base.test.ts | 70 ++++++++-------- packages/core/src/providers/test/mockData.ts | 75 ++++++++++++++++++ .../providers/test/snapshots/base.test.ts.md | 22 +++-- .../test/snapshots/base.test.ts.snap | Bin 470 -> 576 bytes packages/core/src/sources/base.ts | 51 ++++++++++++ packages/core/src/types.ts | 7 +- tsconfig.json | 4 + 7 files changed, 189 insertions(+), 40 deletions(-) create mode 100644 packages/core/src/providers/test/mockData.ts create mode 100644 packages/core/src/sources/base.ts diff --git a/packages/core/src/providers/test/base.test.ts b/packages/core/src/providers/test/base.test.ts index 3a30499d..9892b025 100644 --- a/packages/core/src/providers/test/base.test.ts +++ b/packages/core/src/providers/test/base.test.ts @@ -2,10 +2,14 @@ import test from 'ava'; import filesystem from '@flatbread/source-filesystem'; import markdownTransformer from '@flatbread/transformer-markdown'; import { FlatbreadProvider } from '../base'; +import { SourceMemory } from '../../sources/base'; +import { mockData } from './mockData'; + +const sourceMemory = new SourceMemory(mockData); function basicProject() { return new FlatbreadProvider({ - source: filesystem(), + source: sourceMemory, transformer: markdownTransformer({ markdown: { gfm: true, @@ -48,7 +52,7 @@ test('relational filter query', async (t) => { const result = await flatbread.query({ source: ` query AllAuthors { - allAuthors(filter: {friend: {name: {wildcard: "Anot*"}}}) { + allAuthors(filter: {friend: {name: {wildcard: "Ev*"}}}) { name enjoys } @@ -59,37 +63,37 @@ test('relational filter query', async (t) => { t.snapshot(result); }); -// test('update collection record', async (t) => { -// const flatbread = basicProject(); -// const sitting = (Math.random() * 100) | 0; -// const result: any = await flatbread.query({ -// rootValue: { author: { id: '2a3e', skills: { sitting } } }, -// source: ` -// mutation UpdateAuthor($author: AuthorInput){ -// updateAuthor(Author: $author) { -// id -// skills { -// sitting -// } -// } -// } -// `, -// }); +test('update collection record', async (t) => { + const flatbread = basicProject(); + const sitting = (Math.random() * 100) | 0; + const result: any = await flatbread.query({ + rootValue: { author: { id: '2a3e', skills: { sitting } } }, + source: ` + mutation UpdateAuthor($author: AuthorInput){ + updateAuthor(Author: $author) { + id + skills { + sitting + } + } + } + `, + }); -// t.is(result.data.updateAuthor.skills.sitting, sitting); + t.is(result.data.updateAuthor.skills.sitting, sitting); -// const updated: any = await flatbread.query({ -// source: ` -// query { -// Author(id: "2a3e") { -// id -// skills { -// sitting -// } -// } -// } -// `, -// }); + const updated: any = await flatbread.query({ + source: ` + query { + Author(id: "2a3e") { + id + skills { + sitting + } + } + } + `, + }); -// t.is(updated.data.Author.skills.sitting, sitting); -// }); + t.is(updated.data.Author.skills.sitting, sitting); +}); diff --git a/packages/core/src/providers/test/mockData.ts b/packages/core/src/providers/test/mockData.ts new file mode 100644 index 00000000..44180443 --- /dev/null +++ b/packages/core/src/providers/test/mockData.ts @@ -0,0 +1,75 @@ +import { VFile } from 'vfile'; + +export const mockData = { + Author: [ + new VFile({ + path: '/content/authors/eva.md', + extname: '.md', + value: `--- + id: 40s3 + name: Eva + entity: Cat + enjoys: + - sitting + - standing + - mow mow + - sleepy time + - attention + friend: 2a3e + image: eva.svg + date_joined: 2002-02-25T16:41:59.558Z + skills: + sitting: 100000 + breathing: 4.7 + liquid_consumption: 10 + existence: funky + sports: -200 +--- + `, + }), + new VFile({ + path: '/content/authors/tony.md', + extname: '.md', + value: `--- + name: Tony + id: 2a3e + friend: ab2c + enjoys: + - cats + - tea + - making this + date_joined: 2021-02-25T16:41:59.558Z + skills: + sitting: 71 + breathing: 7.07 + liquid_consumption: 100 + existence: simulation + sports: -2 + cat_pat: 1500 +--- + `, + }), + new VFile({ + path: '/content/authors/daes.md', + extname: '.md', + value: `--- + id: ab2c + name: Daes + entity: Human + enjoys: + - cats + - coffee + - design + friend: 40s3 + date_joined: 2021-04-22T16:41:59.558Z + skills: + sitting: 304 + breathing: 1.034234 + liquid_consumption: -100 + existence: etheral + sports: 3 +--- + `, + }), + ], +}; diff --git a/packages/core/src/providers/test/snapshots/base.test.ts.md b/packages/core/src/providers/test/snapshots/base.test.ts.md index b48e6771..5ca7858a 100644 --- a/packages/core/src/providers/test/snapshots/base.test.ts.md +++ b/packages/core/src/providers/test/snapshots/base.test.ts.md @@ -13,9 +13,13 @@ Generated by [AVA](https://avajs.dev). allAuthors: [ { enjoys: [ - 'apples', + 'sitting', + 'standing', + 'mow mow', + 'sleepy time', + 'attention', ], - name: 'Another User', + name: 'Eva', }, { enjoys: [ @@ -25,6 +29,14 @@ Generated by [AVA](https://avajs.dev). ], name: 'Tony', }, + { + enjoys: [ + 'cats', + 'coffee', + 'design', + ], + name: 'Daes', + }, ], }, } @@ -39,10 +51,10 @@ Generated by [AVA](https://avajs.dev). { enjoys: [ 'cats', - 'tea', - 'making this', + 'coffee', + 'design', ], - name: 'Tony', + name: 'Daes', }, ], }, diff --git a/packages/core/src/providers/test/snapshots/base.test.ts.snap b/packages/core/src/providers/test/snapshots/base.test.ts.snap index 950c7672817dab7f153879b50fbae21405ddc62d..61d815b009304b40952f4953def70e0acb14d36d 100644 GIT binary patch literal 576 zcmV-G0>Ax1RzV$?(}4Wvp;4D=Ti!~z>rB_xD|gcvI^vLc4AOl-VMdnU0}svrg|ee$#4_db8m&(Hg# zL6XH&^W+6uWfPnZq)~CSy@Pq7jFwwwl3DY3e>jv@-WOj4Ych903-}c}Un5+Eum|Ct z!6wEQpbWd+E`#$7fWZZT+eEwsfPfbdbL(_ygAoJ1So`*9;}XPM2PLE#M-awjRq6sCk-sQ3$({SoyQ zQQijj~qh@yUGvpJ*8?^>86mcG^s{Sw!#&;S+6>AZ`_rRVn&p;FD|8 zTGUo$+JAb>MuJ#cQ*$9K%9??8DyL*DvJ+)i5ewsNG(xQDg+nxItWP$7mSZBFPnkp` zl~~MA`wPJ9|MV}w4=DHktnEKu^v}Kl@q^Ak3B5D+yj+T_@OMiQJfK1(JR&@YKP|&Q O8}2VAm0!N#1pojV)(D0G literal 470 zcmV;{0V)1LRzVvL3gW;qU%xa)KRULESn45te&c&O z1I<(ek{^o*00000000BcQoByWKoGn=KN2MYCxQ=%sA!-7T8gw#AR#EI(8al6Cl@>M zT@;xPegFx+Ak=g;6o`U~f*$^ZijK93ox~_aAPOvvZoRuV-W|`dSr@+Dmgm=8xk~VM zok`cO_WC>sUFor|bbKYxhK&YO?1=gr$&u)RJkWJKw-8HUR>0h=x45kU(y&}E6IdVs z1Qr2yuyGkQ0!9V&h%Bb)VRn$Epff10CMXI_h+1Dcejpg|W~EC8S8FwK1l&$%DRv4XcQ4#D1nZ zmlYBqB6&lsX;b=X%PJwFY=MAFW@_){NeBY M0G9*XF@6L909BROMgRZ+ diff --git a/packages/core/src/sources/base.ts b/packages/core/src/sources/base.ts new file mode 100644 index 00000000..7d1cb857 --- /dev/null +++ b/packages/core/src/sources/base.ts @@ -0,0 +1,51 @@ +import { cloneDeep } from 'lodash-es'; +import { VFile } from 'vfile'; +import { + FlatbreadArgs, + LoadedCollectionEntry, + LoadedFlatbreadConfig, + Source, +} from '../types'; + +interface MemContext { + id: string; + collectionName: string; +} + +export class SourceMemory implements Source { + private data: Record = {}; + + public id = '@flatbread/sourceMemory'; + + constructor(data: Record) { + this.data = data; + } + + initialize(config: LoadedFlatbreadConfig) {} + + async fetch( + entries: LoadedCollectionEntry[], + { addRecord }: FlatbreadArgs + ) { + for (const entry of entries) { + if (!this.data[entry.name]) + throw new Error(`can't find collection ${entry.name}`); + for (const record of this.data[entry.name]) { + addRecord(entry, cloneDeep(record), { + id: record.path, + collectionName: entry.name, + }); + } + } + } + + async put(doc: VFile, context: MemContext) { + const record = this.data[context.collectionName].find( + (entry) => entry.path === context.id + ); + + record.value = doc; + + return { doc, context }; + } +} diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 06332f36..14526716 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -74,14 +74,17 @@ export interface FlatbreadArgs { addRecord( collection: LoadedCollectionEntry, record: EntryNode, - context: Context + context?: Context ): void; } export interface Source { initialize?: (flatbreadConfig: LoadedFlatbreadConfig) => void; id?: string; - put: (source: VFile, ctx: Context) => Promise; + put: ( + source: VFile, + ctx: Context + ) => Promise<{ doc: VFile; context: Context }>; fetch: ( allContentTypes: LoadedCollectionEntry[], flatbread: FlatbreadArgs diff --git a/tsconfig.json b/tsconfig.json index 2de15ee4..ed99ee0c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,5 +24,9 @@ ] } }, + //https://github.com/avajs/ava/discussions/3036#discussioncomment-2928239 + "ts-node": { + "transpileOnly": true + }, "exclude": ["**/dist/**", "**/node_modules/**"] } From 27912b96a837f7b9325a1ccbb8c622565104f8f6 Mon Sep 17 00:00:00 2001 From: Adam Sparks Date: Sat, 6 Aug 2022 14:44:35 -0500 Subject: [PATCH 09/22] sourceVirtual --- packages/core/src/providers/test/base.test.ts | 9 ++++----- packages/core/src/sources/{base.ts => virtual.ts} | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) rename packages/core/src/sources/{base.ts => virtual.ts} (94%) diff --git a/packages/core/src/providers/test/base.test.ts b/packages/core/src/providers/test/base.test.ts index 9892b025..a980104d 100644 --- a/packages/core/src/providers/test/base.test.ts +++ b/packages/core/src/providers/test/base.test.ts @@ -1,15 +1,14 @@ -import test from 'ava'; -import filesystem from '@flatbread/source-filesystem'; import markdownTransformer from '@flatbread/transformer-markdown'; +import test from 'ava'; +import { SourceVirtual } from '../../sources/virtual'; import { FlatbreadProvider } from '../base'; -import { SourceMemory } from '../../sources/base'; import { mockData } from './mockData'; -const sourceMemory = new SourceMemory(mockData); +const sourceVirtual = new SourceVirtual(mockData); function basicProject() { return new FlatbreadProvider({ - source: sourceMemory, + source: sourceVirtual, transformer: markdownTransformer({ markdown: { gfm: true, diff --git a/packages/core/src/sources/base.ts b/packages/core/src/sources/virtual.ts similarity index 94% rename from packages/core/src/sources/base.ts rename to packages/core/src/sources/virtual.ts index 7d1cb857..3fb4f439 100644 --- a/packages/core/src/sources/base.ts +++ b/packages/core/src/sources/virtual.ts @@ -12,7 +12,7 @@ interface MemContext { collectionName: string; } -export class SourceMemory implements Source { +export class SourceVirtual implements Source { private data: Record = {}; public id = '@flatbread/sourceMemory'; From a1fd32d62ee33285a39f58499dd76013800b8f72 Mon Sep 17 00:00:00 2001 From: Adam Sparks Date: Sun, 7 Aug 2022 03:37:31 -0500 Subject: [PATCH 10/22] disable star paths, add basic create mutations --- packages/core/package.json | 1 + .../src/generators/collectionMutations.ts | 35 +++++++++++++--- packages/core/src/generators/schema.ts | 42 ++++++++++++++----- packages/core/src/providers/test/base.test.ts | 38 ++++++++++++++++- packages/core/src/sources/virtual.ts | 12 ++++-- packages/core/src/types.ts | 5 ++- packages/source-filesystem/src/index.ts | 25 ++++------- .../src/utils/gatherFileNodes.ts | 2 +- .../src/utils/tests/gatherFileNodes.test.ts | 32 +++++++------- packages/transformer-markdown/src/index.ts | 2 +- pnpm-lock.yaml | 8 ++++ 11 files changed, 144 insertions(+), 58 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index b49d91f8..446e7d98 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -35,6 +35,7 @@ "lodash-es": "4.17.21", "lru-cache": "7.13.2", "matcher": "5.0.0", + "nanoid": "4.0.0", "plur": "5.1.0" }, "devDependencies": { diff --git a/packages/core/src/generators/collectionMutations.ts b/packages/core/src/generators/collectionMutations.ts index c3e4421a..d31c11e6 100644 --- a/packages/core/src/generators/collectionMutations.ts +++ b/packages/core/src/generators/collectionMutations.ts @@ -2,9 +2,9 @@ import { ObjectTypeComposer, SchemaComposer } from 'graphql-compose'; import { merge } from 'lodash-es'; import { CollectionContext, + CollectionEntry, EntryNode, LoadedFlatbreadConfig, - Transformer, } from '../types'; export interface AddCollectionMutationsArgs { @@ -13,7 +13,9 @@ export interface AddCollectionMutationsArgs { config: LoadedFlatbreadConfig; objectComposer: ObjectTypeComposer; schemaComposer: SchemaComposer; + collectionEntry: CollectionEntry; updateCollectionRecord: ( + collection: CollectionEntry, entry: EntryNode & { _metadata: CollectionContext } ) => Promise; } @@ -28,16 +30,17 @@ export default function addCollectionMutations( objectComposer, schemaComposer, updateCollectionRecord, + collectionEntry, } = args; schemaComposer.Mutation.addFields({ [`update${name}`]: { type: objectComposer, - args: { [name]: objectComposer.getInputType() }, - description: `Update or create a ${name}`, + args: { [name]: objectComposer.getInputTypeComposer() }, + description: `Update a ${name}`, async resolve(source, payload) { // remove _metadata to prevent injection - const { _metadata, ...update } = source.author; + const { _metadata, ...update } = payload[name]; const targetRecord = objectComposer .getResolver('findById') @@ -47,10 +50,32 @@ export default function addCollectionMutations( delete update[targetRecord._metadata.referenceField]; const newRecord = merge(targetRecord, update); - await updateCollectionRecord(newRecord); + await updateCollectionRecord(collectionEntry, newRecord); return newRecord; }, }, + [`create${name}`]: { + type: objectComposer, + args: { + [name]: objectComposer + .getInputTypeComposer() + .clone(`${name}CreateInput`) + .removeField('id'), + }, + description: `Create a ${name}`, + async resolve(source, payload, args) { + const record = merge(payload[name], { + _metadata: { + referenceField: collectionEntry.referenceField ?? 'id', + collection: name, + transformedBy: collectionEntry?.defaultTransformer, + sourcedBy: collectionEntry?.defaultSource, + }, + }); + + return await updateCollectionRecord(collectionEntry, record); + }, + }, }); } diff --git a/packages/core/src/generators/schema.ts b/packages/core/src/generators/schema.ts index 0da36681..81fdacfd 100644 --- a/packages/core/src/generators/schema.ts +++ b/packages/core/src/generators/schema.ts @@ -1,6 +1,7 @@ import { schemaComposer } from 'graphql-compose'; import { composeWithJson } from 'graphql-compose-json'; -import { defaultsDeep, get, merge } from 'lodash-es'; +import { defaultsDeep, get, merge, set } from 'lodash-es'; +import { nanoid } from 'nanoid'; import plur from 'plur'; import { VFile } from 'vfile'; @@ -12,6 +13,7 @@ import { LoadedFlatbreadConfig, Source, Transformer, + CollectionEntry, } from '../types'; import { getFieldOverrides } from '../utils/fieldOverrides'; import { map } from '../utils/map'; @@ -108,18 +110,33 @@ export async function generateSchema( undefined: config.transformer[0], }; - async function updateCollectionRecord(entry: EntryNode & { _metadata: any }) { - const { _metadata: ctx, ...record } = entry; - const { serialize } = transformersById[ctx.transformedBy]; + async function updateCollectionRecord( + collection: CollectionEntry, + entry: EntryNode & { _metadata: any } + ) { + const ctx = entry._metadata; + const { serialize, id: transformerId } = + transformersById[ctx.transformedBy]; + + if (ctx.reference) { + const index = allContentNodesJSON[ctx.collection].findIndex( + (c) => get(c, ctx.referenceField) === ctx.reference + ); + + if (index < 0) throw new Error('Failed to find record to update'); + // replace in memory representation of record + allContentNodesJSON[ctx.collection][index] = entry; + } else { + entry._metadata.reference = nanoid(); + set(entry, entry._metadata.referenceField, entry._metadata.reference); + entry._metadata.transformedBy = transformerId; + allContentNodesJSON[ctx.collection].push(entry); + } + + const { _metadata, ...record } = entry; const file = await serialize(record, ctx.transformContext); + await config?.source.put(file, ctx.sourceContext, ctx); - await config?.source.put(file, ctx.sourceContext); - const index = allContentNodesJSON[ctx.collection].findIndex( - (c) => get(c, ctx.referenceField) === ctx.reference - ); - - // replace in memory representation of record - allContentNodesJSON[ctx.collection][index] = entry; return entry; } @@ -143,6 +160,8 @@ export async function generateSchema( /// Query resolvers // + // TODO: add a new type of plugin that can add resolvers to each collection, they should be called here + addCollectionQueries({ name, pluralName, @@ -160,6 +179,7 @@ export async function generateSchema( schemaComposer, updateCollectionRecord, config, + collectionEntry: config.content.find((c) => c.name === name), }); } diff --git a/packages/core/src/providers/test/base.test.ts b/packages/core/src/providers/test/base.test.ts index a980104d..1da68c9e 100644 --- a/packages/core/src/providers/test/base.test.ts +++ b/packages/core/src/providers/test/base.test.ts @@ -66,7 +66,7 @@ test('update collection record', async (t) => { const flatbread = basicProject(); const sitting = (Math.random() * 100) | 0; const result: any = await flatbread.query({ - rootValue: { author: { id: '2a3e', skills: { sitting } } }, + variableValues: { author: { id: '2a3e', skills: { sitting } } }, source: ` mutation UpdateAuthor($author: AuthorInput){ updateAuthor(Author: $author) { @@ -96,3 +96,39 @@ test('update collection record', async (t) => { t.is(updated.data.Author.skills.sitting, sitting); }); + +test('create collection record', async (t) => { + const flatbread = basicProject(); + const sitting = 69; + const result: any = await flatbread.query({ + variableValues: { test: { skills: { sitting } } }, + source: ` + mutation CreateAuthor($test: AuthorCreateInput){ + createAuthor(Author: $test) { + id + skills { + sitting + } + } + } + `, + }); + + t.is(result.data.createAuthor.skills.sitting, sitting); + + const updated: any = await flatbread.query({ + variableValues: { id: result.data.createAuthor.id }, + source: ` + query QueryAuthor($id: String) { + Author(id: $id) { + id + skills { + sitting + } + } + } + `, + }); + + t.is(updated.data.Author.skills.sitting, sitting); +}); diff --git a/packages/core/src/sources/virtual.ts b/packages/core/src/sources/virtual.ts index 3fb4f439..59a7d404 100644 --- a/packages/core/src/sources/virtual.ts +++ b/packages/core/src/sources/virtual.ts @@ -39,12 +39,16 @@ export class SourceVirtual implements Source { } } - async put(doc: VFile, context: MemContext) { - const record = this.data[context.collectionName].find( - (entry) => entry.path === context.id + async put(doc: VFile, context: MemContext, parentContext: any) { + const record = this.data[parentContext.collection].find( + (entry) => entry.path === parentContext.reference ); - record.value = doc; + if (record) { + record.value = doc.value; + } else { + this.data[parentContext.collection].push(doc); + } return { doc, context }; } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 14526716..5b946902 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -83,7 +83,8 @@ export interface Source { id?: string; put: ( source: VFile, - ctx: Context + ctx: Context, + parentContext: any ) => Promise<{ doc: VFile; context: Context }>; fetch: ( allContentTypes: LoadedCollectionEntry[], @@ -133,6 +134,8 @@ export interface CollectionEntry { overrides?: Override[]; refs?: Record; referenceField?: string; + defaultTransformer?: string; + defaultSource?: string; } export interface LoadedCollectionEntry extends CollectionEntry { diff --git a/packages/source-filesystem/src/index.ts b/packages/source-filesystem/src/index.ts index 6f588963..efb5038a 100644 --- a/packages/source-filesystem/src/index.ts +++ b/packages/source-filesystem/src/index.ts @@ -1,15 +1,12 @@ -import slugify from '@sindresorhus/slugify'; -import { defaultsDeep, merge } from 'lodash-es'; -import { read, write } from 'to-vfile'; -import ownPackage from '../package.json' assert { type: 'json' }; import type { - CollectionContext, - LoadedCollectionEntry, FlatbreadArgs, + LoadedCollectionEntry, LoadedFlatbreadConfig, - SourcePlugin, } from '@flatbread/core'; +import slugify from '@sindresorhus/slugify'; +import { defaultsDeep } from 'lodash-es'; import { relative, resolve } from 'path'; +import { read, write } from 'to-vfile'; import type { VFile } from 'vfile'; import type { FileNode, @@ -65,7 +62,7 @@ async function getAllNodes( flatbread: FlatbreadArgs, config: InitializedSourceFilesystemConfig ): Promise { - const nodeEntries = await Promise.all( + await Promise.all( allCollectionEntries.map( async (contentType): Promise> => new Promise(async (res) => @@ -76,18 +73,10 @@ async function getAllNodes( ) ) ); - - const nodes = Object.fromEntries( - nodeEntries as Iterable - ); } -// TODO: _flatbread data should be extracted from plugins -// plugin should return a context object and be given the same context object back when saving, -// this context object will be saved internally under _flatbread[collectionId] - -async function put(doc: VFile, context: Context) { - doc.basename = context.filename; +async function put(doc: VFile, context: Context, parentContext: any) { + doc.basename = context?.filename ?? parentContext.reference; doc.path = resolve(process.cwd(), context.path); await write(doc); diff --git a/packages/source-filesystem/src/utils/gatherFileNodes.ts b/packages/source-filesystem/src/utils/gatherFileNodes.ts index 8f880636..c3201400 100644 --- a/packages/source-filesystem/src/utils/gatherFileNodes.ts +++ b/packages/source-filesystem/src/utils/gatherFileNodes.ts @@ -51,7 +51,7 @@ export default async function gatherFileNodes( ) ?? ['.md', '.mdx', '.markdown']; // gather all the globs in the path ( [capture-groups], **, *) - const [pathPrefix, ...globs] = path.split(/\/(?:\[|\*+)/); + const [pathPrefix, ...globs] = path.split(/\/(?:\[)/); // for each segment - gather names for capture groups // and calculate what to remove from matches ex: [name].md => remove .md from match diff --git a/packages/source-filesystem/src/utils/tests/gatherFileNodes.test.ts b/packages/source-filesystem/src/utils/tests/gatherFileNodes.test.ts index e9b2699c..67bc3134 100644 --- a/packages/source-filesystem/src/utils/tests/gatherFileNodes.test.ts +++ b/packages/source-filesystem/src/utils/tests/gatherFileNodes.test.ts @@ -32,20 +32,20 @@ test('basic case', async (t) => { t.snapshot(result2); }); -test('double level recursion', async (t) => { - const result = await gatherFileNodes('deeply/**/*.md', opts); - t.snapshot(result); -}); +// test('double level recursion', async (t) => { +// const result = await gatherFileNodes('deeply/**/*.md', opts); +// t.snapshot(result); +// }); test('double level recursion named', async (t) => { const result = await gatherFileNodes('deeply/[a]/[b].md', opts); t.snapshot(result); }); -test('single level recursion', async (t) => { - const result = await gatherFileNodes('./*.md', opts as any); - t.snapshot(result); -}); +// test('single level recursion', async (t) => { +// const result = await gatherFileNodes('./*.md', opts as any); +// t.snapshot(result); +// }); test('double level recursion named without parent directory', async (t) => { const result = await gatherFileNodes('./[genre]/[title].md', opts); @@ -57,15 +57,15 @@ test('single level named', async (t) => { t.snapshot(result); }); -test('double level first named', async (t) => { - const result = await gatherFileNodes('./[genre]/*.md', opts); - t.snapshot(result); -}); +// test('double level first named', async (t) => { +// const result = await gatherFileNodes('./[genre]/*.md', opts); +// t.snapshot(result); +// }); -test('double level second named', async (t) => { - const result = await gatherFileNodes('./**/[title].md', opts); - t.snapshot(result); -}); +// test('double level second named', async (t) => { +// const result = await gatherFileNodes('./**/[title].md', opts); +// t.snapshot(result); +// }); test('triple level', async (t) => { const result = await gatherFileNodes('./[random]/[name]/[title].md', opts); diff --git a/packages/transformer-markdown/src/index.ts b/packages/transformer-markdown/src/index.ts index 77358696..eb8dd087 100644 --- a/packages/transformer-markdown/src/index.ts +++ b/packages/transformer-markdown/src/index.ts @@ -40,7 +40,7 @@ function serialize( config: MarkdownTransformerConfig ) { const { _content, ...rest } = data; - const doc = matter.stringify(_content.raw, rest, config.grayMatter); + const doc = matter.stringify(_content?.raw ?? '', rest, config.grayMatter); return new VFile(doc); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6ef1c28b..a19f7dfb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -152,6 +152,7 @@ importers: lodash-es: 4.17.21 lru-cache: 7.13.2 matcher: 5.0.0 + nanoid: 4.0.0 plur: 5.1.0 tsup: 6.2.1 typescript: 4.7.4 @@ -163,6 +164,7 @@ importers: lodash-es: 4.17.21 lru-cache: 7.13.2 matcher: 5.0.0 + nanoid: 4.0.0 plur: 5.1.0 devDependencies: '@types/lodash-es': 4.17.6 @@ -7210,6 +7212,12 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + /nanoid/4.0.0: + resolution: {integrity: sha512-IgBP8piMxe/gf73RTQx7hmnhwz0aaEXYakvqZyE302IXW3HyVNhdNGC+O2MwMAVhLEnvXlvKtGbtJf6wvHihCg==} + engines: {node: ^14 || ^16 || >=18} + hasBin: true + dev: false + /napi-build-utils/1.0.2: resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==} From 22da94951017bfc8b345c09a247a3b5cc17b2efc Mon Sep 17 00:00:00 2001 From: Adam Sparks Date: Sun, 7 Aug 2022 19:42:01 -0500 Subject: [PATCH 11/22] handle creating new records with variable paths --- examples/sveltekit/flatbread.config.js | 7 ---- packages/core/package.json | 2 + packages/core/src/generators/schema.ts | 24 +++++++---- packages/core/src/sources/virtual.ts | 2 +- packages/core/src/types.ts | 2 +- packages/core/src/utils/createUniqueId.ts | 9 +++++ packages/source-filesystem/src/index.test.ts | 40 +++++++++++++++++++ packages/source-filesystem/src/index.ts | 36 +++++++++++++++-- .../src/utils/gatherFileNodes.ts | 5 +++ packages/source-filesystem/tsup.config.ts | 2 +- pnpm-lock.yaml | 27 +++++++++---- 11 files changed, 127 insertions(+), 29 deletions(-) create mode 100644 packages/core/src/utils/createUniqueId.ts create mode 100644 packages/source-filesystem/src/index.test.ts diff --git a/examples/sveltekit/flatbread.config.js b/examples/sveltekit/flatbread.config.js index 8d9565db..30f03342 100644 --- a/examples/sveltekit/flatbread.config.js +++ b/examples/sveltekit/flatbread.config.js @@ -31,13 +31,6 @@ export default defineConfig({ authors: 'Author', }, }, - { - path: 'content/markdown/posts/**/*.md', - name: 'PostCategoryBlob', - refs: { - authors: 'Author', - }, - }, { path: 'content/markdown/authors', name: 'Author', diff --git a/packages/core/package.json b/packages/core/package.json index 446e7d98..c35ac2af 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -40,7 +40,9 @@ }, "devDependencies": { "@types/lodash-es": "4.17.6", + "@types/nanoid-dictionary": "4.2.0", "@types/node": "16.11.47", + "nanoid-dictionary": "4.3.0", "tsup": "6.2.1", "typescript": "4.7.4", "vfile": "5.3.4" diff --git a/packages/core/src/generators/schema.ts b/packages/core/src/generators/schema.ts index 81fdacfd..39995e31 100644 --- a/packages/core/src/generators/schema.ts +++ b/packages/core/src/generators/schema.ts @@ -1,20 +1,20 @@ import { schemaComposer } from 'graphql-compose'; import { composeWithJson } from 'graphql-compose-json'; import { defaultsDeep, get, merge, set } from 'lodash-es'; -import { nanoid } from 'nanoid'; import plur from 'plur'; import { VFile } from 'vfile'; import { cacheSchema, checkCacheForSchema } from '../cache/cache'; import { + CollectionEntry, ConfigResult, EntryNode, LoadedCollectionEntry, LoadedFlatbreadConfig, Source, Transformer, - CollectionEntry, } from '../types'; +import { createUniqueId } from '../utils/createUniqueId'; import { getFieldOverrides } from '../utils/fieldOverrides'; import { map } from '../utils/map'; import addCollectionMutations from './collectionMutations'; @@ -115,8 +115,11 @@ export async function generateSchema( entry: EntryNode & { _metadata: any } ) { const ctx = entry._metadata; - const { serialize, id: transformerId } = - transformersById[ctx.transformedBy]; + const { + serialize, + extensions, + id: transformerId, + } = transformersById[ctx.transformedBy]; if (ctx.reference) { const index = allContentNodesJSON[ctx.collection].findIndex( @@ -127,15 +130,20 @@ export async function generateSchema( // replace in memory representation of record allContentNodesJSON[ctx.collection][index] = entry; } else { - entry._metadata.reference = nanoid(); + entry._metadata.reference = createUniqueId(); set(entry, entry._metadata.referenceField, entry._metadata.reference); entry._metadata.transformedBy = transformerId; + entry._metadata.extension = extensions?.[0]; allContentNodesJSON[ctx.collection].push(entry); } const { _metadata, ...record } = entry; const file = await serialize(record, ctx.transformContext); - await config?.source.put(file, ctx.sourceContext, ctx); + await config?.source.put(file, ctx.sourceContext, { + parentContext: ctx, + collection, + record, + }); return entry; } @@ -179,7 +187,9 @@ export async function generateSchema( schemaComposer, updateCollectionRecord, config, - collectionEntry: config.content.find((c) => c.name === name), + collectionEntry: config.content.find( + (c) => c.name === name + ) as CollectionEntry, }); } diff --git a/packages/core/src/sources/virtual.ts b/packages/core/src/sources/virtual.ts index 59a7d404..d2be745a 100644 --- a/packages/core/src/sources/virtual.ts +++ b/packages/core/src/sources/virtual.ts @@ -39,7 +39,7 @@ export class SourceVirtual implements Source { } } - async put(doc: VFile, context: MemContext, parentContext: any) { + async put(doc: VFile, context: MemContext, { parentContext }: any) { const record = this.data[parentContext.collection].find( (entry) => entry.path === parentContext.reference ); diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 5b946902..e8ccf83f 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -84,7 +84,7 @@ export interface Source { put: ( source: VFile, ctx: Context, - parentContext: any + opts: { parentContext: any; collection: CollectionEntry; record: any } ) => Promise<{ doc: VFile; context: Context }>; fetch: ( allContentTypes: LoadedCollectionEntry[], diff --git a/packages/core/src/utils/createUniqueId.ts b/packages/core/src/utils/createUniqueId.ts new file mode 100644 index 00000000..2f68dbda --- /dev/null +++ b/packages/core/src/utils/createUniqueId.ts @@ -0,0 +1,9 @@ +import { customAlphabet } from 'nanoid'; +import { lowercase, numbers } from 'nanoid-dictionary'; + +const nanoid = customAlphabet(lowercase + numbers); + +export function createUniqueId() { + // only use lower case to avoid issues with windows ignoring case on filenames + return nanoid(); +} diff --git a/packages/source-filesystem/src/index.test.ts b/packages/source-filesystem/src/index.test.ts new file mode 100644 index 00000000..6af017b8 --- /dev/null +++ b/packages/source-filesystem/src/index.test.ts @@ -0,0 +1,40 @@ +import test from 'ava'; +import { createPath } from './index'; + +test('create path can correctly populate a path', (t) => { + const path = createPath( + { + name: 'Test', + path: '/[test]/[nested.test]/[blah].md', + }, + { + test: 'first', + nested: { + test: 'second-part', + }, + blah: 'blarghhh', + }, + { extension: '' } + ); + + t.is(path, '/first/second-part/blarghhh.md'); +}); + +test('create path can correctly populate a path without an extension', (t) => { + const path = createPath( + { + name: 'Test', + path: '/[test]/[nested.test]/[blah]', + }, + { + test: 'first', + nested: { + test: 'second-part', + }, + blah: 'blarghhh', + }, + { extension: '.md', reference: 'test-name', referenceField: 'id' } + ); + + t.is(path, '/first/second-part/blarghhh/test-name.md'); +}); diff --git a/packages/source-filesystem/src/index.ts b/packages/source-filesystem/src/index.ts index efb5038a..75e9c3c8 100644 --- a/packages/source-filesystem/src/index.ts +++ b/packages/source-filesystem/src/index.ts @@ -1,11 +1,12 @@ import type { + CollectionEntry, FlatbreadArgs, LoadedCollectionEntry, LoadedFlatbreadConfig, } from '@flatbread/core'; import slugify from '@sindresorhus/slugify'; -import { defaultsDeep } from 'lodash-es'; -import { relative, resolve } from 'path'; +import { defaultsDeep, get } from 'lodash-es'; +import path, { relative, resolve } from 'path'; import { read, write } from 'to-vfile'; import type { VFile } from 'vfile'; import type { @@ -75,9 +76,36 @@ async function getAllNodes( ); } -async function put(doc: VFile, context: Context, parentContext: any) { +export function createPath( + collection: CollectionEntry, + record: any, + parentContext: any +): string { + const partialPath = collection.path.replace( + /\[(.*?)\]/g, + (_: any, match: any) => get(record, match) + ); + + const filename = path.parse(partialPath); + + if (!filename.ext) { + return resolve( + partialPath, + parentContext.reference + parentContext.extension + ); + } + + return partialPath; +} + +async function put( + doc: VFile, + context: Context, + { parentContext, collection, record }: any +) { + const path = context?.path ?? createPath(collection, record, parentContext); doc.basename = context?.filename ?? parentContext.reference; - doc.path = resolve(process.cwd(), context.path); + doc.path = resolve(process.cwd(), path); await write(doc); diff --git a/packages/source-filesystem/src/utils/gatherFileNodes.ts b/packages/source-filesystem/src/utils/gatherFileNodes.ts index c3201400..a4bdf664 100644 --- a/packages/source-filesystem/src/utils/gatherFileNodes.ts +++ b/packages/source-filesystem/src/utils/gatherFileNodes.ts @@ -41,6 +41,11 @@ export default async function gatherFileNodes( path: string, { readDirectory = readDir, extensions }: GatherFileNodesOptions = {} ): Promise { + if (path.includes('*')) + throw new Error( + `* wildcards are not supported, only variable named paths like [example]\nPlease change path in config "${path}"` + ); + /** * Prepend a period to the extension if it doesn't have one. * If no extensions are provided, use the default ones. diff --git a/packages/source-filesystem/tsup.config.ts b/packages/source-filesystem/tsup.config.ts index 24b848e6..fa78cb57 100644 --- a/packages/source-filesystem/tsup.config.ts +++ b/packages/source-filesystem/tsup.config.ts @@ -4,7 +4,7 @@ export const tsup: Options = { splitting: false, sourcemap: true, clean: true, - entryPoints: ['src/*'], + entryPoints: ['src/index.ts'], format: ['esm'], target: 'esnext', dts: true, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a19f7dfb..3f3f4d9b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -106,8 +106,8 @@ importers: svimg: 3.0.0 devDependencies: '@flatbread/transformer-yaml': link:../../packages/transformer-yaml - '@sveltejs/adapter-static': 1.0.0-next.38 - '@sveltejs/kit': 1.0.0-next.403_svelte@3.49.0+vite@3.0.4 + '@sveltejs/adapter-static': 1.0.0-next.39 + '@sveltejs/kit': 1.0.0-next.405_svelte@3.49.0+vite@3.0.4 '@typescript-eslint/eslint-plugin': 4.33.0_3ekaj7j3owlolnuhj3ykrb7u7i '@typescript-eslint/parser': 4.33.0_hxadhbs2xogijvk7vq4t2azzbu autoprefixer: 10.4.7_postcss@8.4.14 @@ -145,6 +145,7 @@ importers: packages/core: specifiers: '@types/lodash-es': 4.17.6 + '@types/nanoid-dictionary': 4.2.0 '@types/node': 16.11.47 graphql: 16.5.0 graphql-compose: 9.0.8 @@ -153,6 +154,7 @@ importers: lru-cache: 7.13.2 matcher: 5.0.0 nanoid: 4.0.0 + nanoid-dictionary: 4.3.0 plur: 5.1.0 tsup: 6.2.1 typescript: 4.7.4 @@ -168,7 +170,9 @@ importers: plur: 5.1.0 devDependencies: '@types/lodash-es': 4.17.6 + '@types/nanoid-dictionary': 4.2.0 '@types/node': 16.11.47 + nanoid-dictionary: 4.3.0 tsup: 6.2.1_typescript@4.7.4 typescript: 4.7.4 vfile: 5.3.4 @@ -1569,14 +1573,12 @@ packages: '@sinonjs/commons': 1.8.3 dev: true - /@sveltejs/adapter-static/1.0.0-next.38: - resolution: {integrity: sha512-O1b264K62E3OrUnsFxMjKn3CUJF50fxGcW0rWk8fa5kjzskPsSyTxS3jnWNryFaVJ3oSUtx57m4qFW43S1910Q==} - dependencies: - tiny-glob: 0.2.9 + /@sveltejs/adapter-static/1.0.0-next.39: + resolution: {integrity: sha512-EeD39H6iEe0UEKnKxLFTZFZpi/FcX5xfbAvsMQ+B09aDZccpQmkJBSIo+4kq1JsQGSjwi/+J3aE9bR67R6CIyQ==} dev: true - /@sveltejs/kit/1.0.0-next.403_svelte@3.49.0+vite@3.0.4: - resolution: {integrity: sha512-pKlmthl1SZkbx671Jp+LBoRne0vNzsjSgta9iRhqW/bt/0mx/IjlMd/NOeLuJGo30dAJdefrySoSamiaq47M/g==} + /@sveltejs/kit/1.0.0-next.405_svelte@3.49.0+vite@3.0.4: + resolution: {integrity: sha512-jHSa74F7k+hC+0fof75g/xm/+1M5sM66Qt6v8eLLMSgjkp36Lb5xOioBhbl6w0NYoE5xysLsBWuu+yHytfvCBA==} engines: {node: '>=16.9'} hasBin: true requiresBuild: true @@ -1588,6 +1590,7 @@ packages: chokidar: 3.5.3 sade: 1.8.1 svelte: 3.49.0 + tiny-glob: 0.2.9 vite: 3.0.4 transitivePeerDependencies: - diff-match-patch @@ -1806,6 +1809,10 @@ packages: resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==} dev: false + /@types/nanoid-dictionary/4.2.0: + resolution: {integrity: sha512-DyQddKC2AYsInXBMqKSwhom4gpouV8SF1smsCPSzR8hp20vZJ5o5Yxg7qXThCHwr/H6VMq01UArEGbF0q2FXig==} + dev: true + /@types/node/10.17.60: resolution: {integrity: sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==} dev: false @@ -7207,6 +7214,10 @@ packages: thenify-all: 1.6.0 dev: true + /nanoid-dictionary/4.3.0: + resolution: {integrity: sha512-Xw1+/QnRGWO1KJ0rLfU1xR85qXmAHyLbE3TUkklu9gOIDburP6CsUnLmTaNECGpBh5SHb2uPFmx0VT8UPyoeyw==} + dev: true + /nanoid/3.3.4: resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} From f31e91fca08b9bf36736aa7aecc4ed9153f765de Mon Sep 17 00:00:00 2001 From: Adam Sparks Date: Sun, 7 Aug 2022 20:06:05 -0500 Subject: [PATCH 12/22] remove nanoid-dictionary --- packages/core/package.json | 2 -- packages/core/src/utils/createUniqueId.ts | 5 ++--- pnpm-lock.yaml | 12 ------------ 3 files changed, 2 insertions(+), 17 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index c35ac2af..446e7d98 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -40,9 +40,7 @@ }, "devDependencies": { "@types/lodash-es": "4.17.6", - "@types/nanoid-dictionary": "4.2.0", "@types/node": "16.11.47", - "nanoid-dictionary": "4.3.0", "tsup": "6.2.1", "typescript": "4.7.4", "vfile": "5.3.4" diff --git a/packages/core/src/utils/createUniqueId.ts b/packages/core/src/utils/createUniqueId.ts index 2f68dbda..c860ed01 100644 --- a/packages/core/src/utils/createUniqueId.ts +++ b/packages/core/src/utils/createUniqueId.ts @@ -1,9 +1,8 @@ import { customAlphabet } from 'nanoid'; -import { lowercase, numbers } from 'nanoid-dictionary'; -const nanoid = customAlphabet(lowercase + numbers); +// only use lower case letters and numbers to avoid issues with windows ignoring case on filenames +const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz0123456789'); export function createUniqueId() { - // only use lower case to avoid issues with windows ignoring case on filenames return nanoid(); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3f3f4d9b..418fa204 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -145,7 +145,6 @@ importers: packages/core: specifiers: '@types/lodash-es': 4.17.6 - '@types/nanoid-dictionary': 4.2.0 '@types/node': 16.11.47 graphql: 16.5.0 graphql-compose: 9.0.8 @@ -154,7 +153,6 @@ importers: lru-cache: 7.13.2 matcher: 5.0.0 nanoid: 4.0.0 - nanoid-dictionary: 4.3.0 plur: 5.1.0 tsup: 6.2.1 typescript: 4.7.4 @@ -170,9 +168,7 @@ importers: plur: 5.1.0 devDependencies: '@types/lodash-es': 4.17.6 - '@types/nanoid-dictionary': 4.2.0 '@types/node': 16.11.47 - nanoid-dictionary: 4.3.0 tsup: 6.2.1_typescript@4.7.4 typescript: 4.7.4 vfile: 5.3.4 @@ -1809,10 +1805,6 @@ packages: resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==} dev: false - /@types/nanoid-dictionary/4.2.0: - resolution: {integrity: sha512-DyQddKC2AYsInXBMqKSwhom4gpouV8SF1smsCPSzR8hp20vZJ5o5Yxg7qXThCHwr/H6VMq01UArEGbF0q2FXig==} - dev: true - /@types/node/10.17.60: resolution: {integrity: sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==} dev: false @@ -7214,10 +7206,6 @@ packages: thenify-all: 1.6.0 dev: true - /nanoid-dictionary/4.3.0: - resolution: {integrity: sha512-Xw1+/QnRGWO1KJ0rLfU1xR85qXmAHyLbE3TUkklu9gOIDburP6CsUnLmTaNECGpBh5SHb2uPFmx0VT8UPyoeyw==} - dev: true - /nanoid/3.3.4: resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} From e43f01c716cb4e9db5976740559de5a9f024c6d4 Mon Sep 17 00:00:00 2001 From: Adam Sparks Date: Mon, 8 Aug 2022 01:02:26 -0500 Subject: [PATCH 13/22] cleanups, add required fields to resolvers, add upsert resolver --- .../src/generators/collectionMutations.ts | 102 +++++++++++++----- packages/core/src/generators/schema.ts | 29 +++-- packages/core/src/types.ts | 6 ++ packages/core/src/utils/initializeConfig.ts | 5 +- packages/flatbread/README.md | 4 + packages/source-filesystem/src/index.ts | 8 +- 6 files changed, 120 insertions(+), 34 deletions(-) diff --git a/packages/core/src/generators/collectionMutations.ts b/packages/core/src/generators/collectionMutations.ts index d31c11e6..13cd7ca4 100644 --- a/packages/core/src/generators/collectionMutations.ts +++ b/packages/core/src/generators/collectionMutations.ts @@ -1,9 +1,10 @@ import { ObjectTypeComposer, SchemaComposer } from 'graphql-compose'; -import { merge } from 'lodash-es'; +import { get, merge } from 'lodash-es'; import { CollectionContext, CollectionEntry, EntryNode, + LoadedCollectionEntry, LoadedFlatbreadConfig, } from '../types'; @@ -13,7 +14,7 @@ export interface AddCollectionMutationsArgs { config: LoadedFlatbreadConfig; objectComposer: ObjectTypeComposer; schemaComposer: SchemaComposer; - collectionEntry: CollectionEntry; + collectionEntry: LoadedCollectionEntry; updateCollectionRecord: ( collection: CollectionEntry, entry: EntryNode & { _metadata: CollectionContext } @@ -33,27 +34,72 @@ export default function addCollectionMutations( collectionEntry, } = args; + async function update( + payload: Record, + existing: EntryNode + ) { + // remove _metadata to prevent injection + const { _metadata, ...update } = payload?.[name]; + + const targetRecord = objectComposer + .getResolver('findById') + .resolve({ args: update }); + + // remove supplied key (might not be required) + delete update[targetRecord._metadata.referenceField]; + const newRecord = merge(targetRecord, update); + + await updateCollectionRecord(collectionEntry, newRecord); + + return newRecord; + } + + async function create(source: unknown, payload: Record) { + collectionEntry.creationRequiredFields.forEach((field) => { + if (!payload[name].hasOwnProperty(field)) + throw new Error( + `field ${field} is required when creating a new ${name}` + ); + }); + + const record = merge(payload[name], { + _metadata: { + referenceField: collectionEntry.referenceField ?? 'id', + collection: name, + transformedBy: collectionEntry?.defaultTransformer, + sourcedBy: collectionEntry?.defaultSource, + } as CollectionContext, + }); + + return await updateCollectionRecord(collectionEntry, record); + } + schemaComposer.Mutation.addFields({ [`update${name}`]: { type: objectComposer, - args: { [name]: objectComposer.getInputTypeComposer() }, + args: { + [name]: objectComposer + .getInputTypeComposer() + .makeFieldNonNull(collectionEntry.creationRequiredFields), + }, description: `Update a ${name}`, - async resolve(source, payload) { - // remove _metadata to prevent injection - const { _metadata, ...update } = payload[name]; + async resolve(source: unknown, payload: Record) { + const { _metadata, ...args } = payload?.[name]; - const targetRecord = objectComposer + const existingRecord = objectComposer .getResolver('findById') - .resolve({ args: update }); - - // remove supplied key (might not be required) - delete update[targetRecord._metadata.referenceField]; - const newRecord = merge(targetRecord, update); - - await updateCollectionRecord(collectionEntry, newRecord); + .resolve({ args }); - return newRecord; + if (!existingRecord) + throw new Error( + `${name} with ${collectionEntry.referenceField} of ${get( + args, + collectionEntry.referenceField + )} not found` + ); + return update(payload, existingRecord); }, + update, }, [`create${name}`]: { type: objectComposer, @@ -61,20 +107,24 @@ export default function addCollectionMutations( [name]: objectComposer .getInputTypeComposer() .clone(`${name}CreateInput`) - .removeField('id'), + .removeField('id') + .makeFieldNonNull(collectionEntry.creationRequiredFields), }, description: `Create a ${name}`, - async resolve(source, payload, args) { - const record = merge(payload[name], { - _metadata: { - referenceField: collectionEntry.referenceField ?? 'id', - collection: name, - transformedBy: collectionEntry?.defaultTransformer, - sourcedBy: collectionEntry?.defaultSource, - }, - }); + resolve: create, + }, + [`upsert${name}`]: { + type: objectComposer, + args: { [name]: objectComposer.getInputTypeComposer() }, + async resolve(source: unknown, payload: Record) { + const { _metadata, ...args } = payload?.[name]; + + const existingRecord = objectComposer + .getResolver('findById') + .resolve({ args }); - return await updateCollectionRecord(collectionEntry, record); + if (existingRecord) return update(payload, existingRecord); + create(source, payload); }, }, }); diff --git a/packages/core/src/generators/schema.ts b/packages/core/src/generators/schema.ts index 39995e31..34909ca2 100644 --- a/packages/core/src/generators/schema.ts +++ b/packages/core/src/generators/schema.ts @@ -46,8 +46,15 @@ export async function generateSchema( // Invoke the content source resolver to retrieve the content nodes let allContentNodes: Record = {}; + let collectionEntriesByName = Object.fromEntries( + config.content.map((collection: LoadedCollectionEntry) => [ + collection.name, + collection, + ]) + ); + const addRecord = - (source: Source) => + (sourceId: string) => ( collection: LoadedCollectionEntry, record: EntryNode, @@ -59,7 +66,7 @@ export async function generateSchema( record, context: { sourceContext: context, - sourcedBy: source.id, + sourcedBy: sourceId, collection: collection.name, referenceField: collection.referenceField ?? 'id', }, @@ -69,8 +76,20 @@ export async function generateSchema( return newRecord; }; + function addCreationRequiredFields( + collection: CollectionEntry, + fields: string[] + ): void { + if (!collectionEntriesByName[collection.name]) + throw new Error(`Couldn't find collection ${collection.name}`); + collectionEntriesByName?.[collection.name]?.creationRequiredFields?.push( + ...fields + ); + } + await config.source.fetch(config.content, { - addRecord: addRecord(config.source), + addRecord: addRecord(config.source.id as string), + addCreationRequiredFields, }); // Transform the content nodes to the expected JSON format if needed @@ -187,9 +206,7 @@ export async function generateSchema( schemaComposer, updateCollectionRecord, config, - collectionEntry: config.content.find( - (c) => c.name === name - ) as CollectionEntry, + collectionEntry: collectionEntriesByName[name], }); } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index e8ccf83f..a74209f9 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -76,6 +76,10 @@ export interface FlatbreadArgs { record: EntryNode, context?: Context ): void; + addCreationRequiredFields( + collection: LoadedCollectionEntry, + fields: string[] + ): void; } export interface Source { @@ -134,10 +138,12 @@ export interface CollectionEntry { overrides?: Override[]; refs?: Record; referenceField?: string; + creationRequiredFields?: string[]; defaultTransformer?: string; defaultSource?: string; } export interface LoadedCollectionEntry extends CollectionEntry { referenceField: string; + creationRequiredFields: string[]; } diff --git a/packages/core/src/utils/initializeConfig.ts b/packages/core/src/utils/initializeConfig.ts index f1b4f2e5..37f57d32 100644 --- a/packages/core/src/utils/initializeConfig.ts +++ b/packages/core/src/utils/initializeConfig.ts @@ -22,7 +22,10 @@ export function initializeConfig( return { ...config, content: config.content?.map((content: Partial) => - defaultsDeep(content, { referenceField: 'id' }) + defaultsDeep(content, { + referenceField: 'id', + creationRequiredFields: [], + }) ), transformer, loaded: { diff --git a/packages/flatbread/README.md b/packages/flatbread/README.md index 4b152a3e..4f6596f9 100644 --- a/packages/flatbread/README.md +++ b/packages/flatbread/README.md @@ -83,6 +83,10 @@ export default defineConfig({ { path: 'content/authors', collection: 'Author', + // the field to use as the primary key, 'id' by default + referenceField: 'id', + // a list of fields that are required when creating a new record (mostly used by plugins) + creationRequiredFields: [] refs: { friend: 'Author', }, diff --git a/packages/source-filesystem/src/index.ts b/packages/source-filesystem/src/index.ts index 75e9c3c8..b4787607 100644 --- a/packages/source-filesystem/src/index.ts +++ b/packages/source-filesystem/src/index.ts @@ -31,7 +31,7 @@ interface Context { */ async function getNodesFromDirectory( collectionEntry: LoadedCollectionEntry, - { addRecord }: FlatbreadArgs, + { addRecord, addCreationRequiredFields }: FlatbreadArgs, config: InitializedSourceFilesystemConfig ): Promise { const { extensions } = config; @@ -39,6 +39,12 @@ async function getNodesFromDirectory( extensions, }); + // collect all the variable path segments [like] [these] + const requiredFields = Array.from( + collectionEntry.path.matchAll(/\[(.*?)\]/g) + ).map((m) => m[1]); + addCreationRequiredFields(collectionEntry, requiredFields); + await Promise.all( nodes.map(async (node: FileNode): Promise => { const doc = await read(node.path); From a18ee39b6fac2492f88a85dc23dd71dd2d1c4cb9 Mon Sep 17 00:00:00 2001 From: Adam Sparks Date: Thu, 11 Aug 2022 01:47:53 -0500 Subject: [PATCH 14/22] expose collectionResolvers to config --- examples/sveltekit/flatbread.config.js | 17 ++++++ .../src/generators/collectionMutations.ts | 53 +++++-------------- .../core/src/generators/collectionQueries.ts | 47 ++++++---------- packages/core/src/generators/schema.ts | 34 ++++++++---- packages/core/src/providers/test/base.test.ts | 42 +++++++++++++++ packages/core/src/types.ts | 19 +++++++ packages/core/src/utils/initializeConfig.ts | 1 + 7 files changed, 134 insertions(+), 79 deletions(-) diff --git a/examples/sveltekit/flatbread.config.js b/examples/sveltekit/flatbread.config.js index 30f03342..21a22d1e 100644 --- a/examples/sveltekit/flatbread.config.js +++ b/examples/sveltekit/flatbread.config.js @@ -16,6 +16,23 @@ const transformerConfig = { export default defineConfig({ source: sourceFilesystem(), transformer: [transformerMarkdown(transformerConfig), transformerYaml()], + + collectionResolvers: [ + function fakeResolver(schemaComposer, args) { + const { name } = args; + + schemaComposer.Query.addFields({ + [`fake${name}`]: { + type: 'String', + description: `fake resolver`, + resolve() { + return `fake ${name}!`; + }, + }, + }); + }, + ], + content: [ { path: 'content/markdown/posts', diff --git a/packages/core/src/generators/collectionMutations.ts b/packages/core/src/generators/collectionMutations.ts index 13cd7ca4..68edf422 100644 --- a/packages/core/src/generators/collectionMutations.ts +++ b/packages/core/src/generators/collectionMutations.ts @@ -1,38 +1,13 @@ import { ObjectTypeComposer, SchemaComposer } from 'graphql-compose'; import { get, merge } from 'lodash-es'; -import { - CollectionContext, - CollectionEntry, - EntryNode, - LoadedCollectionEntry, - LoadedFlatbreadConfig, -} from '../types'; - -export interface AddCollectionMutationsArgs { - name: string; - pluralName: string; - config: LoadedFlatbreadConfig; - objectComposer: ObjectTypeComposer; - schemaComposer: SchemaComposer; - collectionEntry: LoadedCollectionEntry; - updateCollectionRecord: ( - collection: CollectionEntry, - entry: EntryNode & { _metadata: CollectionContext } - ) => Promise; -} +import { CollectionContext, CollectionResolverArgs, EntryNode } from '../types'; export default function addCollectionMutations( - args: AddCollectionMutationsArgs + schemaComposer: SchemaComposer, + args: CollectionResolverArgs ) { - const { - name, - pluralName, - config, - objectComposer, - schemaComposer, - updateCollectionRecord, - collectionEntry, - } = args; + const { name, objectTypeComposer, updateCollectionRecord, collectionEntry } = + args; async function update( payload: Record, @@ -41,7 +16,7 @@ export default function addCollectionMutations( // remove _metadata to prevent injection const { _metadata, ...update } = payload?.[name]; - const targetRecord = objectComposer + const targetRecord = objectTypeComposer .getResolver('findById') .resolve({ args: update }); @@ -76,9 +51,9 @@ export default function addCollectionMutations( schemaComposer.Mutation.addFields({ [`update${name}`]: { - type: objectComposer, + type: objectTypeComposer, args: { - [name]: objectComposer + [name]: objectTypeComposer .getInputTypeComposer() .makeFieldNonNull(collectionEntry.creationRequiredFields), }, @@ -86,7 +61,7 @@ export default function addCollectionMutations( async resolve(source: unknown, payload: Record) { const { _metadata, ...args } = payload?.[name]; - const existingRecord = objectComposer + const existingRecord = objectTypeComposer .getResolver('findById') .resolve({ args }); @@ -102,9 +77,9 @@ export default function addCollectionMutations( update, }, [`create${name}`]: { - type: objectComposer, + type: objectTypeComposer, args: { - [name]: objectComposer + [name]: objectTypeComposer .getInputTypeComposer() .clone(`${name}CreateInput`) .removeField('id') @@ -114,12 +89,12 @@ export default function addCollectionMutations( resolve: create, }, [`upsert${name}`]: { - type: objectComposer, - args: { [name]: objectComposer.getInputTypeComposer() }, + type: objectTypeComposer, + args: { [name]: objectTypeComposer.getInputTypeComposer() }, async resolve(source: unknown, payload: Record) { const { _metadata, ...args } = payload?.[name]; - const existingRecord = objectComposer + const existingRecord = objectTypeComposer .getResolver('findById') .resolve({ args }); diff --git a/packages/core/src/generators/collectionQueries.ts b/packages/core/src/generators/collectionQueries.ts index b5cbabd2..dd4fa887 100644 --- a/packages/core/src/generators/collectionQueries.ts +++ b/packages/core/src/generators/collectionQueries.ts @@ -1,39 +1,26 @@ -import { ObjectTypeComposer, SchemaComposer } from 'graphql-compose'; import resolveQueryArgs from '../resolvers/arguments'; +import { SchemaComposer } from 'graphql-compose'; +import { cloneDeep } from 'lodash-es'; import { generateArgsForAllItemQuery, generateArgsForManyItemQuery, generateArgsForSingleItemQuery, } from '../generators/arguments'; -import { cloneDeep } from 'lodash-es'; -import { EntryNode, LoadedFlatbreadConfig, Transformer } from '../types'; - -export interface AddCollectionQueriesArgs { - name: string; - pluralName: string; - config: LoadedFlatbreadConfig; - objectComposer: ObjectTypeComposer; - schemaComposer: SchemaComposer; - allContentNodesJSON: Record; - transformersById: Record; -} +import { CollectionResolverArgs, EntryNode } from '../types'; -export default function addCollectionQueries(args: AddCollectionQueriesArgs) { - const { - name, - pluralName, - config, - objectComposer, - schemaComposer, - allContentNodesJSON, - } = args; +export default function addCollectionQueries( + schemaComposer: SchemaComposer, + args: CollectionResolverArgs & { allContentNodesJSON: Record } +) { + const { name, pluralName, config, objectTypeComposer, allContentNodesJSON } = + args; const pluralTypeQueryName = 'all' + pluralName; - objectComposer.addResolver({ + objectTypeComposer.addResolver({ name: 'findById', - type: () => objectComposer, + type: () => objectTypeComposer, description: `Find one ${name} by its ID`, args: generateArgsForSingleItemQuery(), resolve: (rp: Record) => @@ -44,9 +31,9 @@ export default function addCollectionQueries(args: AddCollectionQueriesArgs) { ), }); - objectComposer.addResolver({ + objectTypeComposer.addResolver({ name: 'findMany', - type: () => [objectComposer], + type: () => [objectTypeComposer], description: `Find many ${pluralName} by their IDs`, args: generateArgsForManyItemQuery(pluralName), resolve: (rp: Record) => { @@ -65,10 +52,10 @@ export default function addCollectionQueries(args: AddCollectionQueriesArgs) { }, }); - objectComposer.addResolver({ + objectTypeComposer.addResolver({ name: 'all', args: generateArgsForAllItemQuery(pluralName), - type: () => [objectComposer], + type: () => [objectTypeComposer], description: `Return a set of ${pluralName}`, resolve: (rp: Record) => { const nodes = cloneDeep(allContentNodesJSON[name]); @@ -86,10 +73,10 @@ export default function addCollectionQueries(args: AddCollectionQueriesArgs) { /** * Add find by ID to each content type */ - [name]: objectComposer.getResolver('findById'), + [name]: objectTypeComposer.getResolver('findById'), /** * Add find 'many' to each content type */ - [pluralTypeQueryName]: objectComposer.getResolver('all'), + [pluralTypeQueryName]: objectTypeComposer.getResolver('all'), }); } diff --git a/packages/core/src/generators/schema.ts b/packages/core/src/generators/schema.ts index 34909ca2..c787ccbf 100644 --- a/packages/core/src/generators/schema.ts +++ b/packages/core/src/generators/schema.ts @@ -168,14 +168,14 @@ export async function generateSchema( } // Main builder loop - iterate through each content type and generate query resolvers + relationships for it - for (const [name, objectComposer] of Object.entries(schemaArray)) { + for (const [name, objectTypeComposer] of Object.entries(schemaArray)) { const pluralName = plur(name, 2); // /// Global meta fields // - objectComposer.addFields({ + objectTypeComposer.addFields({ _collection: { type: 'String', description: 'The collection name', @@ -189,25 +189,39 @@ export async function generateSchema( // TODO: add a new type of plugin that can add resolvers to each collection, they should be called here - addCollectionQueries({ + const collectionEntry = collectionEntriesByName[name]; + + addCollectionQueries(schemaComposer, { name, pluralName, - objectComposer, - schemaComposer, - transformersById, allContentNodesJSON, + updateCollectionRecord, + objectTypeComposer, config, + collectionEntry, }); - addCollectionMutations({ + addCollectionMutations(schemaComposer, { name, pluralName, - objectComposer, - schemaComposer, + objectTypeComposer, updateCollectionRecord, config, - collectionEntry: collectionEntriesByName[name], + collectionEntry, }); + + await Promise.all( + config.collectionResolvers.map((collectionResolver) => + collectionResolver(schemaComposer, { + name, + pluralName, + objectTypeComposer, + updateCollectionRecord, + config, + collectionEntry, + }) + ) + ); } // Create map of references on each content node diff --git a/packages/core/src/providers/test/base.test.ts b/packages/core/src/providers/test/base.test.ts index 11823df2..3cec314b 100644 --- a/packages/core/src/providers/test/base.test.ts +++ b/packages/core/src/providers/test/base.test.ts @@ -3,6 +3,7 @@ import test from 'ava'; import { SourceVirtual } from '../../sources/virtual'; import { FlatbreadProvider } from '../base'; import { mockData } from './mockData'; +import { CollectionResolverArgs } from '../../types'; const sourceVirtual = new SourceVirtual(mockData); @@ -28,6 +29,47 @@ function basicProject() { }); } +test('create custom collection resolver', async (t) => { + const flatbread = await new FlatbreadProvider({ + source: sourceVirtual, + transformer: markdownTransformer({ + markdown: { + gfm: true, + externalLinks: true, + }, + }), + + collectionResolvers: [ + function fakeResolver(schemaComposer, args) { + const { name } = args; + + schemaComposer.Query.addFields({ + [`fake${name}`]: { + type: 'String', + description: `fake resolver`, + resolve() { + return `fake ${name}!`; + }, + }, + }); + }, + ], + + content: [ + { + path: 'examples/content/markdown/authors', + name: 'Author', + refs: { + friend: 'Author', + }, + }, + ], + }); + + const result: any = await flatbread.query({ source: `{ fakeAuthor }`}); + t.is(result.data.fakeAuthor, 'fake Author!'); +}); + test('basic query', async (t) => { const flatbread = basicProject(); diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index a74209f9..1f12ffbf 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -1,4 +1,5 @@ import { GraphQLFieldConfigArgumentMap, GraphQLInputType } from 'graphql'; +import { ObjectTypeComposer, SchemaComposer } from 'graphql-compose'; import { Maybe } from 'graphql/jsutils/Maybe'; import type { VFile } from 'vfile'; @@ -24,12 +25,30 @@ export interface FlatbreadConfig { source: Source; transformer?: Transformer | Transformer[]; content: CollectionEntry[]; + collectionResolvers?: CollectionResolver[]; } +export interface CollectionResolverArgs { + name: string; + pluralName: string; + config: LoadedFlatbreadConfig; + objectTypeComposer: ObjectTypeComposer; + collectionEntry: LoadedCollectionEntry; + updateCollectionRecord: ( + collection: CollectionEntry, + entry: EntryNode & { _metadata: CollectionContext } + ) => Promise; +} + +export type CollectionResolver = (schemaComposer: SchemaComposer, + args: CollectionResolverArgs +) => void | Promise; + export interface LoadedFlatbreadConfig { source: Source; transformer: Transformer[]; content: LoadedCollectionEntry[]; + collectionResolvers: CollectionResolver[]; loaded: { extensions: string[]; }; diff --git a/packages/core/src/utils/initializeConfig.ts b/packages/core/src/utils/initializeConfig.ts index 37f57d32..de5ac3f9 100644 --- a/packages/core/src/utils/initializeConfig.ts +++ b/packages/core/src/utils/initializeConfig.ts @@ -21,6 +21,7 @@ export function initializeConfig( return { ...config, + collectionResolvers: config.collectionResolvers || [], content: config.content?.map((content: Partial) => defaultsDeep(content, { referenceField: 'id', From 259d9402f782dba231bf1c2f32530b09b15ef24b Mon Sep 17 00:00:00 2001 From: Adam Sparks Date: Thu, 11 Aug 2022 01:52:26 -0500 Subject: [PATCH 15/22] prettier --- packages/core/src/providers/test/base.test.ts | 2 +- packages/core/src/types.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/core/src/providers/test/base.test.ts b/packages/core/src/providers/test/base.test.ts index 3cec314b..0ca5ff82 100644 --- a/packages/core/src/providers/test/base.test.ts +++ b/packages/core/src/providers/test/base.test.ts @@ -66,7 +66,7 @@ test('create custom collection resolver', async (t) => { ], }); - const result: any = await flatbread.query({ source: `{ fakeAuthor }`}); + const result: any = await flatbread.query({ source: `{ fakeAuthor }` }); t.is(result.data.fakeAuthor, 'fake Author!'); }); diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 1f12ffbf..da36a2c4 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -40,7 +40,8 @@ export interface CollectionResolverArgs { ) => Promise; } -export type CollectionResolver = (schemaComposer: SchemaComposer, +export type CollectionResolver = ( + schemaComposer: SchemaComposer, args: CollectionResolverArgs ) => void | Promise; From fe429617a9dee8286b3514f87e937c7d07553865 Mon Sep 17 00:00:00 2001 From: Adam Sparks Date: Thu, 11 Aug 2022 01:57:20 -0500 Subject: [PATCH 16/22] test husky --- packages/core/src/providers/test/base.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/providers/test/base.test.ts b/packages/core/src/providers/test/base.test.ts index 0ca5ff82..c365b7d9 100644 --- a/packages/core/src/providers/test/base.test.ts +++ b/packages/core/src/providers/test/base.test.ts @@ -17,6 +17,7 @@ function basicProject() { }, }), + content: [ { path: 'examples/content/markdown/authors', From 40f4c2682b93325a5ba75ec795041fb1f83d53e3 Mon Sep 17 00:00:00 2001 From: Adam Sparks Date: Thu, 11 Aug 2022 01:59:06 -0500 Subject: [PATCH 17/22] test husky --- packages/core/src/providers/test/base.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/src/providers/test/base.test.ts b/packages/core/src/providers/test/base.test.ts index c365b7d9..0ca5ff82 100644 --- a/packages/core/src/providers/test/base.test.ts +++ b/packages/core/src/providers/test/base.test.ts @@ -17,7 +17,6 @@ function basicProject() { }, }), - content: [ { path: 'examples/content/markdown/authors', From 02bc50f73f319d4d538b70720284cf32060ebdec Mon Sep 17 00:00:00 2001 From: Adam Sparks Date: Sat, 13 Aug 2022 01:37:33 -0500 Subject: [PATCH 18/22] remove stray arg --- packages/core/src/generators/collectionMutations.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/src/generators/collectionMutations.ts b/packages/core/src/generators/collectionMutations.ts index 68edf422..f66dab2f 100644 --- a/packages/core/src/generators/collectionMutations.ts +++ b/packages/core/src/generators/collectionMutations.ts @@ -74,7 +74,6 @@ export default function addCollectionMutations( ); return update(payload, existingRecord); }, - update, }, [`create${name}`]: { type: objectTypeComposer, From 20158dff588ba7f4f01d96ebb0e1f4fc8d27d335 Mon Sep 17 00:00:00 2001 From: Adam Sparks Date: Tue, 16 Aug 2022 02:21:02 -0500 Subject: [PATCH 19/22] migrate sveltekit --- .../{__layout.svelte => +layout.svelte} | 0 examples/sveltekit/src/routes/+page.svelte | 54 +++++++ examples/sveltekit/src/routes/+page.ts | 79 ++++++++++ examples/sveltekit/src/routes/index.svelte | 143 ------------------ pnpm-lock.yaml | 97 +++++++++++- 5 files changed, 226 insertions(+), 147 deletions(-) rename examples/sveltekit/src/routes/{__layout.svelte => +layout.svelte} (100%) create mode 100644 examples/sveltekit/src/routes/+page.svelte create mode 100644 examples/sveltekit/src/routes/+page.ts delete mode 100644 examples/sveltekit/src/routes/index.svelte diff --git a/examples/sveltekit/src/routes/__layout.svelte b/examples/sveltekit/src/routes/+layout.svelte similarity index 100% rename from examples/sveltekit/src/routes/__layout.svelte rename to examples/sveltekit/src/routes/+layout.svelte diff --git a/examples/sveltekit/src/routes/+page.svelte b/examples/sveltekit/src/routes/+page.svelte new file mode 100644 index 00000000..258d4680 --- /dev/null +++ b/examples/sveltekit/src/routes/+page.svelte @@ -0,0 +1,54 @@ + + +
+ +
+      
+        {JSON.stringify(data, null, 2)}
+      
+    
+
+ + {#each data.allPostCategories as post, _ (post.id)} +
+

{post.title}

+
    +
  • +
    + {#each post.authors as author} +
    +
    + +
    + {author.name} +
    + {/each} +
    +
  • +
  • + Rating: {post.rating} +
  • +
+
{@html post._content.html}
+
+ {/each} +
+
diff --git a/examples/sveltekit/src/routes/+page.ts b/examples/sveltekit/src/routes/+page.ts new file mode 100644 index 00000000..63210f5b --- /dev/null +++ b/examples/sveltekit/src/routes/+page.ts @@ -0,0 +1,79 @@ +import { error } from '@sveltejs/kit'; + +export const load = async ({ fetch }) => { + const query = ` + query PostCategory { + allPostCategories (sortBy: "title", order: DESC) { + _metadata { + sourceContext { + filename + slug + } + collection + } + id + title + category + slug + rating + _content { + raw + html + excerpt + timeToRead + } + authors { + _metadata { + sourceContext { + slug + } + } + id + name + entity + enjoys + image { + srcset + srcsetwebp + srcsetavif + placeholder + aspectratio + } + friend { + name + date_joined + } + date_joined + skills { + sitting + breathing + liquid_consumption + existence + sports + } + } + } + } + `; + + try { + const response = await fetch('http://localhost:5057/graphql', { + body: JSON.stringify({ + query, + }), + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + method: 'POST', + }); + + const { data, errors } = await response.json(); + + if (errors) + throw error(500, errors.map(({ message }) => message).join('\\n')); + return data; + } catch (e) { + throw error(500, 'Failed to load data'); + } +}; diff --git a/examples/sveltekit/src/routes/index.svelte b/examples/sveltekit/src/routes/index.svelte deleted file mode 100644 index 53c0ffa9..00000000 --- a/examples/sveltekit/src/routes/index.svelte +++ /dev/null @@ -1,143 +0,0 @@ - - - - -
- -
-      
-        {JSON.stringify(data, null, 2)}
-      
-    
-
- - {#each data.allPostCategories as post, _ (post.id)} -
-

{post.title}

-
    -
  • -
    - {#each post.authors as author} -
    -
    - -
    - {author.name} -
    - {/each} -
    -
  • -
  • - Rating: {post.rating} -
  • -
-
{@html post._content.html}
-
- {/each} -
-
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index afce7949..e66f005a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -111,7 +111,7 @@ importers: devDependencies: '@flatbread/transformer-yaml': link:../../packages/transformer-yaml '@sveltejs/adapter-static': 1.0.0-next.39 - '@sveltejs/kit': 1.0.0-next.405_svelte@3.49.0+vite@3.0.4 + '@sveltejs/kit': 1.0.0-next.408_svelte@3.49.0+vite@3.0.4 '@typescript-eslint/eslint-plugin': 4.33.0_3ekaj7j3owlolnuhj3ykrb7u7i '@typescript-eslint/parser': 4.33.0_hxadhbs2xogijvk7vq4t2azzbu autoprefixer: 10.4.7_postcss@8.4.14 @@ -1464,6 +1464,10 @@ packages: typescript: 4.7.4 dev: true + /@polka/url/1.0.0-next.21: + resolution: {integrity: sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==} + dev: true + /@protobufjs/aspromise/1.1.2: resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} dev: false @@ -1578,8 +1582,8 @@ packages: resolution: {integrity: sha512-EeD39H6iEe0UEKnKxLFTZFZpi/FcX5xfbAvsMQ+B09aDZccpQmkJBSIo+4kq1JsQGSjwi/+J3aE9bR67R6CIyQ==} dev: true - /@sveltejs/kit/1.0.0-next.405_svelte@3.49.0+vite@3.0.4: - resolution: {integrity: sha512-jHSa74F7k+hC+0fof75g/xm/+1M5sM66Qt6v8eLLMSgjkp36Lb5xOioBhbl6w0NYoE5xysLsBWuu+yHytfvCBA==} + /@sveltejs/kit/1.0.0-next.408_svelte@3.49.0+vite@3.0.4: + resolution: {integrity: sha512-AOSa0o7EJnFN56IptIMGvcbLNPlGdFKtzq0RYnYnzJAvtAocWrNdAL26gIFj2f6iYdBDRR6mm/a8X1yCQ9mCdA==} engines: {node: '>=16.9'} hasBin: true requiresBuild: true @@ -1589,9 +1593,18 @@ packages: dependencies: '@sveltejs/vite-plugin-svelte': 1.0.1_svelte@3.49.0+vite@3.0.4 chokidar: 3.5.3 + cookie: 0.5.0 + devalue: 2.0.1 + kleur: 4.1.5 + magic-string: 0.26.2 + mime: 3.0.0 + node-fetch: 3.2.10 sade: 1.8.1 + set-cookie-parser: 2.5.1 + sirv: 2.0.2 svelte: 3.49.0 tiny-glob: 0.2.9 + undici: 5.8.2 vite: 3.0.4 transitivePeerDependencies: - diff-match-patch @@ -3166,7 +3179,6 @@ packages: /cookie/0.5.0: resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} engines: {node: '>= 0.6'} - dev: false /core-js-pure/3.23.5: resolution: {integrity: sha512-8t78LdpKSuCq4pJYCYk8hl7XEkAX+BP16yRIwL3AanTksxuEf7CM83vRyctmiEL8NDZ3jpUcv56fk9/zG3aIuw==} @@ -3247,6 +3259,11 @@ packages: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} dev: true + /data-uri-to-buffer/4.0.0: + resolution: {integrity: sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==} + engines: {node: '>= 12'} + dev: true + /data-urls/2.0.0: resolution: {integrity: sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==} engines: {node: '>=10'} @@ -3413,6 +3430,10 @@ packages: minimist: 1.2.6 dev: true + /devalue/2.0.1: + resolution: {integrity: sha512-I2TiqT5iWBEyB8GRfTDP0hiLZ0YeDJZ+upDxjBfOC2lebO5LezQMv7QvIUTzdb64jQyAKLf1AHADtGN+jw6v8Q==} + dev: true + /didyoumean/1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} dev: true @@ -4983,6 +5004,14 @@ packages: bser: 2.1.1 dev: true + /fetch-blob/3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.2.1 + dev: true + /figures/3.2.0: resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} engines: {node: '>=8'} @@ -5091,6 +5120,13 @@ packages: engines: {node: '>=0.4.x'} dev: false + /formdata-polyfill/4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + dependencies: + fetch-blob: 3.2.0 + dev: true + /forwarded/0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -7184,6 +7220,12 @@ packages: hasBin: true dev: false + /mime/3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + dev: true + /mimic-fn/2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -7239,6 +7281,11 @@ packages: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} + /mrmime/1.0.1: + resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==} + engines: {node: '>=10'} + dev: true + /ms/2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} @@ -7352,6 +7399,11 @@ packages: /node-addon-api/5.0.0: resolution: {integrity: sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA==} + /node-domexception/1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + dev: true + /node-emoji/1.11.0: resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} dependencies: @@ -7370,6 +7422,15 @@ packages: whatwg-url: 5.0.0 dev: false + /node-fetch/3.2.10: + resolution: {integrity: sha512-MhuzNwdURnZ1Cp4XTazr69K0BTizsBroX7Zx3UgDSVcZYKF/6p0CBe4EUb/hLqmzVhl0UpYfgRljQ4yxE+iCxA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + data-uri-to-buffer: 4.0.0 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + dev: true + /node-gyp-build/4.5.0: resolution: {integrity: sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg==} hasBin: true @@ -8559,6 +8620,10 @@ packages: - supports-color dev: false + /set-cookie-parser/2.5.1: + resolution: {integrity: sha512-1jeBGaKNGdEq4FgIrORu/N570dwoPYio8lSoYLWmX7sQ//0JY08Xh9o5pBcgmHQ/MbsYp/aZnOe1s1lIsbLprQ==} + dev: true + /setprototypeof/1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} dev: false @@ -8623,6 +8688,15 @@ packages: dependencies: is-arrayish: 0.3.2 + /sirv/2.0.2: + resolution: {integrity: sha512-4Qog6aE29nIjAOKe/wowFTxOdmbEZKb+3tsLljaBRzJwtqto0BChD2zzH0LhgCSXiI+V7X+Y45v14wBZQ1TK3w==} + engines: {node: '>= 10'} + dependencies: + '@polka/url': 1.0.0-next.21 + mrmime: 1.0.1 + totalist: 3.0.0 + dev: true + /sisteransi/1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} dev: true @@ -9241,6 +9315,11 @@ packages: engines: {node: '>=0.6'} dev: false + /totalist/3.0.0: + resolution: {integrity: sha512-eM+pCBxXO/njtF7vdFsHuqb+ElbxqtI4r5EAvk6grfAFyJ6IvWlSkfZ5T9ozC6xWw3Fj1fGoSmrl0gUs46JVIw==} + engines: {node: '>=6'} + dev: true + /tough-cookie/4.0.0: resolution: {integrity: sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==} engines: {node: '>=6'} @@ -9501,6 +9580,11 @@ packages: which-boxed-primitive: 1.0.2 dev: true + /undici/5.8.2: + resolution: {integrity: sha512-3KLq3pXMS0Y4IELV045fTxqz04Nk9Ms7yfBBHum3yxsTR4XNn+ZCaUbf/mWitgYDAhsplQ0B1G4S5D345lMO3A==} + engines: {node: '>=12.18'} + dev: true + /unified/10.1.2: resolution: {integrity: sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==} dependencies: @@ -9758,6 +9842,11 @@ packages: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} dev: false + /web-streams-polyfill/3.2.1: + resolution: {integrity: sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==} + engines: {node: '>= 8'} + dev: true + /webidl-conversions/3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} dev: false From 215eccea084673f1956887fd1ade2632be9de2df Mon Sep 17 00:00:00 2001 From: Adam Sparks Date: Tue, 16 Aug 2022 04:06:16 -0500 Subject: [PATCH 20/22] add error messages, prevent creating record with existing id, use referenceField with filtering --- examples/content/markdown/authors/Adam.md | 9 ++ examples/content/markdown/authors/Amanda.md | 8 ++ examples/content/markdown/authors/daes.md | 2 +- examples/content/markdown/authors/eva.md | 2 +- examples/content/markdown/authors/tony.md | 2 +- examples/content/markdown/authors/yoshi.md | 2 +- examples/content/markdown/posts/anotha-one.md | 4 +- examples/content/markdown/posts/b.md | 4 +- .../content/markdown/posts/example-post.md | 4 +- examples/content/markdown/posts/soup.md | 3 +- examples/sveltekit/flatbread.config.js | 1 + packages/core/src/errors.ts | 21 +++- packages/core/src/generators/arguments.ts | 12 +- .../src/generators/collectionMutations.ts | 42 ++++--- .../core/src/generators/collectionQueries.ts | 36 ++++-- packages/core/src/generators/schema.ts | 18 +-- packages/core/src/providers/test/base.test.ts | 22 +++- .../providers/test/snapshots/base.test.ts.md | 44 +++++++- .../test/snapshots/base.test.ts.snap | Bin 576 -> 836 bytes packages/core/src/resolvers/arguments.ts | 33 ++++-- packages/flatbread/README.md | 35 ++++++ .../snapshots/gatherFileNodes.test.ts.md | 104 ------------------ .../snapshots/gatherFileNodes.test.ts.snap | Bin 1170 -> 964 bytes 23 files changed, 226 insertions(+), 182 deletions(-) create mode 100644 examples/content/markdown/authors/Adam.md create mode 100644 examples/content/markdown/authors/Amanda.md diff --git a/examples/content/markdown/authors/Adam.md b/examples/content/markdown/authors/Adam.md new file mode 100644 index 00000000..606e93d2 --- /dev/null +++ b/examples/content/markdown/authors/Adam.md @@ -0,0 +1,9 @@ +--- +name: Adam +friend: Amanda +enjoys: + - cats + - coding + - flatbread +--- + diff --git a/examples/content/markdown/authors/Amanda.md b/examples/content/markdown/authors/Amanda.md new file mode 100644 index 00000000..e2c14b4c --- /dev/null +++ b/examples/content/markdown/authors/Amanda.md @@ -0,0 +1,8 @@ +--- +name: Amanda +enjoys: + - cats + - coding + - flatbread +--- + diff --git a/examples/content/markdown/authors/daes.md b/examples/content/markdown/authors/daes.md index 029219a5..3de9c976 100644 --- a/examples/content/markdown/authors/daes.md +++ b/examples/content/markdown/authors/daes.md @@ -6,7 +6,7 @@ enjoys: - cats - coffee - design -friend: 40s3 +friend: Eva date_joined: 2021-04-22T16:41:59.558Z skills: sitting: 304 diff --git a/examples/content/markdown/authors/eva.md b/examples/content/markdown/authors/eva.md index 52b4e0d8..6432e934 100644 --- a/examples/content/markdown/authors/eva.md +++ b/examples/content/markdown/authors/eva.md @@ -8,7 +8,7 @@ enjoys: - mow mow - sleepy time - attention -friend: 2a3e +friend: Tony image: eva.svg date_joined: 2002-02-25T16:41:59.558Z skills: diff --git a/examples/content/markdown/authors/tony.md b/examples/content/markdown/authors/tony.md index 416dd8ba..1cdf4d44 100644 --- a/examples/content/markdown/authors/tony.md +++ b/examples/content/markdown/authors/tony.md @@ -6,7 +6,7 @@ enjoys: - cats - tea - making this -friend: 40s3 +friend: Eva image: tony.svg date_joined: 2021-02-25T16:41:59.558Z skills: diff --git a/examples/content/markdown/authors/yoshi.md b/examples/content/markdown/authors/yoshi.md index 047f60fe..fbcd95c4 100644 --- a/examples/content/markdown/authors/yoshi.md +++ b/examples/content/markdown/authors/yoshi.md @@ -7,7 +7,7 @@ enjoys: - encroaching upon personal space - being concerned - smooth jazz -friend: ab2c +friend: Daes date_joined: 2018-10-25T16:23:59.558Z skills: sitting: 10 diff --git a/examples/content/markdown/posts/anotha-one.md b/examples/content/markdown/posts/anotha-one.md index 71175a86..358649bd 100644 --- a/examples/content/markdown/posts/anotha-one.md +++ b/examples/content/markdown/posts/anotha-one.md @@ -2,8 +2,8 @@ id: 92348fds-453fdh-59ddsd-3332-09876 title: 'Test post A' authors: - - 40s3 - - 2a3e + - Eva + - Tony rating: 84.3 --- diff --git a/examples/content/markdown/posts/b.md b/examples/content/markdown/posts/b.md index 4287b935..4ec67703 100644 --- a/examples/content/markdown/posts/b.md +++ b/examples/content/markdown/posts/b.md @@ -2,8 +2,8 @@ id: 2348fds-563fdh-59ddsd-3332-09876 title: 'Test post B' authors: - - 1111 - - ab2c + - Ushi + - Daes rating: 44 --- diff --git a/examples/content/markdown/posts/example-post.md b/examples/content/markdown/posts/example-post.md index c57b41f8..7e08b8fd 100644 --- a/examples/content/markdown/posts/example-post.md +++ b/examples/content/markdown/posts/example-post.md @@ -2,8 +2,8 @@ id: sdfsdf-23423-sdfsd-23444-dfghf title: 'Example post of things' authors: - - 2a3e - - 40s3 + - Tony + - Eva rating: 74 --- diff --git a/examples/content/markdown/posts/soup.md b/examples/content/markdown/posts/soup.md index 1425e759..0ed9154b 100644 --- a/examples/content/markdown/posts/soup.md +++ b/examples/content/markdown/posts/soup.md @@ -2,8 +2,7 @@ id: jksfd4-234fdh-5345fj-3455-09836 title: 'Soup Reflection' authors: - - r3c6 - - ab2c + - Yoshi rating: 96 --- diff --git a/examples/sveltekit/flatbread.config.js b/examples/sveltekit/flatbread.config.js index 30f03342..4bdc882c 100644 --- a/examples/sveltekit/flatbread.config.js +++ b/examples/sveltekit/flatbread.config.js @@ -34,6 +34,7 @@ export default defineConfig({ { path: 'content/markdown/authors', name: 'Author', + referenceField: 'name', refs: { friend: 'Author', }, diff --git a/packages/core/src/errors.ts b/packages/core/src/errors.ts index 106e5e96..c66fbf55 100644 --- a/packages/core/src/errors.ts +++ b/packages/core/src/errors.ts @@ -2,8 +2,7 @@ import { outdent } from './utils/outdent'; export class IllegalFieldNameError extends Error { constructor(illegalSequence: string) { - super(); - this.message = outdent` + super(outdent` The sequence "${illegalSequence}" is reserved and not allowed in field names Either: - remove all instances of "${illegalSequence}" in the names of fields in your content @@ -13,6 +12,22 @@ export class IllegalFieldNameError extends Error { ..., fieldNameTransform: (value) => value.replaceAll("${illegalSequence}",'-') } - `; + `); } } + +export class ReferenceAlreadyExistsError> extends Error { + constructor( + payload: K, + collectionName: string, + metadata: { referenceField: string; reference: string } + ) { + const payloadString = JSON.stringify(payload, null, 2); + super( + outdent` + Failed to create + ${payloadString} + ${collectionName} with ${metadata.referenceField} of ${metadata.reference} already exists` + ); + } +} \ No newline at end of file diff --git a/packages/core/src/generators/arguments.ts b/packages/core/src/generators/arguments.ts index f6c1ded9..818d636a 100644 --- a/packages/core/src/generators/arguments.ts +++ b/packages/core/src/generators/arguments.ts @@ -17,7 +17,7 @@ export const generateArgsForAllItemQuery = (pluralType: string) => ({ * @param pluralType plural name of the content type */ export const generateArgsForManyItemQuery = (pluralType: string) => ({ - ids: { + references: { type: '[String]', }, ...skip(), @@ -26,16 +26,6 @@ export const generateArgsForManyItemQuery = (pluralType: string) => ({ ...sortBy(pluralType), }); -/** - * Generates the accepted arguments for a 'single-item' query on a content type. - * - */ -export const generateArgsForSingleItemQuery = () => ({ - id: { - type: 'String', - }, -}); - /** * Argument for skipping the first `n` items from the query results. */ diff --git a/packages/core/src/generators/collectionMutations.ts b/packages/core/src/generators/collectionMutations.ts index 13cd7ca4..e8f967e5 100644 --- a/packages/core/src/generators/collectionMutations.ts +++ b/packages/core/src/generators/collectionMutations.ts @@ -1,5 +1,6 @@ import { ObjectTypeComposer, SchemaComposer } from 'graphql-compose'; import { get, merge } from 'lodash-es'; +import { ReferenceAlreadyExistsError } from '../errors'; import { CollectionContext, CollectionEntry, @@ -17,17 +18,15 @@ export interface AddCollectionMutationsArgs { collectionEntry: LoadedCollectionEntry; updateCollectionRecord: ( collection: CollectionEntry, - entry: EntryNode & { _metadata: CollectionContext } + entry: EntryNode & { _metadata: Partial } ) => Promise; } export default function addCollectionMutations( args: AddCollectionMutationsArgs -) { +): void { const { name, - pluralName, - config, objectComposer, schemaComposer, updateCollectionRecord, @@ -41,22 +40,35 @@ export default function addCollectionMutations( // remove _metadata to prevent injection const { _metadata, ...update } = payload?.[name]; - const targetRecord = objectComposer - .getResolver('findById') - .resolve({ args: update }); - // remove supplied key (might not be required) - delete update[targetRecord._metadata.referenceField]; - const newRecord = merge(targetRecord, update); + delete update[existing._metadata.referenceField]; + const newRecord = merge(existing, update); - await updateCollectionRecord(collectionEntry, newRecord); + await updateCollectionRecord( + collectionEntry, + newRecord as EntryNode & { _metadata: Partial } + ); return newRecord; } async function create(source: unknown, payload: Record) { + const existingRecordWithId = await objectComposer + .getResolver('findByReferenceField') + .resolve({ args: payload[name] }); + + if (existingRecordWithId) { + throw new ReferenceAlreadyExistsError( + payload[name], + name, + existingRecordWithId._metadata + ); + } + collectionEntry.creationRequiredFields.forEach((field) => { - if (!payload[name].hasOwnProperty(field)) + // const + + if (Object.hasOwn(payload[name], field)) throw new Error( `field ${field} is required when creating a new ${name}` ); @@ -87,7 +99,7 @@ export default function addCollectionMutations( const { _metadata, ...args } = payload?.[name]; const existingRecord = objectComposer - .getResolver('findById') + .getResolver('findByReferenceField') .resolve({ args }); if (!existingRecord) @@ -106,8 +118,6 @@ export default function addCollectionMutations( args: { [name]: objectComposer .getInputTypeComposer() - .clone(`${name}CreateInput`) - .removeField('id') .makeFieldNonNull(collectionEntry.creationRequiredFields), }, description: `Create a ${name}`, @@ -120,7 +130,7 @@ export default function addCollectionMutations( const { _metadata, ...args } = payload?.[name]; const existingRecord = objectComposer - .getResolver('findById') + .getResolver('findByReferenceField') .resolve({ args }); if (existingRecord) return update(payload, existingRecord); diff --git a/packages/core/src/generators/collectionQueries.ts b/packages/core/src/generators/collectionQueries.ts index b5cbabd2..462c5484 100644 --- a/packages/core/src/generators/collectionQueries.ts +++ b/packages/core/src/generators/collectionQueries.ts @@ -1,13 +1,17 @@ import { ObjectTypeComposer, SchemaComposer } from 'graphql-compose'; import resolveQueryArgs from '../resolvers/arguments'; +import { cloneDeep, get } from 'lodash-es'; import { generateArgsForAllItemQuery, generateArgsForManyItemQuery, - generateArgsForSingleItemQuery, } from '../generators/arguments'; -import { cloneDeep } from 'lodash-es'; -import { EntryNode, LoadedFlatbreadConfig, Transformer } from '../types'; +import { + EntryNode, + LoadedCollectionEntry, + LoadedFlatbreadConfig, + Transformer, +} from '../types'; export interface AddCollectionQueriesArgs { name: string; @@ -17,6 +21,7 @@ export interface AddCollectionQueriesArgs { schemaComposer: SchemaComposer; allContentNodesJSON: Record; transformersById: Record; + collectionEntry: LoadedCollectionEntry; } export default function addCollectionQueries(args: AddCollectionQueriesArgs) { @@ -26,20 +31,27 @@ export default function addCollectionQueries(args: AddCollectionQueriesArgs) { config, objectComposer, schemaComposer, + collectionEntry, allContentNodesJSON, } = args; const pluralTypeQueryName = 'all' + pluralName; objectComposer.addResolver({ - name: 'findById', + name: 'findByReferenceField', type: () => objectComposer, - description: `Find one ${name} by its ID`, - args: generateArgsForSingleItemQuery(), + description: `Find one ${name} by its ${collectionEntry.referenceField}`, + args: { + [collectionEntry.referenceField]: objectComposer + .getInputTypeComposer() + .getField(collectionEntry.referenceField), + }, resolve: (rp: Record) => cloneDeep( allContentNodesJSON[name].find( - (node: EntryNode) => node.id === rp.args.id + (node: EntryNode) => + node[collectionEntry.referenceField] === + rp.args[collectionEntry.referenceField] ) ), }); @@ -47,15 +59,16 @@ export default function addCollectionQueries(args: AddCollectionQueriesArgs) { objectComposer.addResolver({ name: 'findMany', type: () => [objectComposer], - description: `Find many ${pluralName} by their IDs`, + description: `Find many ${pluralName} by their ${collectionEntry.referenceField}`, args: generateArgsForManyItemQuery(pluralName), resolve: (rp: Record) => { - const idsToFind = rp.args.ids ?? []; + const referencesToFind = rp.args.references ?? []; const matches = cloneDeep(allContentNodesJSON[name])?.filter((node: EntryNode) => - idsToFind?.includes(node.id) + referencesToFind?.includes(get(node, collectionEntry.referenceField)) ) ?? []; return resolveQueryArgs(matches, rp.args, config, { + collectionEntry, type: { name: name, pluralName: pluralName, @@ -73,6 +86,7 @@ export default function addCollectionQueries(args: AddCollectionQueriesArgs) { resolve: (rp: Record) => { const nodes = cloneDeep(allContentNodesJSON[name]); return resolveQueryArgs(nodes, rp.args, config, { + collectionEntry, type: { name: name, pluralName: pluralName, @@ -86,7 +100,7 @@ export default function addCollectionQueries(args: AddCollectionQueriesArgs) { /** * Add find by ID to each content type */ - [name]: objectComposer.getResolver('findById'), + [name]: objectComposer.getResolver('findByReferenceField'), /** * Add find 'many' to each content type */ diff --git a/packages/core/src/generators/schema.ts b/packages/core/src/generators/schema.ts index 9ecfb487..351ce057 100644 --- a/packages/core/src/generators/schema.ts +++ b/packages/core/src/generators/schema.ts @@ -1,6 +1,6 @@ import { schemaComposer } from 'graphql-compose'; import { composeWithJson } from 'graphql-compose-json'; -import { defaultsDeep, get, merge, set } from 'lodash-es'; +import { get, merge, set } from 'lodash-es'; import plur from 'plur'; import { VFile } from 'vfile'; import { cacheSchema, checkCacheForSchema } from '../cache/cache'; @@ -11,15 +11,13 @@ import { EntryNode, LoadedCollectionEntry, LoadedFlatbreadConfig, - Source, Transformer, } from '../types'; import { createUniqueId } from '../utils/createUniqueId'; -import { getFieldOverrides } from '../utils/fieldOverrides'; -import { generateCollection } from './generateCollection'; import { map } from '../utils/map'; import addCollectionMutations from './collectionMutations'; import addCollectionQueries from './collectionQueries'; +import { generateCollection } from './generateCollection'; /** * Generates a GraphQL schema from content nodes. @@ -151,8 +149,9 @@ export async function generateSchema( // replace in memory representation of record allContentNodesJSON[ctx.collection][index] = entry; } else { - entry._metadata.reference = createUniqueId(); - set(entry, entry._metadata.referenceField, entry._metadata.reference); + const reference = get(entry, entry._metadata.referenceField); + entry._metadata.reference = reference ?? createUniqueId(); + if (!reference) set(entry, entry._metadata.referenceField, entry._metadata.reference); entry._metadata.transformedBy = transformerId; entry._metadata.extension = extensions?.[0]; allContentNodesJSON[ctx.collection].push(entry); @@ -198,6 +197,7 @@ export async function generateSchema( schemaComposer, transformersById, allContentNodesJSON, + collectionEntry: collectionEntriesByName[name], config, }); @@ -234,7 +234,7 @@ export async function generateSchema( )} that are referenced by this ${name}`, resolver: () => refTypeTC.getResolver('findMany'), prepareArgs: { - ids: (source) => source[refField], + references: (source) => source[refField], }, projection: { [refField]: true }, }); @@ -242,9 +242,9 @@ export async function generateSchema( // If the reference field has a single node typeTC.addRelation(refField, { description: `The ${refType} referenced by this ${name}`, - resolver: () => refTypeTC.getResolver('findById'), + resolver: () => refTypeTC.getResolver('findByReferenceField'), prepareArgs: { - id: (source) => source[refField], + [collectionEntriesByName[refType].referenceField]: (source) => source[refField], }, projection: { [refField]: true }, }); diff --git a/packages/core/src/providers/test/base.test.ts b/packages/core/src/providers/test/base.test.ts index 11823df2..1f18f1ee 100644 --- a/packages/core/src/providers/test/base.test.ts +++ b/packages/core/src/providers/test/base.test.ts @@ -103,7 +103,7 @@ test('create collection record', async (t) => { const result: any = await flatbread.query({ variableValues: { test: { skills: { sitting } } }, source: ` - mutation CreateAuthor($test: AuthorCreateInput){ + mutation CreateAuthor($test: AuthorInput){ createAuthor(Author: $test) { id skills { @@ -132,3 +132,23 @@ test('create collection record', async (t) => { t.is(updated.data.Author.skills.sitting, sitting); }); + +test('prevents creating record with duplicate reference', async (t) => { + const flatbread = basicProject(); + + const result = await flatbread.query({ + variableValues: { test: { id: '2a3e' } }, + source: ` + mutation CreateAuthor($test: AuthorInput){ + createAuthor(Author: $test) { + id + skills { + sitting + } + } + } + `, + }); + + t.snapshot(result.errors); +}); diff --git a/packages/core/src/providers/test/snapshots/base.test.ts.md b/packages/core/src/providers/test/snapshots/base.test.ts.md index 74f150ee..509391d8 100644 --- a/packages/core/src/providers/test/snapshots/base.test.ts.md +++ b/packages/core/src/providers/test/snapshots/base.test.ts.md @@ -13,9 +13,13 @@ Generated by [AVA](https://avajs.dev). allAuthors: [ { enjoys: [ - 'apples', + 'sitting', + 'standing', + 'mow mow', + 'sleepy time', + 'attention', ], - name: 'Another User', + name: 'Eva', }, { enjoys: [ @@ -25,6 +29,14 @@ Generated by [AVA](https://avajs.dev). ], name: 'Tony', }, + { + enjoys: [ + 'cats', + 'coffee', + 'design', + ], + name: 'Daes', + }, ], }, } @@ -39,11 +51,35 @@ Generated by [AVA](https://avajs.dev). { enjoys: [ 'cats', - 'tea', - 'making this', + 'coffee', + 'design', ], name: 'Daes', }, ], }, } + +## prevents creating record with duplicate reference + +> Snapshot 1 + + [ + GraphQLError { + extensions: {}, + locations: [ + { + column: 9, + line: 3, + }, + ], + message: `Failed to create␊ + {␊ + "id": "2a3e"␊ + }␊ + Author with id of 2a3e already exists`, + path: [ + 'createAuthor', + ], + }, + ] diff --git a/packages/core/src/providers/test/snapshots/base.test.ts.snap b/packages/core/src/providers/test/snapshots/base.test.ts.snap index 61d815b009304b40952f4953def70e0acb14d36d..fd6c482f8e476e486c6fba796e2a7e921702cea9 100644 GIT binary patch literal 836 zcmV-K1H1e|RzV}8635PB9Kn{cocvrPUt=2YNmur5Tex^p*h<;>@n5S?( zi#SH?Nn+10EP|s68Ez*gCJb`KAjBX?3Au`j#|bq^X_8oxv)IKFp7x6_%za!u&A6y? z&ztS(b|_Wk$a;^mkBDu=*VJKYR!Tk~G~Lzfb%Rum$VH8i?@1!HC}bo458Tc~TQhP= zTe$TR>!c8$m5&cqU!uxu#D}7)J)-&zRdx`jSx_At$=ZywK4JdO+J;~;9AL(Ev^9rx z2Hc1AgU&7UjPVic)j|#Jh zn}|+9xm;9!gbI%lFA!S=vu3)53>TXr;mm*BSyX!XRzEd?1#?wHk?nYTzRQ)O_B-z?g0ebr@&16E>Rw zBmQg`lxPLJli!!%@hDF2v8M1e$o*~s^6Edm1z;X+g8Q0I~iF8U$(fTi{&Z%pcHQnD>Q$Z3lChT!-O5EGW@Mp8^UdzV~uHk z1{&4-@nOgJzQ1VkaWK<&B+IE)xcJPs zsIXN~viBzb3yFzOzVqEQo@AQj6VCy~Yr2A%D!xpmz2+cCC^n(j^Mm-)B9-Dacp`vZ O8T|!f>antM2mk=Yag0U) literal 576 zcmV-G0>Ax1RzV$?(}4Wvp;4D=Ti!~z>rB_xD|gcvI^vLc4AOl-VMdnU0}svrg|ee$#4_db8m&(Hg# zL6XH&^W+6uWfPnZq)~CSy@Pq7jFwwwl3DY3e>jv@-WOj4Ych903-}c}Un5+Eum|Ct z!6wEQpbWd+E`#$7fWZZT+eEwsfPfbdbL(_ygAoJ1So`*9;}XPM2PLE#M-awjRq6sCk-sQ3$({SoyQ zQQijj~qh@yUGvpJ*8?^>86mcG^s{Sw!#&;S+6>AZ`_rRVn&p;FD|8 zTGUo$+JAb>MuJ#cQ*$9K%9??8DyL*DvJ+)i5ewsNG(xQDg+nxItWP$7mSZBFPnkp` zl~~MA`wPJ9|MV}w4=DHktnEKu^v}Kl@q^Ak3B5D+yj+T_@OMiQJfK1(JR&@YKP|&Q O8}2VAm0!N#1pojV)(D0G diff --git a/packages/core/src/resolvers/arguments.ts b/packages/core/src/resolvers/arguments.ts index 9c727db0..c306009f 100644 --- a/packages/core/src/resolvers/arguments.ts +++ b/packages/core/src/resolvers/arguments.ts @@ -1,11 +1,12 @@ -import { keyBy } from 'lodash-es'; +import { get, keyBy } from 'lodash-es'; import sift, { generateFilterSetManifest, TargetAndComparator, } from '../utils/sift'; -import { ContentNode, FlatbreadConfig } from '../types'; +import { ContentNode, FlatbreadConfig, LoadedCollectionEntry } from '../types'; import { FlatbreadProvider } from '../providers/base'; interface ResolveQueryArgsOptions { + collectionEntry: LoadedCollectionEntry; type: { name: string; pluralName: string; @@ -27,14 +28,20 @@ const resolveQueryArgs = async ( if (filter) { // Place the nodes into a keyed object by ID so we can easily filter by ID without doing tons of looping. // TODO: store all nodes in an ID-keyed object. - // TODO: replace id field with user-defined/fallback identifier field. - const nodeById = keyBy(nodes, 'id'); + const nodeByReference = keyBy( + nodes, + options.collectionEntry.referenceField + ); // Turn the filter into a GraphQL subquery that returns an array of matching content node IDs. - const listOfNodeIDsToFilter = await resolveFilter(filter, config, options); + const listOfRecordReferencesToFilter = await resolveFilter( + filter, + config, + options + ); - nodes = listOfNodeIDsToFilter.map( - (desiredNodeId) => nodeById[desiredNodeId] + nodes = listOfRecordReferencesToFilter.map( + (desiredNodeReference) => nodeByReference[desiredNodeReference] ); } @@ -83,7 +90,7 @@ const resolveQueryArgs = async ( * */ function buildFilterQueryFragment(filterSetManifest: TargetAndComparator) { - let filterToQuery = []; + const filterToQuery = []; for (const filter of filterSetManifest) { let graphQLFieldAccessor = ''; @@ -134,11 +141,10 @@ export const resolveFilter = async ( // Build a GraphQL query fragment that will be used to resolve content nodes in a structure expected by the sift function, for the given filter. const filterQueryFragment = buildFilterQueryFragment(filterSetManifest); - // TODO: replace id field with user-defined/fallback identifier field const queryString = ` query ${options.type.pluralQueryName}_FilterSubquery { ${options.type.pluralQueryName} { - id + ${options.collectionEntry.referenceField} ${filterQueryFragment} } } @@ -150,7 +156,12 @@ export const resolveFilter = async ( const result = data?.[options.type.pluralQueryName] as ContentNode[]; - return result.filter(sift(filter)).map((node) => node.id); + return result + .filter(sift(filter)) + .map( + (node) => + get(node, options.collectionEntry.referenceField) as string | number + ); }; /** diff --git a/packages/flatbread/README.md b/packages/flatbread/README.md index 54f813cb..018ec1b4 100644 --- a/packages/flatbread/README.md +++ b/packages/flatbread/README.md @@ -257,6 +257,41 @@ Limits the number of returned entries to the specified amount. Accepts an intege [Check out the example integrations](https://github.com/FlatbreadLabs/flatbread/tree/main/playground) of using Flatbread with frameworks like SvelteKit and Next.js. +## Create and update records (mutations) + +Create a new record +```graphql + +mutate ($example: PostInput) { + createPost(Post: $example) { + id + title + } +} +``` + +Update an existing record +```graphql +mutate ($example: PostInput) { + updatePost(Post: $example) { + id + title + } +} +``` + + +Upsert a record (will update if reference exists, or create a new one) +```graphql +mutate ($example: PostInput) { + upsertPost(Post: $example) { + id + title + } +} +``` + + ## Field overrides Field overrides allow you to define custom GraphQL types or resolvers on top of fields in your content. For example, you could [optimize images](https://github.com/FlatbreadLabs/flatbread/tree/main/packages/resolver-svimg/), encapsulate an endpoint, and more! diff --git a/packages/source-filesystem/src/utils/tests/snapshots/gatherFileNodes.test.ts.md b/packages/source-filesystem/src/utils/tests/snapshots/gatherFileNodes.test.ts.md index ea374d37..766581fa 100644 --- a/packages/source-filesystem/src/utils/tests/snapshots/gatherFileNodes.test.ts.md +++ b/packages/source-filesystem/src/utils/tests/snapshots/gatherFileNodes.test.ts.md @@ -41,19 +41,6 @@ Generated by [AVA](https://avajs.dev). }, ] -## double level recursion - -> Snapshot 1 - - [ - { - data: {}, - isDirectory: Function isDirectory {}, - name: 'file.md', - path: 'deeply/nested/file.md', - }, - ] - ## double level recursion named > Snapshot 1 @@ -70,19 +57,6 @@ Generated by [AVA](https://avajs.dev). }, ] -## single level recursion - -> Snapshot 1 - - [ - { - data: {}, - isDirectory: Function isDirectory {}, - name: 'random file.md', - path: '/random file.md', - }, - ] - ## double level recursion named without parent directory > Snapshot 1 @@ -141,84 +115,6 @@ Generated by [AVA](https://avajs.dev). }, ] -## double level first named - -> Snapshot 1 - - [ - { - data: { - genre: 'Comedy', - }, - isDirectory: Function isDirectory {}, - name: 'Nine Lives of Tomas Katz, The.md', - path: 'Comedy/Nine Lives of Tomas Katz, The.md', - }, - { - data: { - genre: 'Comedy', - }, - isDirectory: Function isDirectory {}, - name: 'Road to Wellville, The.md', - path: 'Comedy/Road to Wellville, The.md', - }, - { - data: { - genre: 'Drama', - }, - isDirectory: Function isDirectory {}, - name: 'Life for Sale (Life for Sale (Kotirauha).md', - path: 'Drama/Life for Sale (Life for Sale (Kotirauha).md', - }, - { - data: { - genre: 'Documentary', - }, - isDirectory: Function isDirectory {}, - name: 'TerrorStorm: A History of Government-Sponsored Terrorism.md', - path: 'Documentary/TerrorStorm: A History of Government-Sponsored Terrorism.md', - }, - ] - -## double level second named - -> Snapshot 1 - - [ - { - data: { - title: 'Nine Lives of Tomas Katz, The', - }, - isDirectory: Function isDirectory {}, - name: 'Nine Lives of Tomas Katz, The.md', - path: 'Comedy/Nine Lives of Tomas Katz, The.md', - }, - { - data: { - title: 'Road to Wellville, The', - }, - isDirectory: Function isDirectory {}, - name: 'Road to Wellville, The.md', - path: 'Comedy/Road to Wellville, The.md', - }, - { - data: { - title: 'Life for Sale (Life for Sale (Kotirauha)', - }, - isDirectory: Function isDirectory {}, - name: 'Life for Sale (Life for Sale (Kotirauha).md', - path: 'Drama/Life for Sale (Life for Sale (Kotirauha).md', - }, - { - data: { - title: 'TerrorStorm: A History of Government-Sponsored Terrorism', - }, - isDirectory: Function isDirectory {}, - name: 'TerrorStorm: A History of Government-Sponsored Terrorism.md', - path: 'Documentary/TerrorStorm: A History of Government-Sponsored Terrorism.md', - }, - ] - ## triple level > Snapshot 1 diff --git a/packages/source-filesystem/src/utils/tests/snapshots/gatherFileNodes.test.ts.snap b/packages/source-filesystem/src/utils/tests/snapshots/gatherFileNodes.test.ts.snap index 8437dd5363aff4fd90c0e1b76dcda6c3b9b8f4a8..343025d6595f0dc9399a54acdd5554269f9fd798 100644 GIT binary patch literal 964 zcmV;#13UadRzVlDR?S2b2t(hM!P0dKA zA*o=|4j+pM00000000B+S50dZK^WfIWRtY9O^Y8;Y++8?Vrx(+6kCL9X$vh@O+gCM zI>}C&shgcRyNSk|JrqB{AD|vu^&kjdJ$dpfC@6UH?#+`YL47xy-KN`ZzD!CF1|D`M zdEaNAd7qit$yPe4@zhOY<2f~%snJ_WVz3lWYsAE9uBlWoLnk>S!%gGfmZ}nyETPK* zDJ)qZ6hZPZJkJ6SqfjqWCWRnHgq&6^7L!n?gph=y2u(Oi3WsMN@Cfh<@Yx{ zOQd7$Xft^jnvYj2S~%L7t=K*&!(RF^X@QS z%+y-YCoghvz8J5~;VOUUtNeQt%KNTzqGFTdYgE$;Ow(vRl5tsbm1I}zam7#Tw9>fN zQ}ZlM;a(+hg1`nE^mOOA$ppz~$WRUSXQl+nl6XUG?sSU2(|Os%bjBB{5L_f6E_)77 z;#p=`DVF&&yg-GXg%e;Tk>k3-1sn}_I5V;}rZ+RiQ+X?wh+_Po#(*RBD@^Pg0EJq9 zqyqZ{uwDS9z3ibz*~32UPk?n3@ZQVb(I|VT4|@k3zA?a9*u#FPQTDDnUN*`q)rGrs zLiYai#*|(#PH&*+%wt=JS*2OVp6$CTgi@G{p| z-PikGx3&8R?LXj&eg6-L);I- mG7WeDc+(c!0@c*}U$R5Cmu}aVU)BE7ZTv5B&NI}y5dZ)|*3e7< literal 1170 zcmV;D1a134RzVDeTn8IsGnnfc>@eYo~wx zi2rCujUS5$00000000B+SwV zlb2+hx9pY#KHg07|KI%c|1bgXc%jmU?mA9e~Dj@qXJYNLtLAl+?IV}WfA{4hurILkqS_oOFh|sB6$>H#9 z06qkK0r({*$X%=~)2c#tr#U@6ZJ}Kj%35d&q5nW|FUnZR*^RQ1HLY(Cphw>pnVe?= z6uD?xs06fjMrB;nL*Y6R zG*sA)xz|T1f2S)#&Kx{n0xSR)0ape^E8B_|YZE~0ljzwF;ZIT1ZZ&OidgLG&;W#u; z0$vA%fG_oBu7wj+9)ieUfTC?#2LV@XO~qNK@-c|40=@?P3Yg7kDus0_%OFw*+yZ_WC;tOnrO3XdH!~-Jnd7|DsjF)PIR^Ib3bBKJiJeZ?n z`L{;P?{C2JnKw9bvEuj=_5Bs*`*awS@tVmA${kzxJ0@GtI-}eA!Ug70xLOFjNMHjU z)Vk-nVuCbkb|Bh2UZlX=1AM2m?--SRr-6M39KI#MTwY^;YE<@JL!^7>8VyBf>Ezn`PnT&Wn7DT! z@jMyRAp6WGz!2szw#4jp2+V%Mff9CU7@J7z7vi=GOrU%mYPR;#X_+w|%R z*_UC=tEm*rC^5vvE?eTN?3tdb%ws~Te!D+xn3orY7nAnd_3PqRwC$RplGmoR-viC!w^=ul&mOT%wujtHUDYVw53UduK zM#;GH87SCYv&4C7rYBEHTIHeJH&H#9B&tcGnk1@8qS~H`ssIQ6R2u&b*wKFu3#lZk zJHUE(a}w34xBX;-q?>%Q=4jXSWUj}(efvtVdYm2&_wJv!+Iy-aoBp0E`xXr80X_hH kmCE%ylVrX5byco?C+~FI<|nR|)_bl00bvJS1BD_00J %K!iX From 04a9983fdbeb6e1a22dcd9ca7b0e9625f7f627c5 Mon Sep 17 00:00:00 2001 From: Adam Sparks Date: Tue, 16 Aug 2022 05:16:49 -0500 Subject: [PATCH 21/22] fix tests --- packages/core/src/generators/schema.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/core/src/generators/schema.ts b/packages/core/src/generators/schema.ts index 351ce057..17cb09f4 100644 --- a/packages/core/src/generators/schema.ts +++ b/packages/core/src/generators/schema.ts @@ -43,9 +43,9 @@ export async function generateSchema( config.source.initialize?.(config); // Invoke the content source resolver to retrieve the content nodes - let allContentNodes: Record = {}; + const allContentNodes: Record = {}; - let collectionEntriesByName = Object.fromEntries( + const collectionEntriesByName = Object.fromEntries( config.content.map((collection: LoadedCollectionEntry) => [ collection.name, collection, @@ -151,7 +151,8 @@ export async function generateSchema( } else { const reference = get(entry, entry._metadata.referenceField); entry._metadata.reference = reference ?? createUniqueId(); - if (!reference) set(entry, entry._metadata.referenceField, entry._metadata.reference); + if (!reference) + set(entry, entry._metadata.referenceField, entry._metadata.reference); entry._metadata.transformedBy = transformerId; entry._metadata.extension = extensions?.[0]; allContentNodesJSON[ctx.collection].push(entry); @@ -220,6 +221,7 @@ export async function generateSchema( Object.entries(refs).forEach(([refField, refType]) => { const refTypeTC = schemaComposer.getOTC(refType); + const refCollectionEntry = collectionEntriesByName[refType]; // If the current content type has this valid reference field as declared in the config, we'll add a resolver for this reference if (!typeTC.hasField(refField)) return; @@ -244,7 +246,8 @@ export async function generateSchema( description: `The ${refType} referenced by this ${name}`, resolver: () => refTypeTC.getResolver('findByReferenceField'), prepareArgs: { - [collectionEntriesByName[refType].referenceField]: (source) => source[refField], + [refCollectionEntry.referenceField]: (source: EntryNode) => + source[refField], }, projection: { [refField]: true }, }); From eb4c5b05c0be9ff7cb36047055f2551a1ae1b515 Mon Sep 17 00:00:00 2001 From: Adam Sparks Date: Tue, 16 Aug 2022 05:35:56 -0500 Subject: [PATCH 22/22] linting --- examples/content/markdown/authors/Adam.md | 1 - examples/content/markdown/authors/Amanda.md | 1 - examples/sveltekit/src/routes/+page.svelte | 5 +++-- package.json | 2 +- packages/core/src/errors.ts | 6 ++++-- packages/flatbread/README.md | 5 +++-- 6 files changed, 11 insertions(+), 9 deletions(-) diff --git a/examples/content/markdown/authors/Adam.md b/examples/content/markdown/authors/Adam.md index 606e93d2..bf1b5e25 100644 --- a/examples/content/markdown/authors/Adam.md +++ b/examples/content/markdown/authors/Adam.md @@ -6,4 +6,3 @@ enjoys: - coding - flatbread --- - diff --git a/examples/content/markdown/authors/Amanda.md b/examples/content/markdown/authors/Amanda.md index e2c14b4c..7aaf7743 100644 --- a/examples/content/markdown/authors/Amanda.md +++ b/examples/content/markdown/authors/Amanda.md @@ -5,4 +5,3 @@ enjoys: - coding - flatbread --- - diff --git a/examples/sveltekit/src/routes/+page.svelte b/examples/sveltekit/src/routes/+page.svelte index 258d4680..ba092e91 100644 --- a/examples/sveltekit/src/routes/+page.svelte +++ b/examples/sveltekit/src/routes/+page.svelte @@ -5,7 +5,6 @@ /** @type {import('./$types').PageData}*/ export let data; - if (browser) { import('svimg/dist/s-image'); } @@ -20,7 +19,9 @@
-
+    
       
         {JSON.stringify(data, null, 2)}
       
diff --git a/package.json b/package.json
index d8d33df0..f47015b7 100644
--- a/package.json
+++ b/package.json
@@ -16,7 +16,7 @@
     "lint:prettier": "prettier --check --plugin-search-dir=. .",
     "lint": "pnpm lint:prettier",
     "lint:fix": "pnpm lint:fix:prettier",
-    "lint:fix:prettier": "pretty-quick --staged",
+    "lint:fix:prettier": "prettier --write --plugin-search-dir=. .",
     "play": "cd examples/sveltekit && pnpm dev",
     "play:build": "pnpm build && cd examples/sveltekit && pnpm build",
     "prepublish:ci": "pnpm -r update",
diff --git a/packages/core/src/errors.ts b/packages/core/src/errors.ts
index c66fbf55..f36a1d19 100644
--- a/packages/core/src/errors.ts
+++ b/packages/core/src/errors.ts
@@ -16,7 +16,9 @@ export class IllegalFieldNameError extends Error {
   }
 }
 
-export class ReferenceAlreadyExistsError> extends Error {
+export class ReferenceAlreadyExistsError<
+  K extends Record
+> extends Error {
   constructor(
     payload: K,
     collectionName: string,
@@ -30,4 +32,4 @@ export class ReferenceAlreadyExistsError> extends Er
       ${collectionName} with ${metadata.referenceField} of ${metadata.reference} already exists`
     );
   }
-}
\ No newline at end of file
+}
diff --git a/packages/flatbread/README.md b/packages/flatbread/README.md
index 018ec1b4..34e7b024 100644
--- a/packages/flatbread/README.md
+++ b/packages/flatbread/README.md
@@ -260,6 +260,7 @@ Limits the number of returned entries to the specified amount. Accepts an intege
 ## Create and update records (mutations)
 
 Create a new record
+
 ```graphql
 
 mutate ($example: PostInput) {
@@ -271,6 +272,7 @@ mutate ($example: PostInput) {
 ```
 
 Update an existing record
+
 ```graphql
 mutate ($example: PostInput) {
   updatePost(Post: $example) {
@@ -280,8 +282,8 @@ mutate ($example: PostInput) {
 }
 ```
 
-
 Upsert a record (will update if reference exists, or create a new one)
+
 ```graphql
 mutate ($example: PostInput) {
   upsertPost(Post: $example) {
@@ -291,7 +293,6 @@ mutate ($example: PostInput) {
 }
 ```
 
-
 ## Field overrides
 
 Field overrides allow you to define custom GraphQL types or resolvers on top of fields in your content. For example, you could [optimize images](https://github.com/FlatbreadLabs/flatbread/tree/main/packages/resolver-svimg/), encapsulate an endpoint, and more!