@tinacms/graphql 1.5.15 → 1.5.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,144 @@
1
+ # @tinacms/graphql
2
+
3
+ [Tina](https://tina.io) is a headless content management system with support for Markdown, MDX, JSON, YAML, and more. This package contains the logic required to turn a collection of folders and files into a database that can be queried using [GraphQL](https://tina.io/docs/graphql/queries).
4
+
5
+ ## Features
6
+
7
+ - Query Markdown, MDX, JSON, YAML and more using GraphQL
8
+ - Supports references between documents
9
+ - Pre-generation of schema and query data to expedite website compilation
10
+
11
+ ## Getting Started
12
+
13
+ The easiest way to use this package is as part of a broader TinaCMS site. The starter can be tested locally using:
14
+
15
+ ```
16
+ npx create-tina-app@latest
17
+ ```
18
+
19
+ Alternatively, you can directly install and use this package.
20
+
21
+ ### Install @tinacms/graphql
22
+
23
+ ```
24
+ pnpm install
25
+ pnpm add @tinacms/graphql
26
+ ```
27
+
28
+ ### Build your program
29
+
30
+ The following example demonstrates how to use this package.
31
+
32
+ ```ts
33
+ import { MemoryLevel } from 'memory-level';
34
+ import {
35
+ Database,
36
+ FilesystemBridge,
37
+ buildSchema,
38
+ resolve
39
+ } from '@tinacms/graphql';
40
+ import {
41
+ Schema
42
+ } from '@tinacms/schema-tools';
43
+
44
+ const dir = 'content';
45
+
46
+ // Where to source content from
47
+ const bridge = new FilesystemBridge(dir);
48
+ // Where to store the index data
49
+ const indexStorage = new MemoryLevel<string, Record<string, string>>();
50
+
51
+ // Create the schema/structure of the database
52
+ const rawSchema: Schema = {
53
+ collections: [
54
+ {
55
+ name: 'post',
56
+ path: '', // Don't require content to be placed within a subdirectory
57
+ fields: [
58
+ {
59
+ type: 'string',
60
+ name: 'title',
61
+ isTitle: true,
62
+ required: true
63
+ }
64
+ ]
65
+ }
66
+ ]
67
+ };
68
+ const schema = await buildSchema({
69
+ schema: rawSchema,
70
+ build: {
71
+ publicFolder: '',
72
+ outputFolder: ''
73
+ }
74
+ });
75
+
76
+ // Create the object for editing and querying your repository
77
+ const database = new Database({
78
+ bridge,
79
+ level: indexStorage,
80
+ tinaDirectory: 'tina'
81
+ });
82
+
83
+ // Generate the index data required to support querying
84
+ await database.indexContent(schema)
85
+
86
+ // Query the database and output the result
87
+ // In this case, it will retrieve the title of the post 'in.md'
88
+ const graphQLQuery = `
89
+ query {
90
+ document(collection: "post", relativePath: "in.md") {
91
+ ...on Document {
92
+ _values,
93
+ _sys { title }
94
+ }
95
+ }
96
+ }
97
+ `
98
+ const result = await resolve({
99
+ database,
100
+ query: graphQLQuery,
101
+ variables: {}
102
+ });
103
+
104
+ // Output the result
105
+ console.log(JSON.stringify(result))
106
+ ```
107
+
108
+ For the program to work:
109
+
110
+ 1. Install packages:
111
+ - [`@tinacms/schema-tools`](https://www.npmjs.com/package/@tinacms/schema-tools)
112
+ - [`memory-level`](https://www.npmjs.com/package/memory-level)
113
+ 2. Add a file `content/in.md` of the following form:
114
+
115
+ ```md
116
+ ---
117
+ title: Hello
118
+ ---
119
+ ```
120
+
121
+ The output should be:
122
+
123
+ ```json
124
+ {"data":{"document":{"_values":{"_collection":"post","_template":"post","title":"Hello"},"_sys":{"title":"Hello"}}}}
125
+ ```
126
+
127
+ ## Development
128
+
129
+ The package is part of the [TinaCMS repository](https://github.com/tinacms/tinacms/).
130
+
131
+ ## Documentation
132
+
133
+ Visit [Tina's documentation](https://tina.io/docs/) to learn more.
134
+
135
+ ## Questions?
136
+
137
+ [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?url=https%3A%2F%2Ftinacms.org&text=I%20just%20checked%20out%20@tinacms%20on%20GitHub%20and%20it%20is%20sweet%21&hashtags=TinaCMS%2Cjamstack%2Cheadlesscms)
138
+ [![Forum](https://shields.io/github/discussions/tinacms/tinacms)](https://github.com/tinacms/tinacms/discussions)
139
+
140
+ Visit the [GitHub Discussions](https://github.com/tinacms/tinacms/discussions) or our [Community Discord](https://discord.com/invite/zumN63Ybpf) to ask questions, or look us up on on Twitter at [@tinacms](https://twitter.com/tinacms).
141
+
142
+ ## Contributing
143
+
144
+ Please see our [./CONTRIBUTING.md](https://github.com/tinacms/tinacms/blob/main/CONTRIBUTING.md)
@@ -2,7 +2,7 @@
2
2
 
3
3
  */
4
4
  import { LookupMapType } from '../database';
5
- import type { ObjectTypeDefinitionNode, InlineFragmentNode, FieldDefinitionNode } from 'graphql';
5
+ import type { FieldDefinitionNode, InlineFragmentNode, ObjectTypeDefinitionNode } from 'graphql';
6
6
  import type { Collection, Template } from '@tinacms/schema-tools';
7
7
  import { TinaSchema } from '@tinacms/schema-tools';
8
8
  export declare const createBuilder: ({ tinaSchema, }: {
@@ -177,7 +177,7 @@ export declare class Builder {
177
177
  * ```
178
178
  *
179
179
  * @public
180
- * @param collection a Tina Cloud collection
180
+ * @param collection a TinaCloud collection
181
181
  */
182
182
  collectionFragment: (collection: Collection<true>) => Promise<import("graphql").FragmentDefinitionNode>;
183
183
  /**
package/dist/index.d.ts CHANGED
@@ -1 +1,32 @@
1
- export * from "../src/index"
1
+ import type { Schema, Collection, Template as TinaTemplate } from '@tinacms/schema-tools';
2
+ import { buildDotTinaFiles } from './build';
3
+ export { resolve } from './resolve';
4
+ export { transformDocumentIntoPayload } from './resolver';
5
+ export * from './resolver/error';
6
+ export { TinaLevelClient } from './level/tinaLevel';
7
+ export type { Level } from './database/level';
8
+ export type { QueryOptions, OnDeleteCallback, OnPutCallback, DatabaseArgs, GitProvider, CreateDatabase, } from './database';
9
+ export { Database, createDatabaseInternal, createDatabase, createLocalDatabase, } from './database';
10
+ import type { Config } from '@tinacms/schema-tools';
11
+ export { getChangedFiles, getSha, shaExists } from './git';
12
+ export * from './auth/utils';
13
+ export { sequential, assertShape } from './util';
14
+ export { loadAndParseWithAliases, stringifyFile, parseFile, scanAllContent, scanContentByPaths, transformDocument, } from './database/util';
15
+ export { createSchema } from './schema/createSchema';
16
+ export { buildDotTinaFiles };
17
+ export type DummyType = unknown;
18
+ export declare const buildSchema: (config: Config, flags?: string[]) => Promise<{
19
+ graphQLSchema: {
20
+ kind: "Document";
21
+ definitions: any;
22
+ };
23
+ tinaSchema: import("@tinacms/schema-tools").TinaSchema;
24
+ lookup: Record<string, import("./database").LookupMapType>;
25
+ fragDoc: string;
26
+ queryDoc: string;
27
+ }>;
28
+ export type TinaSchema = Schema;
29
+ export type { TinaTemplate, Schema, Collection };
30
+ export { FilesystemBridge, AuditFileSystemBridge, } from './database/bridge/filesystem';
31
+ export { IsomorphicBridge } from './database/bridge/isomorphic';
32
+ export type { Bridge } from './database/bridge';
package/dist/index.js CHANGED
@@ -1914,7 +1914,7 @@ var Builder = class {
1914
1914
  * ```
1915
1915
  *
1916
1916
  * @public
1917
- * @param collection a Tina Cloud collection
1917
+ * @param collection a TinaCloud collection
1918
1918
  */
1919
1919
  this.collectionFragment = async (collection) => {
1920
1920
  const name = NAMER.dataTypeName(collection.namespace);
@@ -3090,7 +3090,7 @@ var validateField = async (field) => {
3090
3090
  // package.json
3091
3091
  var package_default = {
3092
3092
  name: "@tinacms/graphql",
3093
- version: "1.5.15",
3093
+ version: "1.5.17",
3094
3094
  main: "dist/index.js",
3095
3095
  module: "dist/index.mjs",
3096
3096
  typings: "dist/index.d.ts",
@@ -3116,7 +3116,6 @@ var package_default = {
3116
3116
  types: "pnpm tsc",
3117
3117
  build: "tinacms-scripts build",
3118
3118
  docs: "pnpm typedoc",
3119
- serve: "pnpm nodemon dist/server.js",
3120
3119
  test: "vitest run",
3121
3120
  "test-watch": "vitest"
3122
3121
  },
@@ -3171,7 +3170,6 @@ var package_default = {
3171
3170
  "@types/yup": "^0.29.14",
3172
3171
  "jest-file-snapshot": "^0.5.0",
3173
3172
  "memory-level": "^1.0.0",
3174
- nodemon: "3.1.4",
3175
3173
  typescript: "^5.7.3",
3176
3174
  vite: "^4.5.9",
3177
3175
  vitest: "^0.32.4",
@@ -7071,13 +7069,14 @@ var Database = class {
7071
7069
  documentPaths,
7072
7070
  async (collection, documentPaths2) => {
7073
7071
  if (collection && !collection.isDetached) {
7074
- await _indexContent(
7075
- this,
7076
- this.contentLevel,
7077
- documentPaths2,
7072
+ await _indexContent({
7073
+ database: this,
7074
+ level: this.contentLevel,
7075
+ documentPaths: documentPaths2,
7078
7076
  enqueueOps,
7079
- collection
7080
- );
7077
+ collection,
7078
+ isPartialReindex: true
7079
+ });
7081
7080
  }
7082
7081
  }
7083
7082
  );
@@ -7185,26 +7184,26 @@ var Database = class {
7185
7184
  );
7186
7185
  const doc = await level2.keys({ limit: 1 }).next();
7187
7186
  if (!doc) {
7188
- await _indexContent(
7189
- this,
7190
- level2,
7191
- contentPaths,
7187
+ await _indexContent({
7188
+ database: this,
7189
+ level: level2,
7190
+ documentPaths: contentPaths,
7192
7191
  enqueueOps,
7193
7192
  collection,
7194
- userFields.map((field) => [
7193
+ passwordFields: userFields.map((field) => [
7195
7194
  ...field.path,
7196
7195
  field.passwordFieldName
7197
7196
  ])
7198
- );
7197
+ });
7199
7198
  }
7200
7199
  } else {
7201
- await _indexContent(
7202
- this,
7200
+ await _indexContent({
7201
+ database: this,
7203
7202
  level,
7204
- contentPaths,
7203
+ documentPaths: contentPaths,
7205
7204
  enqueueOps,
7206
7205
  collection
7207
- );
7206
+ });
7208
7207
  }
7209
7208
  }
7210
7209
  );
@@ -7343,7 +7342,15 @@ var hashPasswordValues = async (data, passwordFields) => Promise.all(
7343
7342
  )
7344
7343
  );
7345
7344
  var isGitKeep = (filepath, collection) => filepath.endsWith(`.gitkeep.${(collection == null ? void 0 : collection.format) || "md"}`);
7346
- var _indexContent = async (database, level, documentPaths, enqueueOps, collection, passwordFields) => {
7345
+ var _indexContent = async ({
7346
+ database,
7347
+ level,
7348
+ documentPaths,
7349
+ enqueueOps,
7350
+ collection,
7351
+ passwordFields,
7352
+ isPartialReindex
7353
+ }) => {
7347
7354
  var _a;
7348
7355
  let collectionIndexDefinitions;
7349
7356
  let collectionPath;
@@ -7385,40 +7392,42 @@ var _indexContent = async (database, level, documentPaths, enqueueOps, collectio
7385
7392
  normalizedPath,
7386
7393
  collectionPath || ""
7387
7394
  );
7388
- const item = await rootSublevel.get(normalizedPath);
7389
- if (item) {
7390
- await database.contentLevel.batch([
7391
- ...makeRefOpsForDocument(
7392
- normalizedPath,
7393
- collection == null ? void 0 : collection.name,
7394
- collectionReferences,
7395
- item,
7396
- "del",
7397
- level
7398
- ),
7399
- ...makeIndexOpsForDocument(
7400
- normalizedPath,
7401
- collection.name,
7402
- collectionIndexDefinitions,
7403
- item,
7404
- "del",
7405
- level
7406
- ),
7407
- // folder indices
7408
- ...makeIndexOpsForDocument(
7409
- normalizedPath,
7410
- `${collection.name}_${folderKey}`,
7411
- collectionIndexDefinitions,
7412
- item,
7413
- "del",
7414
- level
7415
- ),
7416
- {
7417
- type: "del",
7418
- key: normalizedPath,
7419
- sublevel: rootSublevel
7420
- }
7421
- ]);
7395
+ if (isPartialReindex) {
7396
+ const item = await rootSublevel.get(normalizedPath);
7397
+ if (item) {
7398
+ await database.contentLevel.batch([
7399
+ ...makeRefOpsForDocument(
7400
+ normalizedPath,
7401
+ collection == null ? void 0 : collection.name,
7402
+ collectionReferences,
7403
+ item,
7404
+ "del",
7405
+ level
7406
+ ),
7407
+ ...makeIndexOpsForDocument(
7408
+ normalizedPath,
7409
+ collection.name,
7410
+ collectionIndexDefinitions,
7411
+ item,
7412
+ "del",
7413
+ level
7414
+ ),
7415
+ // folder indices
7416
+ ...makeIndexOpsForDocument(
7417
+ normalizedPath,
7418
+ `${collection.name}_${folderKey}`,
7419
+ collectionIndexDefinitions,
7420
+ item,
7421
+ "del",
7422
+ level
7423
+ ),
7424
+ {
7425
+ type: "del",
7426
+ key: normalizedPath,
7427
+ sublevel: rootSublevel
7428
+ }
7429
+ ]);
7430
+ }
7422
7431
  }
7423
7432
  if (!isGitKeep(filepath, collection)) {
7424
7433
  await enqueueOps([
package/dist/index.mjs CHANGED
@@ -1847,7 +1847,7 @@ var Builder = class {
1847
1847
  * ```
1848
1848
  *
1849
1849
  * @public
1850
- * @param collection a Tina Cloud collection
1850
+ * @param collection a TinaCloud collection
1851
1851
  */
1852
1852
  this.collectionFragment = async (collection) => {
1853
1853
  const name = NAMER.dataTypeName(collection.namespace);
@@ -3019,7 +3019,7 @@ var validateField = async (field) => {
3019
3019
  // package.json
3020
3020
  var package_default = {
3021
3021
  name: "@tinacms/graphql",
3022
- version: "1.5.15",
3022
+ version: "1.5.17",
3023
3023
  main: "dist/index.js",
3024
3024
  module: "dist/index.mjs",
3025
3025
  typings: "dist/index.d.ts",
@@ -3045,7 +3045,6 @@ var package_default = {
3045
3045
  types: "pnpm tsc",
3046
3046
  build: "tinacms-scripts build",
3047
3047
  docs: "pnpm typedoc",
3048
- serve: "pnpm nodemon dist/server.js",
3049
3048
  test: "vitest run",
3050
3049
  "test-watch": "vitest"
3051
3050
  },
@@ -3100,7 +3099,6 @@ var package_default = {
3100
3099
  "@types/yup": "^0.29.14",
3101
3100
  "jest-file-snapshot": "^0.5.0",
3102
3101
  "memory-level": "^1.0.0",
3103
- nodemon: "3.1.4",
3104
3102
  typescript: "^5.7.3",
3105
3103
  vite: "^4.5.9",
3106
3104
  vitest: "^0.32.4",
@@ -6986,13 +6984,14 @@ var Database = class {
6986
6984
  documentPaths,
6987
6985
  async (collection, documentPaths2) => {
6988
6986
  if (collection && !collection.isDetached) {
6989
- await _indexContent(
6990
- this,
6991
- this.contentLevel,
6992
- documentPaths2,
6987
+ await _indexContent({
6988
+ database: this,
6989
+ level: this.contentLevel,
6990
+ documentPaths: documentPaths2,
6993
6991
  enqueueOps,
6994
- collection
6995
- );
6992
+ collection,
6993
+ isPartialReindex: true
6994
+ });
6996
6995
  }
6997
6996
  }
6998
6997
  );
@@ -7099,26 +7098,26 @@ var Database = class {
7099
7098
  );
7100
7099
  const doc = await level2.keys({ limit: 1 }).next();
7101
7100
  if (!doc) {
7102
- await _indexContent(
7103
- this,
7104
- level2,
7105
- contentPaths,
7101
+ await _indexContent({
7102
+ database: this,
7103
+ level: level2,
7104
+ documentPaths: contentPaths,
7106
7105
  enqueueOps,
7107
7106
  collection,
7108
- userFields.map((field) => [
7107
+ passwordFields: userFields.map((field) => [
7109
7108
  ...field.path,
7110
7109
  field.passwordFieldName
7111
7110
  ])
7112
- );
7111
+ });
7113
7112
  }
7114
7113
  } else {
7115
- await _indexContent(
7116
- this,
7114
+ await _indexContent({
7115
+ database: this,
7117
7116
  level,
7118
- contentPaths,
7117
+ documentPaths: contentPaths,
7119
7118
  enqueueOps,
7120
7119
  collection
7121
- );
7120
+ });
7122
7121
  }
7123
7122
  }
7124
7123
  );
@@ -7257,7 +7256,15 @@ var hashPasswordValues = async (data, passwordFields) => Promise.all(
7257
7256
  )
7258
7257
  );
7259
7258
  var isGitKeep = (filepath, collection) => filepath.endsWith(`.gitkeep.${collection?.format || "md"}`);
7260
- var _indexContent = async (database, level, documentPaths, enqueueOps, collection, passwordFields) => {
7259
+ var _indexContent = async ({
7260
+ database,
7261
+ level,
7262
+ documentPaths,
7263
+ enqueueOps,
7264
+ collection,
7265
+ passwordFields,
7266
+ isPartialReindex
7267
+ }) => {
7261
7268
  let collectionIndexDefinitions;
7262
7269
  let collectionPath;
7263
7270
  if (collection) {
@@ -7298,40 +7305,42 @@ var _indexContent = async (database, level, documentPaths, enqueueOps, collectio
7298
7305
  normalizedPath,
7299
7306
  collectionPath || ""
7300
7307
  );
7301
- const item = await rootSublevel.get(normalizedPath);
7302
- if (item) {
7303
- await database.contentLevel.batch([
7304
- ...makeRefOpsForDocument(
7305
- normalizedPath,
7306
- collection?.name,
7307
- collectionReferences,
7308
- item,
7309
- "del",
7310
- level
7311
- ),
7312
- ...makeIndexOpsForDocument(
7313
- normalizedPath,
7314
- collection.name,
7315
- collectionIndexDefinitions,
7316
- item,
7317
- "del",
7318
- level
7319
- ),
7320
- // folder indices
7321
- ...makeIndexOpsForDocument(
7322
- normalizedPath,
7323
- `${collection.name}_${folderKey}`,
7324
- collectionIndexDefinitions,
7325
- item,
7326
- "del",
7327
- level
7328
- ),
7329
- {
7330
- type: "del",
7331
- key: normalizedPath,
7332
- sublevel: rootSublevel
7333
- }
7334
- ]);
7308
+ if (isPartialReindex) {
7309
+ const item = await rootSublevel.get(normalizedPath);
7310
+ if (item) {
7311
+ await database.contentLevel.batch([
7312
+ ...makeRefOpsForDocument(
7313
+ normalizedPath,
7314
+ collection?.name,
7315
+ collectionReferences,
7316
+ item,
7317
+ "del",
7318
+ level
7319
+ ),
7320
+ ...makeIndexOpsForDocument(
7321
+ normalizedPath,
7322
+ collection.name,
7323
+ collectionIndexDefinitions,
7324
+ item,
7325
+ "del",
7326
+ level
7327
+ ),
7328
+ // folder indices
7329
+ ...makeIndexOpsForDocument(
7330
+ normalizedPath,
7331
+ `${collection.name}_${folderKey}`,
7332
+ collectionIndexDefinitions,
7333
+ item,
7334
+ "del",
7335
+ level
7336
+ ),
7337
+ {
7338
+ type: "del",
7339
+ key: normalizedPath,
7340
+ sublevel: rootSublevel
7341
+ }
7342
+ ]);
7343
+ }
7335
7344
  }
7336
7345
  if (!isGitKeep(filepath, collection)) {
7337
7346
  await enqueueOps([
@@ -1,10 +1,10 @@
1
1
  /**
2
2
 
3
3
  */
4
- import type { GraphQLConfig } from '../types';
5
4
  import type { Schema } from '@tinacms/schema-tools';
5
+ import type { GraphQLConfig } from '../types';
6
6
  /**
7
- * Strips away the Tina Cloud Asset URL from an `image` value
7
+ * Strips away the TinaCloud Asset URL from an `image` value
8
8
  *
9
9
  * @param {string | string[]} value
10
10
  * @param {GraphQLConfig} config
@@ -12,7 +12,7 @@ import type { Schema } from '@tinacms/schema-tools';
12
12
  */
13
13
  export declare const resolveMediaCloudToRelative: (value: string | string[], config: GraphQLConfig, schema: Schema<true>) => string | string[];
14
14
  /**
15
- * Adds Tina Cloud Asset URL to an `image` value
15
+ * Adds TinaCloud Asset URL to an `image` value
16
16
  *
17
17
  * @param {string | string[]} value
18
18
  * @param {GraphQLConfig} config
@@ -1,6 +1,3 @@
1
- /**
2
-
3
- */
4
1
  import { TinaSchema, Schema } from '@tinacms/schema-tools';
5
2
  export declare const createSchema: ({ schema, flags, }: {
6
3
  schema: Schema;
@@ -1,6 +1,3 @@
1
- /**
2
-
3
- */
4
1
  import { type Schema, type Collection } from '@tinacms/schema-tools';
5
2
  export declare const validateSchema: (schema: Schema) => Promise<{
6
3
  collections: Collection<true>[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tinacms/graphql",
3
- "version": "1.5.15",
3
+ "version": "1.5.17",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.mjs",
6
6
  "typings": "dist/index.d.ts",
@@ -44,8 +44,8 @@
44
44
  "readable-stream": "^4.7.0",
45
45
  "scmp": "^2.1.0",
46
46
  "yup": "^0.32.11",
47
- "@tinacms/mdx": "1.6.1",
48
- "@tinacms/schema-tools": "1.7.2"
47
+ "@tinacms/mdx": "1.6.2",
48
+ "@tinacms/schema-tools": "1.7.3"
49
49
  },
50
50
  "publishConfig": {
51
51
  "registry": "https://registry.npmjs.org"
@@ -71,19 +71,17 @@
71
71
  "@types/yup": "^0.29.14",
72
72
  "jest-file-snapshot": "^0.5.0",
73
73
  "memory-level": "^1.0.0",
74
- "nodemon": "3.1.4",
75
74
  "typescript": "^5.7.3",
76
75
  "vite": "^4.5.9",
77
76
  "vitest": "^0.32.4",
78
77
  "zod": "^3.24.2",
79
- "@tinacms/schema-tools": "1.7.2",
80
- "@tinacms/scripts": "1.3.3"
78
+ "@tinacms/schema-tools": "1.7.3",
79
+ "@tinacms/scripts": "1.3.4"
81
80
  },
82
81
  "scripts": {
83
82
  "types": "pnpm tsc",
84
83
  "build": "tinacms-scripts build",
85
84
  "docs": "pnpm typedoc",
86
- "serve": "pnpm nodemon dist/server.js",
87
85
  "test": "vitest run",
88
86
  "test-watch": "vitest"
89
87
  }
package/readme.md DELETED
@@ -1,194 +0,0 @@
1
- ## Getting started
2
-
3
- There's a serve and watch command, which are separate for now:
4
-
5
- ```sh
6
- // terminal 1
7
- cd packages/tina-graphql
8
- yarn watch
9
-
10
- // terminal 2
11
- cd packages/tina-graphql
12
- yarn serve
13
- ```
14
-
15
- You can consume this from the `graphiql` app:
16
-
17
- ```sh
18
- // terminal 3
19
- cd apps/graphiql
20
- yarn start
21
- ```
22
-
23
- Note that this app doesn't use anything from the `client` package right now, it's just an interactive tool to see how things move from the graphql server into tina. That process has been improved in this package but will need to be merged back into the `client` package before this is usable.
24
-
25
- ### Running queries
26
-
27
- By default the app will redirect to `project1` and display the default query generated from the `graphql-helpers` library - which consumes the fixtures from the `project1` folder the the `gql` package, any number of fixtures can be used if you want to add your own, just ensure the `server.ts` file knows about them.
28
-
29
- When you run the initial query, you should see the result along with the Tina sidebar toggle, this indicates that the Tina form has now been populated with the query values. If you change some values around and hit submit, the `onSubmit` function will populate the GraphiQL editor instead of sending it off to the server, you can play around with the mutation before sending it off if you'd like.
30
-
31
- ### Tests
32
-
33
- The most valuable test right now is the `builder.spec.ts`, it's sort of an integration of all the field-level builders. There are also field-level build tests, but not resolvers ones just yet. If you're making changes to the builder just run `yarn test-watch` and hit `p` to provide a pattern, then type "builder", this will isolate that test and if it's passing you probably didn't break anything.
34
-
35
- ## Architecture
36
-
37
- ### Builder
38
-
39
- The builder service is responsible for building out the entire GraphQL schema for a given `.tina` config. This service can run at any time (but needs to be re-run on each schema change) and it's output is a GraphQL schema which can be stored in the schema definition language (SDL) as a string in a database record or as a `.graphql` file. At the top of the schema is a `document` query, this query returns the document, which can be one of any number of templates defined in the `.tina` config. From there, each field in the given template is used to build out the rest of the schema, so each template field is built by the `type` in it's definition
40
-
41
- #### Field-level builders
42
-
43
- Field-level builders take a field definition and produce 4 different GraphQL types:
44
-
45
- ##### `field`
46
-
47
- Builds the type which fits into Tina's field definition shape:
48
-
49
- Given:
50
-
51
- ```yaml
52
- name: Title
53
- label: title
54
- type: text
55
- ```
56
-
57
- ```js
58
- text.build.field({ cache, field });
59
- ```
60
-
61
- Produces
62
-
63
- ```graphql
64
- type TextField {
65
- name: String
66
- label: String
67
- component: String
68
- description: String
69
- }
70
- ```
71
-
72
- ##### `initialValue`
73
-
74
- Tina fields need an initial value when editing existing data. This builder is responsible for providing the shape of that value.
75
-
76
- For most fields this is the same value as `value` - but if you picture the schema as a "graph" - you can see how the "value" of a document reference (ie. a Post has an Author) is not helpful to Tina. Tina only cares about the stored document value of the reference (in this case `/path/to/author.md`) so it's the `initialValue`'s role to provide what makes sense to Tina, regardless of the schema's relationships.
77
-
78
- ##### `value`
79
-
80
- The value of the field, it's the role of this function to provide the shape of the data we should expect for a fully resolved graph.
81
-
82
- For `block` fields, this looks like an array of different shapes, which means it's the `blocks.build.value` function's responsibility to return a `union` array.
83
-
84
- ##### `input`
85
-
86
- When a mutation is made, the shape of this mutation needs to fit the shape create by this function.
87
-
88
- ### Resolvers
89
-
90
- `resolvers` can be thought of as the runtime siblings to `builders`. While it's the job of builders to define the "graph", the resolvers are responsible for taking raw values (like those from a `.md` file) and shaping them so they fit the schema.
91
-
92
- #### Field-level resolvers
93
-
94
- Again, similar to field-level builders, most of the work for resolving the data is passed on to the appropriate field to handle. So if you have a document like so:
95
-
96
- ```md
97
- ---
98
- title: Hello, World!
99
- author: /authors/homer.md
100
- ---
101
- ```
102
-
103
- It's template definition might look like:
104
-
105
- ```yaml
106
- label: Post
107
- ---
108
- fields:
109
- - name: title
110
- label: Title
111
- type: text
112
- - name: author
113
- label: Author
114
- type: select
115
- config:
116
- source:
117
- type: pages
118
- section: authors
119
- ```
120
-
121
- The `text.resolver` object will be responsible for resolving the values related to `title`:
122
-
123
- ##### `field`
124
-
125
- The `field` resolver provides the appropriate values for it's `field` builder counterpart. In the example above the `text.resolve.field` function would return:
126
-
127
- ```json
128
- {
129
- "name": "title",
130
- "label": "Title",
131
- "component": "text"
132
- }
133
- ```
134
-
135
- This would then be passed on to Tina for rendering on the client.
136
-
137
- ##### `initialValue`
138
-
139
- In the example above the `text.resolve.initialValue` would return "Hello, World!"
140
-
141
- For blocks we need to return the object along with a `_template` key, this is used downstream to disambiguate which template the value comes from.
142
-
143
- ##### `value`
144
-
145
- In the example above the `text.resolve.value` would return "Hello, World!", and again, for document references this would return the entire document being referenced, which may or may not be used depending on the graph fields requested
146
-
147
- ##### `input`
148
-
149
- Input resolvers don't do much (except in the case of blocks described later), since the GraphQL mutataion payload has all the necessary information, we just pass the value into these resolvers as a runtime type-check. In the future, this is where field-level validations can take place.
150
-
151
- **Caveats with `blocks`**: `blocks` values are an array of unlike objects, meaning in order to enforce type-safe requests coming into the server, we need to use a somewhat awkward pattern ([read more about the trade-offs here](https://github.com/graphql/graphql-spec/blob/master/rfcs/InputUnion.md#-5-one-of-tagged-union)) which we sort of need to rearrange once it hits the server.
152
-
153
- ## Architecture Diagram
154
-
155
- <iframe style="border:none" width="700" height="350" src="https://whimsical.com/embed/Kh28ULaAYKPRpeCLm3VG63@2Ux7TurymMtzhxz2sLxX"></iframe>
156
-
157
- ## Caveats
158
-
159
- ### Why do we use `GraphQLUnion` instead of `GraphQLInterface` for fields?
160
-
161
- Since `component`, `label`, & `name` are common across all fields, we'd only use a fragment to gather what's unique to that field type, so field definitions using an interface would allow our queries to look like this:
162
-
163
- ```graphql
164
- fields {
165
- component
166
- label
167
- name
168
- ...on SelectField {
169
- options
170
- }
171
- }
172
- ```
173
-
174
- Instead, we use a union - which requires us to load each key inside it's respective fragment:
175
-
176
- ```graphql
177
- fields {
178
- ... on TextareaField {
179
- name
180
- label
181
- component
182
- }
183
- ... on SelectField {
184
- name
185
- label
186
- component
187
- options
188
- }
189
- }
190
- ```
191
-
192
- A GraphQL interface allows you to define a heterogeneous set of types, which have some fields in common. This is a textbook usecase for interfaces, and it's something that could change in the future. But the current reason we're using unions is because unions are exhaustive, and they allow us to scope down the possible field types for a given set of fields.
193
-
194
- An interface would be too broad for our needs, a collection of fields should only contain the types which are possible for that given template config. So while an `interface` would allow us to present **all** possible field types, a `union` gives us the ability to scope down the field list to only allow what the template defines. Using `unions` forces us to be explicit about that in a way that's clear (note: it may be possible to do this with interfaces but there would end up being an interface for each collection of possible fields - making the `interface` term somewhat misleading). Using unions also allows our auto-querybuilder to know that they have populated all possible types of a field, something that seems like it might be more difficult with interfaces.