From 60367269e92b90ef6fe5800eeecd0e1af0c32e61 Mon Sep 17 00:00:00 2001 From: David Matejka Date: Wed, 22 Nov 2023 11:14:39 +0100 Subject: [PATCH 01/17] fix(binding): fix handling errors on reduced has many --- packages/binding/src/core/AccessorErrorManager.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/binding/src/core/AccessorErrorManager.ts b/packages/binding/src/core/AccessorErrorManager.ts index cd3ecacd69..7102fb0be2 100644 --- a/packages/binding/src/core/AccessorErrorManager.ts +++ b/packages/binding/src/core/AccessorErrorManager.ts @@ -162,7 +162,12 @@ export class AccessorErrorManager { } switch (fieldState.type) { case 'entityRealm': - this.setEntityStateErrors(fieldState, child) + // unwrap for reduced has many + if (child.nodeType === 'iNode' && child.children.has(fieldState.entity.id.value)) { + this.setEntityStateErrors(fieldState, child.children.get(fieldState.entity.id.value)!) + } else { + this.setEntityStateErrors(fieldState, child) + } break case 'entityList': this.setEntityListStateErrors(fieldState, child) From 24a12b0d92144145d9a29c7354d42cb60e981741 Mon Sep 17 00:00:00 2001 From: David Matejka Date: Wed, 22 Nov 2023 13:58:01 +0100 Subject: [PATCH 02/17] chore: add missing "types" exports --- packages/react-auto/package.json | 1 + packages/react-binding-ui/package.json | 1 + packages/react-binding/package.json | 1 + packages/react-choice-field-ui/package.json | 1 + packages/react-choice-field/package.json | 1 + packages/react-datagrid-ui/package.json | 1 + packages/react-datagrid/package.json | 1 + packages/react-form-fields-ui/package.json | 1 + packages/react-i18n/package.json | 1 + packages/react-leaflet-fields-ui/package.json | 1 + 10 files changed, 10 insertions(+) diff --git a/packages/react-auto/package.json b/packages/react-auto/package.json index e587363ebc..e93eb247d7 100644 --- a/packages/react-auto/package.json +++ b/packages/react-auto/package.json @@ -9,6 +9,7 @@ "exports": { ".": { "import": { + "types": "./dist/types/index.d.ts", "development": "./dist/development/index.js", "production": "./dist/production/index.js", "default": "./dist/production/index.js" diff --git a/packages/react-binding-ui/package.json b/packages/react-binding-ui/package.json index 09c7ba4c63..6395467729 100644 --- a/packages/react-binding-ui/package.json +++ b/packages/react-binding-ui/package.json @@ -9,6 +9,7 @@ "exports": { ".": { "import": { + "types": "./dist/types/index.d.ts", "development": "./dist/development/index.js", "production": "./dist/production/index.js", "default": "./dist/production/index.js" diff --git a/packages/react-binding/package.json b/packages/react-binding/package.json index a815f9752d..5faff0ee6d 100644 --- a/packages/react-binding/package.json +++ b/packages/react-binding/package.json @@ -8,6 +8,7 @@ "exports": { ".": { "import": { + "types": "./dist/types/index.d.ts", "development": "./dist/development/index.js", "production": "./dist/production/index.js", "default": "./dist/production/index.js" diff --git a/packages/react-choice-field-ui/package.json b/packages/react-choice-field-ui/package.json index 1602e3109e..ebf6a94bbc 100644 --- a/packages/react-choice-field-ui/package.json +++ b/packages/react-choice-field-ui/package.json @@ -9,6 +9,7 @@ "exports": { ".": { "import": { + "types": "./dist/types/index.d.ts", "development": "./dist/development/index.js", "production": "./dist/production/index.js", "default": "./dist/production/index.js" diff --git a/packages/react-choice-field/package.json b/packages/react-choice-field/package.json index 8e84c7d63f..c8779557ab 100644 --- a/packages/react-choice-field/package.json +++ b/packages/react-choice-field/package.json @@ -9,6 +9,7 @@ "exports": { ".": { "import": { + "types": "./dist/types/index.d.ts", "development": "./dist/development/index.js", "production": "./dist/production/index.js", "default": "./dist/production/index.js" diff --git a/packages/react-datagrid-ui/package.json b/packages/react-datagrid-ui/package.json index 55de9e4944..ea2b58a16e 100644 --- a/packages/react-datagrid-ui/package.json +++ b/packages/react-datagrid-ui/package.json @@ -9,6 +9,7 @@ "exports": { ".": { "import": { + "types": "./dist/types/index.d.ts", "development": "./dist/development/index.js", "production": "./dist/production/index.js", "default": "./dist/production/index.js" diff --git a/packages/react-datagrid/package.json b/packages/react-datagrid/package.json index 5cccf42fc0..e72d2231a7 100644 --- a/packages/react-datagrid/package.json +++ b/packages/react-datagrid/package.json @@ -9,6 +9,7 @@ "exports": { ".": { "import": { + "types": "./dist/types/index.d.ts", "development": "./dist/development/index.js", "production": "./dist/production/index.js", "default": "./dist/production/index.js" diff --git a/packages/react-form-fields-ui/package.json b/packages/react-form-fields-ui/package.json index cd24b72aca..064efea0a3 100644 --- a/packages/react-form-fields-ui/package.json +++ b/packages/react-form-fields-ui/package.json @@ -9,6 +9,7 @@ "exports": { ".": { "import": { + "types": "./dist/types/index.d.ts", "development": "./dist/development/index.js", "production": "./dist/production/index.js", "default": "./dist/production/index.js" diff --git a/packages/react-i18n/package.json b/packages/react-i18n/package.json index 59776cdfb7..c9ba57a356 100644 --- a/packages/react-i18n/package.json +++ b/packages/react-i18n/package.json @@ -9,6 +9,7 @@ "exports": { ".": { "import": { + "types": "./dist/types/index.d.ts", "development": "./dist/development/index.js", "production": "./dist/production/index.js", "default": "./dist/production/index.js" diff --git a/packages/react-leaflet-fields-ui/package.json b/packages/react-leaflet-fields-ui/package.json index d7b18f77e6..b12d6f7c84 100644 --- a/packages/react-leaflet-fields-ui/package.json +++ b/packages/react-leaflet-fields-ui/package.json @@ -9,6 +9,7 @@ "exports": { ".": { "import": { + "types": "./dist/types/index.d.ts", "development": "./dist/development/index.js", "production": "./dist/production/index.js", "default": "./dist/production/index.js" From cadb917866467a20a9e2c2b0abb9b43c9740e1f0 Mon Sep 17 00:00:00 2001 From: David Matejka Date: Wed, 22 Nov 2023 13:59:43 +0100 Subject: [PATCH 03/17] chore: create cjs build --- build/createViteConfig.js | 18 +++++++++++++----- packages/.template/package.json | 6 ++++++ packages/admin-i18n/package.json | 6 ++++++ packages/admin/package.json | 6 ++++++ packages/binding/package.json | 6 ++++++ packages/brand/package.json | 6 ++++++ packages/client/package.json | 6 ++++++ packages/interface-tester/package.json | 6 ++++++ packages/layout/package.json | 6 ++++++ packages/react-auto/package.json | 6 ++++++ packages/react-binding-ui/package.json | 6 ++++++ packages/react-binding/package.json | 6 ++++++ packages/react-choice-field-ui/package.json | 6 ++++++ packages/react-choice-field/package.json | 6 ++++++ packages/react-client/package.json | 6 ++++++ packages/react-datagrid-ui/package.json | 6 ++++++ packages/react-datagrid/package.json | 6 ++++++ packages/react-form-fields-ui/package.json | 6 ++++++ packages/react-i18n/package.json | 6 ++++++ packages/react-leaflet-fields-ui/package.json | 6 ++++++ .../react-multipass-rendering/package.json | 6 ++++++ packages/react-utils/package.json | 6 ++++++ packages/ui/package.json | 6 ++++++ packages/utilities/package.json | 6 ++++++ packages/vimeo-file-uploader/package.json | 6 ++++++ packages/vite-plugin/package.json | 6 ++++++ 26 files changed, 163 insertions(+), 5 deletions(-) diff --git a/build/createViteConfig.js b/build/createViteConfig.js index 5463689262..dc227dcd1a 100644 --- a/build/createViteConfig.js +++ b/build/createViteConfig.js @@ -25,7 +25,7 @@ export function createViteConfig(packageName) { build: { lib: { entry, - formats: ['es'], + formats: ['es', 'cjs'], }, minify: false, outDir: resolve(rootDirectory, `${packagePath}/dist/${mode}`), @@ -33,10 +33,18 @@ export function createViteConfig(packageName) { external: (id, importer, resolved) => { return !resolved && !id.startsWith('./') && !id.startsWith('../') && id !== '.' && id !== entry }, - output: { - preserveModules: true, - entryFileNames: '[name].js', - }, + output: [ + { + format: 'esm', + preserveModules: true, + entryFileNames: '[name].js', + }, + { + format: 'cjs', + preserveModules: true, + entryFileNames: '[name].cjs', + }, + ], treeshake: { moduleSideEffects: false, }, diff --git a/packages/.template/package.json b/packages/.template/package.json index c617450cb4..f16d6d327e 100644 --- a/packages/.template/package.json +++ b/packages/.template/package.json @@ -13,6 +13,12 @@ "development": "./dist/development/index.js", "production": "./dist/production/index.js", "default": "./dist/production/index.js" + }, + "require": { + "types": "./dist/types/index.d.ts", + "development": "./dist/development/index.cjs", + "production": "./dist/production/index.cjs", + "default": "./dist/production/index.cjs" } } }, diff --git a/packages/admin-i18n/package.json b/packages/admin-i18n/package.json index 2b620495d2..7bc64a88f6 100644 --- a/packages/admin-i18n/package.json +++ b/packages/admin-i18n/package.json @@ -12,6 +12,12 @@ "development": "./dist/development/index.js", "production": "./dist/production/index.js", "default": "./dist/production/index.js" + }, + "require": { + "types": "./dist/types/index.d.ts", + "development": "./dist/development/index.cjs", + "production": "./dist/production/index.cjs", + "default": "./dist/production/index.cjs" } } }, diff --git a/packages/admin/package.json b/packages/admin/package.json index 69fbef1142..f263ab0d37 100644 --- a/packages/admin/package.json +++ b/packages/admin/package.json @@ -14,6 +14,12 @@ "development": "./dist/development/index.js", "production": "./dist/production/index.js", "default": "./dist/production/index.js" + }, + "require": { + "types": "./dist/types/index.d.ts", + "development": "./dist/development/index.cjs", + "production": "./dist/production/index.cjs", + "default": "./dist/production/index.cjs" } }, "./index.css": { diff --git a/packages/binding/package.json b/packages/binding/package.json index aeea37a32b..d8f64b7967 100644 --- a/packages/binding/package.json +++ b/packages/binding/package.json @@ -12,6 +12,12 @@ "development": "./dist/development/index.js", "production": "./dist/production/index.js", "default": "./dist/production/index.js" + }, + "require": { + "types": "./dist/types/index.d.ts", + "development": "./dist/development/index.cjs", + "production": "./dist/production/index.cjs", + "default": "./dist/production/index.cjs" } } }, diff --git a/packages/brand/package.json b/packages/brand/package.json index f79fbf3eb5..cc8939a8c5 100644 --- a/packages/brand/package.json +++ b/packages/brand/package.json @@ -12,6 +12,12 @@ "development": "./dist/development/index.js", "production": "./dist/production/index.js", "default": "./dist/production/index.js" + }, + "require": { + "types": "./dist/types/index.d.ts", + "development": "./dist/development/index.cjs", + "production": "./dist/production/index.cjs", + "default": "./dist/production/index.cjs" } }, "./index.css": { diff --git a/packages/client/package.json b/packages/client/package.json index 85deee17b3..889b081622 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -10,6 +10,12 @@ "development": "./dist/development/index.js", "production": "./dist/production/index.js", "default": "./dist/production/index.js" + }, + "require": { + "types": "./dist/types/index.d.ts", + "development": "./dist/development/index.cjs", + "production": "./dist/production/index.cjs", + "default": "./dist/production/index.cjs" } } }, diff --git a/packages/interface-tester/package.json b/packages/interface-tester/package.json index a161e5c2cf..329cadeaf1 100644 --- a/packages/interface-tester/package.json +++ b/packages/interface-tester/package.json @@ -12,6 +12,12 @@ "development": "./dist/development/index.js", "production": "./dist/production/index.js", "default": "./dist/production/index.js" + }, + "require": { + "types": "./dist/types/index.d.ts", + "development": "./dist/development/index.cjs", + "production": "./dist/production/index.cjs", + "default": "./dist/production/index.cjs" } } }, diff --git a/packages/layout/package.json b/packages/layout/package.json index cf6119b382..98ba7f1ebc 100644 --- a/packages/layout/package.json +++ b/packages/layout/package.json @@ -12,6 +12,12 @@ "development": "./dist/development/index.js", "production": "./dist/production/index.js", "default": "./dist/production/index.js" + }, + "require": { + "types": "./dist/types/index.d.ts", + "development": "./dist/development/index.cjs", + "production": "./dist/production/index.cjs", + "default": "./dist/production/index.cjs" } }, "./index.css": { diff --git a/packages/react-auto/package.json b/packages/react-auto/package.json index e93eb247d7..fe89d64f49 100644 --- a/packages/react-auto/package.json +++ b/packages/react-auto/package.json @@ -13,6 +13,12 @@ "development": "./dist/development/index.js", "production": "./dist/production/index.js", "default": "./dist/production/index.js" + }, + "require": { + "types": "./dist/types/index.d.ts", + "development": "./dist/development/index.cjs", + "production": "./dist/production/index.cjs", + "default": "./dist/production/index.cjs" } } }, diff --git a/packages/react-binding-ui/package.json b/packages/react-binding-ui/package.json index 6395467729..77ca355964 100644 --- a/packages/react-binding-ui/package.json +++ b/packages/react-binding-ui/package.json @@ -13,6 +13,12 @@ "development": "./dist/development/index.js", "production": "./dist/production/index.js", "default": "./dist/production/index.js" + }, + "require": { + "types": "./dist/types/index.d.ts", + "development": "./dist/development/index.cjs", + "production": "./dist/production/index.cjs", + "default": "./dist/production/index.cjs" } } }, diff --git a/packages/react-binding/package.json b/packages/react-binding/package.json index 5faff0ee6d..a1402c3972 100644 --- a/packages/react-binding/package.json +++ b/packages/react-binding/package.json @@ -12,6 +12,12 @@ "development": "./dist/development/index.js", "production": "./dist/production/index.js", "default": "./dist/production/index.js" + }, + "require": { + "types": "./dist/types/index.d.ts", + "development": "./dist/development/index.cjs", + "production": "./dist/production/index.cjs", + "default": "./dist/production/index.cjs" } } }, diff --git a/packages/react-choice-field-ui/package.json b/packages/react-choice-field-ui/package.json index ebf6a94bbc..cbd752648d 100644 --- a/packages/react-choice-field-ui/package.json +++ b/packages/react-choice-field-ui/package.json @@ -13,6 +13,12 @@ "development": "./dist/development/index.js", "production": "./dist/production/index.js", "default": "./dist/production/index.js" + }, + "require": { + "types": "./dist/types/index.d.ts", + "development": "./dist/development/index.cjs", + "production": "./dist/production/index.cjs", + "default": "./dist/production/index.cjs" } } }, diff --git a/packages/react-choice-field/package.json b/packages/react-choice-field/package.json index c8779557ab..7972f5a667 100644 --- a/packages/react-choice-field/package.json +++ b/packages/react-choice-field/package.json @@ -13,6 +13,12 @@ "development": "./dist/development/index.js", "production": "./dist/production/index.js", "default": "./dist/production/index.js" + }, + "require": { + "types": "./dist/types/index.d.ts", + "development": "./dist/development/index.cjs", + "production": "./dist/production/index.cjs", + "default": "./dist/production/index.cjs" } } }, diff --git a/packages/react-client/package.json b/packages/react-client/package.json index ba15bfcba6..7601c9bf8b 100644 --- a/packages/react-client/package.json +++ b/packages/react-client/package.json @@ -12,6 +12,12 @@ "development": "./dist/development/index.js", "production": "./dist/production/index.js", "default": "./dist/production/index.js" + }, + "require": { + "types": "./dist/types/index.d.ts", + "development": "./dist/development/index.cjs", + "production": "./dist/production/index.cjs", + "default": "./dist/production/index.cjs" } } }, diff --git a/packages/react-datagrid-ui/package.json b/packages/react-datagrid-ui/package.json index ea2b58a16e..9bbd0177ec 100644 --- a/packages/react-datagrid-ui/package.json +++ b/packages/react-datagrid-ui/package.json @@ -13,6 +13,12 @@ "development": "./dist/development/index.js", "production": "./dist/production/index.js", "default": "./dist/production/index.js" + }, + "require": { + "types": "./dist/types/index.d.ts", + "development": "./dist/development/index.cjs", + "production": "./dist/production/index.cjs", + "default": "./dist/production/index.cjs" } } }, diff --git a/packages/react-datagrid/package.json b/packages/react-datagrid/package.json index e72d2231a7..453bfb229c 100644 --- a/packages/react-datagrid/package.json +++ b/packages/react-datagrid/package.json @@ -13,6 +13,12 @@ "development": "./dist/development/index.js", "production": "./dist/production/index.js", "default": "./dist/production/index.js" + }, + "require": { + "types": "./dist/types/index.d.ts", + "development": "./dist/development/index.cjs", + "production": "./dist/production/index.cjs", + "default": "./dist/production/index.cjs" } } }, diff --git a/packages/react-form-fields-ui/package.json b/packages/react-form-fields-ui/package.json index 064efea0a3..cd8ce7e45f 100644 --- a/packages/react-form-fields-ui/package.json +++ b/packages/react-form-fields-ui/package.json @@ -13,6 +13,12 @@ "development": "./dist/development/index.js", "production": "./dist/production/index.js", "default": "./dist/production/index.js" + }, + "require": { + "types": "./dist/types/index.d.ts", + "development": "./dist/development/index.cjs", + "production": "./dist/production/index.cjs", + "default": "./dist/production/index.cjs" } } }, diff --git a/packages/react-i18n/package.json b/packages/react-i18n/package.json index c9ba57a356..6767c16422 100644 --- a/packages/react-i18n/package.json +++ b/packages/react-i18n/package.json @@ -13,6 +13,12 @@ "development": "./dist/development/index.js", "production": "./dist/production/index.js", "default": "./dist/production/index.js" + }, + "require": { + "types": "./dist/types/index.d.ts", + "development": "./dist/development/index.cjs", + "production": "./dist/production/index.cjs", + "default": "./dist/production/index.cjs" } } }, diff --git a/packages/react-leaflet-fields-ui/package.json b/packages/react-leaflet-fields-ui/package.json index b12d6f7c84..b9c57bf1e6 100644 --- a/packages/react-leaflet-fields-ui/package.json +++ b/packages/react-leaflet-fields-ui/package.json @@ -13,6 +13,12 @@ "development": "./dist/development/index.js", "production": "./dist/production/index.js", "default": "./dist/production/index.js" + }, + "require": { + "types": "./dist/types/index.d.ts", + "development": "./dist/development/index.cjs", + "production": "./dist/production/index.cjs", + "default": "./dist/production/index.cjs" } } }, diff --git a/packages/react-multipass-rendering/package.json b/packages/react-multipass-rendering/package.json index 284fc6814c..d73913c1a6 100644 --- a/packages/react-multipass-rendering/package.json +++ b/packages/react-multipass-rendering/package.json @@ -12,6 +12,12 @@ "development": "./dist/development/index.js", "production": "./dist/production/index.js", "default": "./dist/production/index.js" + }, + "require": { + "types": "./dist/types/index.d.ts", + "development": "./dist/development/index.cjs", + "production": "./dist/production/index.cjs", + "default": "./dist/production/index.cjs" } } }, diff --git a/packages/react-utils/package.json b/packages/react-utils/package.json index 545737a743..3830af40f5 100644 --- a/packages/react-utils/package.json +++ b/packages/react-utils/package.json @@ -12,6 +12,12 @@ "development": "./dist/development/index.js", "production": "./dist/production/index.js", "default": "./dist/production/index.js" + }, + "require": { + "types": "./dist/types/index.d.ts", + "development": "./dist/development/index.cjs", + "production": "./dist/production/index.cjs", + "default": "./dist/production/index.cjs" } } }, diff --git a/packages/ui/package.json b/packages/ui/package.json index b553491809..bf2c970663 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -14,6 +14,12 @@ "development": "./dist/development/index.js", "production": "./dist/production/index.js", "default": "./dist/production/index.js" + }, + "require": { + "types": "./dist/types/index.d.ts", + "development": "./dist/development/index.cjs", + "production": "./dist/production/index.cjs", + "default": "./dist/production/index.cjs" } }, "./index.css": { diff --git a/packages/utilities/package.json b/packages/utilities/package.json index 7a805cc18c..525ca14768 100644 --- a/packages/utilities/package.json +++ b/packages/utilities/package.json @@ -12,6 +12,12 @@ "development": "./dist/development/index.js", "production": "./dist/production/index.js", "default": "./dist/production/index.js" + }, + "require": { + "types": "./dist/types/index.d.ts", + "development": "./dist/development/index.cjs", + "production": "./dist/production/index.cjs", + "default": "./dist/production/index.cjs" } } }, diff --git a/packages/vimeo-file-uploader/package.json b/packages/vimeo-file-uploader/package.json index 9da804030a..203d4724d9 100644 --- a/packages/vimeo-file-uploader/package.json +++ b/packages/vimeo-file-uploader/package.json @@ -10,6 +10,12 @@ "development": "./dist/development/index.js", "production": "./dist/production/index.js", "default": "./dist/production/index.js" + }, + "require": { + "types": "./dist/types/index.d.ts", + "development": "./dist/development/index.cjs", + "production": "./dist/production/index.cjs", + "default": "./dist/production/index.cjs" } } }, diff --git a/packages/vite-plugin/package.json b/packages/vite-plugin/package.json index 858abdd749..9af559e154 100644 --- a/packages/vite-plugin/package.json +++ b/packages/vite-plugin/package.json @@ -12,6 +12,12 @@ "development": "./dist/development/index.js", "production": "./dist/production/index.js", "default": "./dist/production/index.js" + }, + "require": { + "types": "./dist/types/index.d.ts", + "development": "./dist/development/index.cjs", + "production": "./dist/production/index.cjs", + "default": "./dist/production/index.cjs" } } }, From cd002e532a72694fb0477b3d79957863b66cbab8 Mon Sep 17 00:00:00 2001 From: David Matejka Date: Wed, 22 Nov 2023 15:50:34 +0100 Subject: [PATCH 04/17] chore: use node 18 --- .github/workflows/ci.yaml | 4 +-- docker-compose.yaml | 8 +++--- ee/admin-server/Dockerfile | 4 +-- ee/admin-server/package.json | 2 +- package.json | 2 +- packages/admin/package.json | 2 +- packages/interface-tester/package.json | 2 +- packages/ui/package.json | 2 +- scripts/ae/ae.dockerfile | 2 +- yarn.lock | 34 ++++++++++++++------------ 10 files changed, 32 insertions(+), 30 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8e0a93bbc6..73a0e72d58 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -37,7 +37,7 @@ jobs: - job_name: dedupe run_script: yarn dedupe --check - container: node:16-alpine + container: node:18-alpine steps: - name: Checkout code uses: actions/checkout@v3 @@ -172,7 +172,7 @@ jobs: if: github.event_name == 'push' && github.ref_type == 'tag' needs: [test, playwright] runs-on: ubuntu-latest - container: node:16-alpine + container: node:18-alpine env: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/docker-compose.yaml b/docker-compose.yaml index 62dd04f239..f4e8b6c432 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,6 +1,6 @@ services: sandbox-vite: - image: node:16-alpine + image: node:18-alpine command: 'npm run start' environment: @@ -14,7 +14,7 @@ services: admin-server: - image: node:16-alpine + image: node:18-alpine command: 'npm run start' working_dir: /src/ee/admin-server @@ -45,7 +45,7 @@ services: admin-server-vite: - image: node:16-alpine + image: node:18-alpine command: 'npm run watch:public' working_dir: /src/ee/admin-server @@ -53,7 +53,7 @@ services: - .:/src:cached playwright: - image: node:16-alpine + image: node:18-alpine command: [ 'npm', 'run', 'pw:dev' ] user: '1000:1000' diff --git a/ee/admin-server/Dockerfile b/ee/admin-server/Dockerfile index 799bb9a973..9c613b9656 100644 --- a/ee/admin-server/Dockerfile +++ b/ee/admin-server/Dockerfile @@ -1,4 +1,4 @@ -FROM node:16-alpine as builder +FROM node:18-alpine as builder WORKDIR /src COPY package.json yarn.lock .yarnrc.yml ./ @@ -40,7 +40,7 @@ COPY ./ ./ RUN yarn as:build -FROM node:16-alpine +FROM node:18-alpine WORKDIR /src COPY --from=builder /src/ee/admin-server/dist/ ./ diff --git a/ee/admin-server/package.json b/ee/admin-server/package.json index d82af00e28..782c7a29fc 100644 --- a/ee/admin-server/package.json +++ b/ee/admin-server/package.json @@ -32,7 +32,7 @@ "@contember/admin": "workspace:*", "@types/cookie": "^0.4.1", "@types/mime": "^2.0.3", - "@types/node": "^16.11.1", + "@types/node": "^18", "@types/node-fetch": "^2.5.12", "@types/ws": "^8.2.0" } diff --git a/package.json b/package.json index ccc21ee500..20440b3af2 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "build": "yarn run ts:build && yarn workspaces foreach -pt run build", "changeset": "changeset", "check": "./scripts/runChecks.sh", - "reinstall": "docker run --rm --interactive --tty --network=host -v \"$PWD:$PWD\" -w \"$PWD\" -u \"1000\" node:16-alpine npx yarn@2 install", + "reinstall": "docker run --rm --interactive --tty --network=host -v \"$PWD:$PWD\" -w \"$PWD\" -u \"1000\" node:18-alpine npx yarn@2 install", "clean": "yarn run clean:generated && yarn run clean:dependencies", "clean:dependencies": "rm -rf ee/*/node_modules || true; rm -rf packages/*/node_modules || true; rm -rf node_modules || true", "clean:generated": "rm -rf dist || true; rm -rf ee/*/dist || true; rm -rf ee/*/temp || true; packages/*/dist || true; rm -rf packages/*/temp || true", diff --git a/packages/admin/package.json b/packages/admin/package.json index f263ab0d37..ca059293c4 100644 --- a/packages/admin/package.json +++ b/packages/admin/package.json @@ -98,7 +98,7 @@ "@types/blueimp-md5": "2.18.0", "@types/is-hotkey": "0.1.5", "@types/leaflet": "^1.9.0", - "@types/node": "^16", + "@types/node": "^18", "@types/node-fetch": "2.6.1", "@types/react": "^18", "@types/react-dom": "^18", diff --git a/packages/interface-tester/package.json b/packages/interface-tester/package.json index 329cadeaf1..00aa8b4729 100644 --- a/packages/interface-tester/package.json +++ b/packages/interface-tester/package.json @@ -46,6 +46,6 @@ }, "devDependencies": { "@types/micromatch": "^4.0.2", - "@types/node": "^16.11.1" + "@types/node": "^18" } } diff --git a/packages/ui/package.json b/packages/ui/package.json index bf2c970663..aeb9fa70d7 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -73,7 +73,7 @@ "@storybook/react-vite": "^7.0.7", "@storybook/testing-library": "^0.0.14-next.2", "@testing-library/react-hooks": "^8.0.1", - "@types/node": "15.0.2", + "@types/node": "^18", "@types/react": "^18", "@types/react-dom": "^18", "babel-loader": "8.2.2", diff --git a/scripts/ae/ae.dockerfile b/scripts/ae/ae.dockerfile index 7d99883735..0548e802eb 100644 --- a/scripts/ae/ae.dockerfile +++ b/scripts/ae/ae.dockerfile @@ -1,4 +1,4 @@ -FROM node:16-alpine AS build +FROM node:18-alpine AS build WORKDIR /src COPY . /src diff --git a/yarn.lock b/yarn.lock index b25ba5594b..1163ff054c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2689,7 +2689,7 @@ __metadata: "@contember/dic": ^1.2.0 "@types/cookie": ^0.4.1 "@types/mime": ^2.0.3 - "@types/node": ^16.11.1 + "@types/node": ^18 "@types/node-fetch": ^2.5.12 "@types/ws": ^8.2.0 cookie: ^0.4.1 @@ -2730,7 +2730,7 @@ __metadata: "@types/blueimp-md5": 2.18.0 "@types/is-hotkey": 0.1.5 "@types/leaflet": ^1.9.0 - "@types/node": ^16 + "@types/node": ^18 "@types/node-fetch": 2.6.1 "@types/react": ^18 "@types/react-dom": ^18 @@ -2833,7 +2833,7 @@ __metadata: "@contember/schema": ^1.2.0 "@contember/schema-utils": ^1.2.0 "@types/micromatch": ^4.0.2 - "@types/node": ^16.11.1 + "@types/node": ^18 fast-glob: ^3.2.12 micromatch: ^4.0.5 peerDependencies: @@ -3153,7 +3153,7 @@ __metadata: "@storybook/react-vite": ^7.0.7 "@storybook/testing-library": ^0.0.14-next.2 "@testing-library/react-hooks": ^8.0.1 - "@types/node": 15.0.2 + "@types/node": ^18 "@types/react": ^18 "@types/react-dom": ^18 babel-loader: 8.2.2 @@ -6961,21 +6961,16 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*": - version: 18.14.6 - resolution: "@types/node@npm:18.14.6" - checksum: 2f88f482cabadc6dbddd627a1674239e68c3c9beab56eb4ae2309fb96fd17fc3a509d99b0309bafe13b58529574f49ecf3a583f2ebe2896dd32fe4be436dc96e - languageName: node - linkType: hard - -"@types/node@npm:15.0.2": - version: 15.0.2 - resolution: "@types/node@npm:15.0.2" - checksum: 273565e1da341b248e08bec105667f0ab8ef89e347a47bbd6e18e35c0946788a53d581e0e3d8b8e3b3b5809db3237fcbf1bbb2251cf1506e5f81622739cb9ff1 +"@types/node@npm:*, @types/node@npm:^18": + version: 18.18.12 + resolution: "@types/node@npm:18.18.12" + dependencies: + undici-types: ~5.26.4 + checksum: 59bb2e94b096761647fe70c79a3134f4d5f6017a493884beeeb525a9047c055db41dc5b0deb91e92160891cb84290f4da8a8ab4cc037f5341ec6ad48eb24ce10 languageName: node linkType: hard -"@types/node@npm:^16, @types/node@npm:^16.0.0, @types/node@npm:^16.11.1": +"@types/node@npm:^16.0.0": version: 16.18.25 resolution: "@types/node@npm:16.18.25" checksum: 181760ad6b54fcc498dfeb249e98bbf0be51d7c35e92e760e1a82004fa42b86e8c33a8f8dd7743b5ef872bda0753d9e6a5b8e3f0aed63e9eb79b4e65760c1fbe @@ -16360,6 +16355,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~5.26.4": + version: 5.26.5 + resolution: "undici-types@npm:5.26.5" + checksum: 3192ef6f3fd5df652f2dc1cd782b49d6ff14dc98e5dced492aa8a8c65425227da5da6aafe22523c67f035a272c599bb89cfe803c1db6311e44bed3042fc25487 + languageName: node + linkType: hard + "unfetch@npm:^4.2.0": version: 4.2.0 resolution: "unfetch@npm:4.2.0" From 3047d3023e8c5f1fa1925d2bd5e9ae1a9bbbfbbf Mon Sep 17 00:00:00 2001 From: David Matejka Date: Wed, 22 Nov 2023 15:51:42 +0100 Subject: [PATCH 05/17] feat(client): add new contember content client --- build/packageList.js | 1 + ee/admin-server/Dockerfile | 1 + packages/client-generator/api-extractor.json | 3 + packages/client-generator/package.json | 55 ++ .../src/ContemberClientGenerator.ts | 48 ++ .../src/EntityTypeSchemaGenerator.ts | 182 +++++ .../src/EnumTypeSchemaGenerator.ts | 12 + .../src/NameSchemaGenerator.ts | 41 ++ packages/client-generator/src/generate.ts | 26 + packages/client-generator/src/index.ts | 4 + packages/client-generator/src/tsconfig.json | 10 + .../tests/generateEnittyTypes.test.ts | 303 ++++++++ .../tests/generateEnumTypes.test.ts | 23 + packages/client-generator/tests/schemas.ts | 73 ++ packages/client-generator/tests/tsconfig.json | 10 + packages/client-generator/tsconfig.json | 8 + packages/client-generator/tsdoc.json | 6 + packages/client-generator/vite.config.js | 21 + .../client/src/builder/GraphQlQueryPrinter.ts | 137 ++++ packages/client/src/builder/index.ts | 3 + .../client/src/builder/nodes/GraphQlField.ts | 31 + .../src/builder/nodes/GraphQlFragment.ts | 13 + .../builder/nodes/GraphQlFragmentSpread.ts | 9 + .../builder/nodes/GraphQlInlineFragment.ts | 12 + packages/client/src/builder/nodes/index.ts | 4 + packages/client/src/builder/types/json.ts | 4 + .../src/content/client/ContentClient.ts | 185 +++++ .../src/content/client/ContentQueryBuilder.ts | 153 ++++ .../client/TypedContentQueryBuilder.ts | 62 ++ packages/client/src/content/client/index.ts | 6 + .../client/nodes/ContentEntitySelection.ts | 247 +++++++ .../content/client/nodes/ContentMutation.ts | 23 + .../src/content/client/nodes/ContentQuery.ts | 24 + .../client/nodes/TypedEntitySelection.ts | 107 +++ .../client/src/content/client/nodes/index.ts | 4 + .../client/src/content/client/types/Input.ts | 213 ++++++ .../client/src/content/client/types/Result.ts | 43 ++ .../client/src/content/client/types/Schema.ts | 49 ++ .../client/src/content/client/types/index.ts | 3 + .../content/client/utils/createListArgs.ts | 23 + .../content/client/utils/createTypedArgs.ts | 15 + .../content/client/utils/mutationFragments.ts | 52 ++ .../client/utils/replaceGraphQlLiteral.ts | 23 + packages/client/src/content/index.ts | 2 +- .../client/src/graphQlClient/GraphQlClient.ts | 139 +++- packages/client/src/index.ts | 1 + .../generateUploadUrlMutationBuilder.spec.ts | 36 +- packages/client/tests/cases/unit/lib.ts | 134 ++++ .../client/tests/cases/unit/mutation.test.ts | 670 ++++++++++++++++++ .../client/tests/cases/unit/query.test.ts | 192 +++++ tsconfig.json | 1 + yarn.lock | 14 + 52 files changed, 3431 insertions(+), 30 deletions(-) create mode 100644 packages/client-generator/api-extractor.json create mode 100644 packages/client-generator/package.json create mode 100644 packages/client-generator/src/ContemberClientGenerator.ts create mode 100644 packages/client-generator/src/EntityTypeSchemaGenerator.ts create mode 100644 packages/client-generator/src/EnumTypeSchemaGenerator.ts create mode 100644 packages/client-generator/src/NameSchemaGenerator.ts create mode 100644 packages/client-generator/src/generate.ts create mode 100644 packages/client-generator/src/index.ts create mode 100644 packages/client-generator/src/tsconfig.json create mode 100644 packages/client-generator/tests/generateEnittyTypes.test.ts create mode 100644 packages/client-generator/tests/generateEnumTypes.test.ts create mode 100644 packages/client-generator/tests/schemas.ts create mode 100644 packages/client-generator/tests/tsconfig.json create mode 100644 packages/client-generator/tsconfig.json create mode 100644 packages/client-generator/tsdoc.json create mode 100644 packages/client-generator/vite.config.js create mode 100644 packages/client/src/builder/GraphQlQueryPrinter.ts create mode 100644 packages/client/src/builder/index.ts create mode 100644 packages/client/src/builder/nodes/GraphQlField.ts create mode 100644 packages/client/src/builder/nodes/GraphQlFragment.ts create mode 100644 packages/client/src/builder/nodes/GraphQlFragmentSpread.ts create mode 100644 packages/client/src/builder/nodes/GraphQlInlineFragment.ts create mode 100644 packages/client/src/builder/nodes/index.ts create mode 100644 packages/client/src/builder/types/json.ts create mode 100644 packages/client/src/content/client/ContentClient.ts create mode 100644 packages/client/src/content/client/ContentQueryBuilder.ts create mode 100644 packages/client/src/content/client/TypedContentQueryBuilder.ts create mode 100644 packages/client/src/content/client/index.ts create mode 100644 packages/client/src/content/client/nodes/ContentEntitySelection.ts create mode 100644 packages/client/src/content/client/nodes/ContentMutation.ts create mode 100644 packages/client/src/content/client/nodes/ContentQuery.ts create mode 100644 packages/client/src/content/client/nodes/TypedEntitySelection.ts create mode 100644 packages/client/src/content/client/nodes/index.ts create mode 100644 packages/client/src/content/client/types/Input.ts create mode 100644 packages/client/src/content/client/types/Result.ts create mode 100644 packages/client/src/content/client/types/Schema.ts create mode 100644 packages/client/src/content/client/types/index.ts create mode 100644 packages/client/src/content/client/utils/createListArgs.ts create mode 100644 packages/client/src/content/client/utils/createTypedArgs.ts create mode 100644 packages/client/src/content/client/utils/mutationFragments.ts create mode 100644 packages/client/src/content/client/utils/replaceGraphQlLiteral.ts create mode 100644 packages/client/tests/cases/unit/lib.ts create mode 100644 packages/client/tests/cases/unit/mutation.test.ts create mode 100644 packages/client/tests/cases/unit/query.test.ts diff --git a/build/packageList.js b/build/packageList.js index f44c2fe92c..e4cd791f09 100644 --- a/build/packageList.js +++ b/build/packageList.js @@ -21,6 +21,7 @@ export const list = { 'binding', 'brand', 'client', + 'client-generator', 'interface-tester', 'layout', 'react-auto', diff --git a/ee/admin-server/Dockerfile b/ee/admin-server/Dockerfile index 9c613b9656..a582019ef6 100644 --- a/ee/admin-server/Dockerfile +++ b/ee/admin-server/Dockerfile @@ -14,6 +14,7 @@ COPY packages/admin-sandbox/package.json ./packages/admin-sandbox/package.json COPY packages/binding/package.json ./packages/binding/package.json COPY packages/brand/package.json ./packages/brand/package.json COPY packages/client/package.json ./packages/client/package.json +COPY packages/client-generator/package.json ./packages/client-generator/package.json COPY packages/interface-tester/package.json ./packages/interface-tester/package.json COPY packages/layout/package.json ./packages/layout/package.json COPY packages/react-auto/package.json ./packages/react-auto/package.json diff --git a/packages/client-generator/api-extractor.json b/packages/client-generator/api-extractor.json new file mode 100644 index 0000000000..66c17dd719 --- /dev/null +++ b/packages/client-generator/api-extractor.json @@ -0,0 +1,3 @@ +{ + "extends": "../../build/api-extractor.json" +} diff --git a/packages/client-generator/package.json b/packages/client-generator/package.json new file mode 100644 index 0000000000..59a2f58b95 --- /dev/null +++ b/packages/client-generator/package.json @@ -0,0 +1,55 @@ +{ + "name": "@contember/client-generator", + "license": "Apache-2.0", + "version": "0.0.0", + "private": true, + "type": "module", + "sideEffects": false, + "main": "./dist/production/index.js", + "bin": { + "contember-client-generator": "./dist/production/generate.js" + }, + "exports": { + ".": { + "import": { + "types": "./dist/types/index.d.ts", + "development": "./dist/development/index.js", + "production": "./dist/production/index.js", + "default": "./dist/production/index.js" + }, + "require": { + "types": "./dist/types/index.d.ts", + "development": "./dist/development/index.cjs", + "production": "./dist/production/index.cjs", + "default": "./dist/production/index.cjs" + } + } + }, + "files": [ + "dist/", + "src/" + ], + "typings": "./dist/types/index.d.ts", + "scripts": { + "build": "yarn build:js:dev && yarn build:js:prod", + "build:js:dev": "vite build --mode development", + "build:js:prod": "vite build --mode production", + "ae:build": "api-extractor run --local", + "ae:test": "api-extractor run", + "test": "vitest" + }, + "dependencies": { + "@contember/client": "workspace:*", + "@contember/schema": "^1.2.0", + "@contember/schema-utils": "^1.2.0" + }, + "devDependencies": { + "@contember/schema-definition": "^1.2.0", + "@types/node": "^18" + }, + "repository": { + "type": "git", + "url": "https://github.com/contember/interface.git", + "directory": "packages/client-generator" + } +} diff --git a/packages/client-generator/src/ContemberClientGenerator.ts b/packages/client-generator/src/ContemberClientGenerator.ts new file mode 100644 index 0000000000..b9c699661b --- /dev/null +++ b/packages/client-generator/src/ContemberClientGenerator.ts @@ -0,0 +1,48 @@ +import { Model } from '@contember/schema' +import { EnumTypeSchemaGenerator } from './EnumTypeSchemaGenerator' +import { EntityTypeSchemaGenerator } from './EntityTypeSchemaGenerator' +import { NameSchemaGenerator } from './NameSchemaGenerator' + + +export class ContemberClientGenerator { + constructor( + private readonly nameSchemaGenerator: NameSchemaGenerator = new NameSchemaGenerator(), + private readonly enumTypeSchemaGenerator: EnumTypeSchemaGenerator = new EnumTypeSchemaGenerator(), + private readonly entityTypeSchemaGenerator: EntityTypeSchemaGenerator = new EntityTypeSchemaGenerator(), + ) { + } + + generate(model: Model.Schema): Record { + const nameSchema = this.nameSchemaGenerator.generate(model) + const enumTypeSchema = this.enumTypeSchemaGenerator.generate(model) + const entityTypeSchema = this.entityTypeSchemaGenerator.generate(model) + + const namesCode = `import { SchemaNames } from '@contember/client' +export const ContemberClientNames: SchemaNames = ` + JSON.stringify(nameSchema, null, 2) + + const indexCode = ` +import { ContemberClientNames } from './names' +import { ContemberClientSchema } from './entities' +import { ContentQueryBuilder, TypedContentQueryBuilder, TypedEntitySelection } from '@contember/client' +export * from './names' +export * from './enums' +export * from './entities' + +export const queryBuilder = new ContentQueryBuilder(ContemberClientNames) as unknown as TypedContentQueryBuilder + +export type FragmentOf = +TypedEntitySelection + +export type FragmentType = any> = +T extends TypedEntitySelection + ? TFields + : never +` + return { + 'names.ts': namesCode, + 'enums.ts': enumTypeSchema, + 'entities.ts': entityTypeSchema, + 'index.ts': indexCode, + } + } +} diff --git a/packages/client-generator/src/EntityTypeSchemaGenerator.ts b/packages/client-generator/src/EntityTypeSchemaGenerator.ts new file mode 100644 index 0000000000..2a12c5ea94 --- /dev/null +++ b/packages/client-generator/src/EntityTypeSchemaGenerator.ts @@ -0,0 +1,182 @@ +import { Model } from '@contember/schema' +import { acceptEveryFieldVisitor, acceptFieldVisitor } from '@contember/schema-utils' + +export class EntityTypeSchemaGenerator { + generate(model: Model.Schema): string { + let code = '' + for (const enumName of Object.keys(model.enums)) { + code += `import type { ${enumName} } from './enums'\n` + } + + code += ` +export type JSONPrimitive = string | number | boolean | null +export type JSONValue = JSONPrimitive | JSONObject | JSONArray +export type JSONObject = { readonly [K in string]?: JSONValue } +export type JSONArray = readonly JSONValue[] + +` + + for (const entity of Object.values(model.entities)) { + code += this.generateTypeEntityCode(model, entity) + } + code += '\n' + code += `export type ContemberClientEntities = {\n` + for (const entity of Object.values(model.entities)) { + code += `\t${entity.name}: ${entity.name}\n` + } + code += '}\n\n' + code += `export type ContemberClientSchema = {\n` + code += '\tentities: ContemberClientEntities\n' + code += '}\n' + return code + } + + private generateTypeEntityCode(model: Model.Schema, entity: Model.Entity): string { + let code = `export type ${entity.name} = {\n` + code += '\tname: \'' + entity.name + '\'\n' + code += '\tunique:\n' + code += this.formatUniqueFields(model, entity) + let columnsCode = '' + let hasOneCode = '' + let hasManyCode = '' + acceptEveryFieldVisitor(model, entity, { + visitHasMany: ctx => { + hasManyCode += `\t\t${ctx.relation.name}: ${ctx.targetEntity.name}\n` + }, + visitHasOne: ctx => { + hasOneCode += `\t\t${ctx.relation.name}: ${ctx.targetEntity.name}\n` + }, + visitColumn: ctx => { + columnsCode += `\t\t${ctx.column.name}: ${columnToTsType(ctx.column)}${ctx.column.nullable ? ` | null` : ''}\n` + }, + }) + + code += '\tcolumns: {\n' + code += columnsCode + code += '\t}\n' + code += '\thasOne: {\n' + code += hasOneCode + code += '\t}\n' + code += '\thasMany: {\n' + code += hasManyCode + code += '\t}\n' + code += '\thasManyBy: {\n' + code += this.formatReducedFields(model, entity) + code += '\t}\n' + code += '}\n' + return code + } + + private formatReducedFields(model: Model.Schema, entity: Model.Entity): string { + let code = '' + acceptEveryFieldVisitor(model, entity, { + visitOneHasMany: ({ entity, relation, targetEntity, targetRelation }) => { + if (!targetRelation) { + return + } + const uniqueConstraints = getFieldsForUniqueWhere(model, targetEntity) + const composedUnique = uniqueConstraints + .filter(fields => fields.length === 2) //todo support all uniques + .filter(fields => fields.includes(targetRelation.name)) + .map(fields => fields.filter(it => it !== targetRelation.name)) + .map(fields => fields[0]) + const singleUnique = uniqueConstraints + .filter(fields => fields.length === 1 && fields[0] !== targetEntity.primary) + .map(fields => fields[0]) + .filter(it => it !== targetRelation.name) + + ;[...composedUnique, ...singleUnique].forEach(fieldName => { + const capitalizeFirstLetter = (value: string) => { + return value.charAt(0).toUpperCase() + value.slice(1) + } + const name = `${relation.name}By${capitalizeFirstLetter(fieldName)}` + + const targetUnique = targetEntity.fields[fieldName] + + code += `\t\t${name}: { entity: ${targetEntity.name}; by: {${fieldName}: ${uniqueType(model, targetEntity, targetUnique)}} }\n` + + }) + }, + visitColumn: () => { + }, + visitManyHasManyInverse: () => { + }, + visitManyHasManyOwning: () => { + }, + visitManyHasOne: () => { + }, + visitOneHasOneInverse: () => { + }, + visitOneHasOneOwning: () => { + }, + }) + return code + } + + private formatUniqueFields(model: Model.Schema, entity: Model.Entity): string { + const fields = getFieldsForUniqueWhere(model, entity) + let code = '' + for (const field of fields) { + code += '\t\t| { ' + code += field.map(it => `${it}: ${uniqueType(model, entity, entity.fields[it])}`).join(', ') + code += ' }\n' + } + return code + } +} + + +const uniqueType = (model: Model.Schema, entity: Model.Entity, field: Model.AnyField): string => { + return acceptFieldVisitor(model, entity, field, { + visitColumn: ctx => { + return columnToTsType(ctx.column) + }, + visitRelation: ctx => { + return ctx.targetEntity.name + `['unique']` + }, + }) +} + + +const columnToTsType = (column: Model.AnyColumn): string => { + switch (column.type) { + case Model.ColumnType.Enum: + return column.columnType + case Model.ColumnType.String: + return 'string' + case Model.ColumnType.Int: + return 'number' + case Model.ColumnType.Double: + return 'number' + case Model.ColumnType.Bool: + return 'boolean' + case Model.ColumnType.DateTime: + return 'string' + case Model.ColumnType.Date: + return 'string' + case Model.ColumnType.Json: + return 'JSONValue' + case Model.ColumnType.Uuid: + return 'string' + default: + ((_: never) => { + throw new Error(`Unknown type ${_}`) + })(column.type) + } +} + +const getFieldsForUniqueWhere = (schema: Model.Schema, entity: Model.Entity): readonly (readonly string[])[] => { + const relations = Object.values( + acceptEveryFieldVisitor(schema, entity, { + visitColumn: () => undefined, + visitManyHasManyInverse: () => undefined, + visitManyHasManyOwning: () => undefined, + visitOneHasMany: ({ relation }) => [relation.name], + visitManyHasOne: () => undefined, + visitOneHasOneInverse: ({ relation }) => [relation.name], + visitOneHasOneOwning: ({ relation }) => [relation.name], + }), + ).filter((it): it is [string] => !!it) + + return [[entity.primary], ...Object.values(entity.unique).map(it => it.fields), ...relations] +} diff --git a/packages/client-generator/src/EnumTypeSchemaGenerator.ts b/packages/client-generator/src/EnumTypeSchemaGenerator.ts new file mode 100644 index 0000000000..6cba49678d --- /dev/null +++ b/packages/client-generator/src/EnumTypeSchemaGenerator.ts @@ -0,0 +1,12 @@ +import { Model } from '@contember/schema' + +export class EnumTypeSchemaGenerator { + generate(model: Model.Schema): string { + let code = '' + for (const [enumName, values] of Object.entries(model.enums)) { + code += `export type ${enumName} = ${values.map(it => '\n\t | ' + JSON.stringify(it)).join('')}\n` + } + + return code || 'export {}\n' + } +} diff --git a/packages/client-generator/src/NameSchemaGenerator.ts b/packages/client-generator/src/NameSchemaGenerator.ts new file mode 100644 index 0000000000..5cfa9022a2 --- /dev/null +++ b/packages/client-generator/src/NameSchemaGenerator.ts @@ -0,0 +1,41 @@ +import { SchemaNames, SchemaEntityNames } from '@contember/client' +import { Model } from '@contember/schema' +import { acceptEveryFieldVisitor } from '@contember/schema-utils' + +export class NameSchemaGenerator { + generate(model: Model.Schema): SchemaNames { + return { + entities: Object.fromEntries( + Object.values(model.entities).map(entity => { + const fields: Record['fields'][string]> = {} + const scalars: string[] = [] + + acceptEveryFieldVisitor(model, entity, { + visitHasOne: ctx => { + fields[ctx.relation.name] = { + type: 'one', + entity: ctx.targetEntity.name, + } + }, + visitHasMany: ctx => { + fields[ctx.relation.name] = { + type: 'many', + entity: ctx.targetEntity.name, + } + }, + visitColumn: ctx => { + scalars.push(ctx.column.name) + fields[ctx.column.name] = { + type: 'column', + } + }, + }) + + return [entity.name, { name: entity.name, fields, scalars }] + }), + ), + } + } +} + + diff --git a/packages/client-generator/src/generate.ts b/packages/client-generator/src/generate.ts new file mode 100644 index 0000000000..2872c417aa --- /dev/null +++ b/packages/client-generator/src/generate.ts @@ -0,0 +1,26 @@ +import * as fs from 'node:fs/promises' +import { resolve, join } from 'node:path' +import { ContemberClientGenerator } from './ContemberClientGenerator'; + +(async () => { + const schemaPath = process.argv[2] + const outDir = process.argv[3] + + if (!schemaPath || !outDir) { + console.error(`Usage: yarn contember-client-generator `) + process.exit(1) + } + + const source = JSON.parse(await fs.readFile(resolve(process.cwd(), process.argv[2]), 'utf8')) + const dir = resolve(process.cwd(), process.argv[3]) + const generator = new ContemberClientGenerator() + const result = generator.generate(source.model) + await fs.mkdir(dir, { recursive: true }) + for (const [name, content] of Object.entries(result)) { + await fs.writeFile(join(dir, name), content, 'utf8') + } +})().catch(e => { + console.error(e) + process.exit(1) +}) + diff --git a/packages/client-generator/src/index.ts b/packages/client-generator/src/index.ts new file mode 100644 index 0000000000..3cf7aba58b --- /dev/null +++ b/packages/client-generator/src/index.ts @@ -0,0 +1,4 @@ +export * from './ContemberClientGenerator' +export * from './EntityTypeSchemaGenerator' +export * from './EnumTypeSchemaGenerator' +export * from './NameSchemaGenerator' diff --git a/packages/client-generator/src/tsconfig.json b/packages/client-generator/src/tsconfig.json new file mode 100644 index 0000000000..d766dd72a3 --- /dev/null +++ b/packages/client-generator/src/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.settings.json", + "compilerOptions": { + "outDir": "../dist/types", + "types": ["node"] + }, + "references": [ + { "path": "../../client/src" } + ] +} diff --git a/packages/client-generator/tests/generateEnittyTypes.test.ts b/packages/client-generator/tests/generateEnittyTypes.test.ts new file mode 100644 index 0000000000..231e43345f --- /dev/null +++ b/packages/client-generator/tests/generateEnittyTypes.test.ts @@ -0,0 +1,303 @@ +import { describe, expect, test } from 'vitest' +import { EntityTypeSchemaGenerator } from '../src' +import { schemas } from './schemas' + + +describe('generate entities', () => { + + const entityGenerator = new EntityTypeSchemaGenerator() + test('generate for scalars', () => { + + expect(entityGenerator.generate(schemas.scalarsSchema.model)).toMatchInlineSnapshot(` + " + export type JSONPrimitive = string | number | boolean | null + export type JSONValue = JSONPrimitive | JSONObject | JSONArray + export type JSONObject = { readonly [K in string]?: JSONValue } + export type JSONArray = readonly JSONValue[] + + export type Foo = { + name: 'Foo' + unique: + | { id: string } + columns: { + id: string + stringCol: string | null + intCol: number | null + doubleCol: number | null + dateCol: string | null + datetimeCol: string | null + booleanCol: boolean | null + jsonCol: JSONValue | null + uuidCol: string | null + } + hasOne: { + } + hasMany: { + } + hasManyBy: { + } + } + + export type ContemberClientEntities = { + Foo: Foo + } + + export type ContemberClientSchema = { + entities: ContemberClientEntities + } + " + `) + }) + + test('generate for has enum', () => { + + expect(entityGenerator.generate(schemas.enumSchema.model)).toMatchInlineSnapshot(` + "import type { FooEnumCol } from './enums' + + export type JSONPrimitive = string | number | boolean | null + export type JSONValue = JSONPrimitive | JSONObject | JSONArray + export type JSONObject = { readonly [K in string]?: JSONValue } + export type JSONArray = readonly JSONValue[] + + export type Foo = { + name: 'Foo' + unique: + | { id: string } + columns: { + id: string + enumCol: FooEnumCol | null + } + hasOne: { + } + hasMany: { + } + hasManyBy: { + } + } + + export type ContemberClientEntities = { + Foo: Foo + } + + export type ContemberClientSchema = { + entities: ContemberClientEntities + } + " + `) + }) + test('generate one has one', () => { + + expect(entityGenerator.generate(schemas.oneHasOneSchema.model)).toMatchInlineSnapshot(` + " + export type JSONPrimitive = string | number | boolean | null + export type JSONValue = JSONPrimitive | JSONObject | JSONArray + export type JSONObject = { readonly [K in string]?: JSONValue } + export type JSONArray = readonly JSONValue[] + + export type Foo = { + name: 'Foo' + unique: + | { id: string } + | { oneHasOneInverseRel: Bar['unique'] } + columns: { + id: string + } + hasOne: { + oneHasOneInverseRel: Bar + } + hasMany: { + } + hasManyBy: { + } + } + export type Bar = { + name: 'Bar' + unique: + | { id: string } + | { oneHasOneOwningRel: Foo['unique'] } + columns: { + id: string + } + hasOne: { + oneHasOneOwningRel: Foo + } + hasMany: { + } + hasManyBy: { + } + } + + export type ContemberClientEntities = { + Foo: Foo + Bar: Bar + } + + export type ContemberClientSchema = { + entities: ContemberClientEntities + } + " + `) + }) + + test('generate one has many', () => { + + expect(entityGenerator.generate(schemas.oneHasManySchema.model)).toMatchInlineSnapshot(` + " + export type JSONPrimitive = string | number | boolean | null + export type JSONValue = JSONPrimitive | JSONObject | JSONArray + export type JSONObject = { readonly [K in string]?: JSONValue } + export type JSONArray = readonly JSONValue[] + + export type Foo = { + name: 'Foo' + unique: + | { id: string } + | { oneHasManyRel: Bar['unique'] } + columns: { + id: string + } + hasOne: { + } + hasMany: { + oneHasManyRel: Bar + } + hasManyBy: { + } + } + export type Bar = { + name: 'Bar' + unique: + | { id: string } + columns: { + id: string + } + hasOne: { + manyHasOneRel: Foo + } + hasMany: { + } + hasManyBy: { + } + } + + export type ContemberClientEntities = { + Foo: Foo + Bar: Bar + } + + export type ContemberClientSchema = { + entities: ContemberClientEntities + } + " + `) + }) + + test('generate many has many', () => { + + expect(entityGenerator.generate(schemas.manyHasManySchema.model)).toMatchInlineSnapshot(` + " + export type JSONPrimitive = string | number | boolean | null + export type JSONValue = JSONPrimitive | JSONObject | JSONArray + export type JSONObject = { readonly [K in string]?: JSONValue } + export type JSONArray = readonly JSONValue[] + + export type Foo = { + name: 'Foo' + unique: + | { id: string } + columns: { + id: string + } + hasOne: { + } + hasMany: { + manyHasManyRel: Bar + } + hasManyBy: { + } + } + export type Bar = { + name: 'Bar' + unique: + | { id: string } + columns: { + id: string + } + hasOne: { + } + hasMany: { + manyHasManyInverseRel: Foo + } + hasManyBy: { + } + } + + export type ContemberClientEntities = { + Foo: Foo + Bar: Bar + } + + export type ContemberClientSchema = { + entities: ContemberClientEntities + } + " + `) + }) + + test('generate reduced has by', () => { + + expect(entityGenerator.generate(schemas.reducedHasManySchema.model)).toMatchInlineSnapshot(` + " + export type JSONPrimitive = string | number | boolean | null + export type JSONValue = JSONPrimitive | JSONObject | JSONArray + export type JSONObject = { readonly [K in string]?: JSONValue } + export type JSONArray = readonly JSONValue[] + + export type Foo = { + name: 'Foo' + unique: + | { id: string } + | { locales: FooLocale['unique'] } + columns: { + id: string + } + hasOne: { + } + hasMany: { + locales: FooLocale + } + hasManyBy: { + localesByLocale: { entity: FooLocale; by: {locale: string} } + } + } + export type FooLocale = { + name: 'FooLocale' + unique: + | { id: string } + | { locale: string, foo: Foo['unique'] } + columns: { + id: string + locale: string + } + hasOne: { + foo: Foo + } + hasMany: { + } + hasManyBy: { + } + } + + export type ContemberClientEntities = { + Foo: Foo + FooLocale: FooLocale + } + + export type ContemberClientSchema = { + entities: ContemberClientEntities + } + " + `) + }) + +}) diff --git a/packages/client-generator/tests/generateEnumTypes.test.ts b/packages/client-generator/tests/generateEnumTypes.test.ts new file mode 100644 index 0000000000..9cc9fd1ce6 --- /dev/null +++ b/packages/client-generator/tests/generateEnumTypes.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, test } from 'vitest' +import { EnumTypeSchemaGenerator } from '../src' + + +test('generate enums', () => { + const enumGenerator = new EnumTypeSchemaGenerator() + expect(enumGenerator.generate({ + entities: {}, + enums: { + OrderStatus: ['new', 'paid', 'cancelled'], + OrderType: ['normal', 'express'], + }, + })).toMatchInlineSnapshot(` + "export type OrderStatus = + | \\"new\\" + | \\"paid\\" + | \\"cancelled\\" + export type OrderType = + | \\"normal\\" + | \\"express\\" + " + `) +}) diff --git a/packages/client-generator/tests/schemas.ts b/packages/client-generator/tests/schemas.ts new file mode 100644 index 0000000000..8084cb5f02 --- /dev/null +++ b/packages/client-generator/tests/schemas.ts @@ -0,0 +1,73 @@ +import { SchemaDefinition as def, createSchema } from '@contember/schema-definition' + +namespace ManyScalarsSchema { + export class Foo { + stringCol = def.stringColumn() + intCol = def.intColumn() + doubleCol = def.doubleColumn() + dateCol = def.dateColumn() + datetimeCol = def.dateTimeColumn() + booleanCol = def.boolColumn() + jsonCol = def.jsonColumn() + uuidCol = def.uuidColumn() + } +} + + +namespace EnumSchema { + export class Foo { + enumCol = def.enumColumn(def.createEnum('foo', 'bar')) + } +} + +namespace OneHasOneSchema { + export class Foo { + oneHasOneInverseRel = def.oneHasOneInverse(Bar, 'oneHasOne') + } + + export class Bar { + oneHasOneOwningRel = def.oneHasOne(Foo, 'oneHasOneInverseRel') + } +} + +namespace OneHasManySchema { + export class Foo { + oneHasManyRel = def.oneHasMany(Bar, 'manyHasOneRel') + } + + export class Bar { + manyHasOneRel = def.manyHasOne(Foo, 'oneHasManyRel') + } +} + + +namespace ManyHasManySchema { + export class Foo { + manyHasManyRel = def.manyHasMany(Bar, 'manyHasManyInverseRel') + } + + export class Bar { + manyHasManyInverseRel = def.manyHasManyInverse(Foo, 'manyHasManyRel') + } +} + +namespace ReducedHasManySchema { + export class Foo { + locales = def.oneHasMany(FooLocale, 'foo') + } + + @def.Unique('locale', 'foo') + export class FooLocale { + locale = def.stringColumn().notNull() + foo = def.manyHasOne(Foo, 'locales') + } +} + +export const schemas = { + scalarsSchema: createSchema(ManyScalarsSchema), + enumSchema: createSchema(EnumSchema), + oneHasOneSchema: createSchema(OneHasOneSchema), + oneHasManySchema: createSchema(OneHasManySchema), + manyHasManySchema: createSchema(ManyHasManySchema), + reducedHasManySchema: createSchema(ReducedHasManySchema), +} diff --git a/packages/client-generator/tests/tsconfig.json b/packages/client-generator/tests/tsconfig.json new file mode 100644 index 0000000000..9f0c88bc7c --- /dev/null +++ b/packages/client-generator/tests/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.settings.json", + "compilerOptions": { + "noEmit": true, + "emitDeclarationOnly": false + }, + "references": [ + { "path": "../src" }, + ], +} diff --git a/packages/client-generator/tsconfig.json b/packages/client-generator/tsconfig.json new file mode 100644 index 0000000000..915c57c02d --- /dev/null +++ b/packages/client-generator/tsconfig.json @@ -0,0 +1,8 @@ +{ + "files": [], + "include": [], + "references": [ + { "path": "./src" }, + { "path": "./tests" }, + ], +} diff --git a/packages/client-generator/tsdoc.json b/packages/client-generator/tsdoc.json new file mode 100644 index 0000000000..a46f62a20a --- /dev/null +++ b/packages/client-generator/tsdoc.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json", + "extends": [ + "../../tsdoc.json" + ] +} diff --git a/packages/client-generator/vite.config.js b/packages/client-generator/vite.config.js new file mode 100644 index 0000000000..1e8e1422a6 --- /dev/null +++ b/packages/client-generator/vite.config.js @@ -0,0 +1,21 @@ +import { createViteConfig } from '../../build/createViteConfig.js' +import { defineConfig } from 'vite' + +export default defineConfig(args => { + const config = createViteConfig('client-generator')(args) + + return { + ...config, + build: { + ...config.build, + rollupOptions: { + ...config.build.rollupOptions, + input: { + 'index': './src/index.ts', + 'generate': './src/generate.ts', + }, + }, + }, + } +}) + diff --git a/packages/client/src/builder/GraphQlQueryPrinter.ts b/packages/client/src/builder/GraphQlQueryPrinter.ts new file mode 100644 index 0000000000..38c2ff5820 --- /dev/null +++ b/packages/client/src/builder/GraphQlQueryPrinter.ts @@ -0,0 +1,137 @@ +import { JSONValue } from '@contember/schema' +import { GraphQlField, GraphQlFragment, GraphQlFragmentSpread, GraphQlInlineFragment, GraphQlSelectionSet } from './nodes' + +/** + * @internal + */ +export type GraphQlPrintResult = { query: string; variables: Record } + +/** + * @internal + */ +export class GraphQlQueryPrinter { + private indentString = '\t' + + private variableCounter = 0 + private variables: Record = {} + + private usedFragments = new Set() + + private body = '' + + + public printDocument( + operation: 'query' | 'mutation', + select: GraphQlSelectionSet, + fragments: Record, + ): GraphQlPrintResult { + this.cleanState() + this.processSelectionSet(select, 1) + const body = this.body + this.body = '' + this.processUsedFragments(fragments) + const fragmentsStr = this.body + const variablesString = this.printVariables() + const query = `${operation}${variablesString} {\n${body}}${fragmentsStr}\n` + return { + query: query, + variables: Object.fromEntries(Object.entries(this.variables).map(([key, value]) => [key, value.value])), + } + } + + + private processUsedFragments(fragments: Record): void { + const printed = new Set() + while (this.usedFragments.size > 0) { + const fragmentName = this.usedFragments.values().next().value + this.usedFragments.delete(fragmentName) + if (printed.has(fragmentName)) { + continue + } + printed.add(fragmentName) + if (!fragments[fragmentName]) { + throw new Error(`Unknown fragment ${fragmentName}`) + } + this.processFragment(fragments[fragmentName]) + } + } + + private processFragment(fragment: GraphQlFragment): void { + this.body += `\nfragment ${fragment.name} on ${fragment.type} {\n` + this.processSelectionSet(fragment.selectionSet, 1) + this.body += '}' + } + + private printVariables(): string { + let variablesString = '' + let i = 0 + for (const [variableName, variable] of Object.entries(this.variables)) { + if (i++ === 0) { + variablesString += '(' + } else { + variablesString += ', ' + } + variablesString += `$${variableName}: ${variable.type}` + } + if (i > 0) { + variablesString += ')' + } + return variablesString + } + + private processSelectionSet(selectionSet: GraphQlSelectionSet, indent: number): void { + for (const node of selectionSet) { + if (node instanceof GraphQlField) { + this.processField(node, indent) + } else if (node instanceof GraphQlFragmentSpread) { + this.body += this.indentString.repeat(indent) + '... ' + node.name + '\n' + this.usedFragments.add(node.name) + } else if (node instanceof GraphQlInlineFragment) { + this.body += this.indentString.repeat(indent) + '... on ' + node.type + ' {\n' + this.processSelectionSet(node.selectionSet, indent + 1) + this.body += this.indentString.repeat(indent) + '}\n' + } + } + } + + private processField(field: GraphQlField, indent: number): void { + const indentString = this.indentString.repeat(indent) + this.body += indentString + (field.alias && field.alias !== field.name ? (field.alias + ': ' + field.name) : field.name) + let i = 0 + for (const [argName, arg] of Object.entries(field.args)) { + if (arg.value === undefined) { + continue + } + const variableName = `${argName}_${arg.graphQlType.replace(/[\W]/g, '')}_${this.variableCounter++}` + if (i++ === 0) { + this.body += '(' + } else { + this.body += ', ' + } + this.body += `${argName}: $${variableName}` + this.variables[variableName] = { + type: arg.graphQlType, + value: arg.value, + } + } + if (i > 0) { + this.body += ')' + } + if (field.selectionSet) { + this.body += ' {\n' + this.processSelectionSet(field.selectionSet, indent + 1) + this.body += this.indentString.repeat(indent) + '}' + } + this.body += '\n' + } + + private cleanState(): void { + this.body = '' + this.variableCounter = 0 + this.variables = {} + this.usedFragments = new Set() + } +} diff --git a/packages/client/src/builder/index.ts b/packages/client/src/builder/index.ts new file mode 100644 index 0000000000..1faa1dcbe9 --- /dev/null +++ b/packages/client/src/builder/index.ts @@ -0,0 +1,3 @@ +export * from './nodes' +export * from './types/json' +export * from './GraphQlQueryPrinter' diff --git a/packages/client/src/builder/nodes/GraphQlField.ts b/packages/client/src/builder/nodes/GraphQlField.ts new file mode 100644 index 0000000000..59b7cad026 --- /dev/null +++ b/packages/client/src/builder/nodes/GraphQlField.ts @@ -0,0 +1,31 @@ +import { JSONValue } from '../types/json' +import { GraphQlFragmentSpread } from './GraphQlFragmentSpread' +import { GraphQlInlineFragment } from './GraphQlInlineFragment' + +/** + * @internal + */ +export class GraphQlField { + constructor( + public readonly alias: string | null, + public readonly name: string, + public readonly args: GraphQlFieldTypedArgs = {}, + public readonly selectionSet?: GraphQlSelectionSet, + ) { + } +} + +/** + * @internal + */ +export type GraphQlFieldTypedArgs = Record + +/** + * @internal + */ +export type GraphQlSelectionSetItem = GraphQlField | GraphQlFragmentSpread | GraphQlInlineFragment + +export type GraphQlSelectionSet = GraphQlSelectionSetItem[] diff --git a/packages/client/src/builder/nodes/GraphQlFragment.ts b/packages/client/src/builder/nodes/GraphQlFragment.ts new file mode 100644 index 0000000000..325efaeb41 --- /dev/null +++ b/packages/client/src/builder/nodes/GraphQlFragment.ts @@ -0,0 +1,13 @@ +import { GraphQlSelectionSet } from './GraphQlField' + +/** + * @internal + */ +export class GraphQlFragment { + constructor( + public readonly name: string, + public readonly type: string, + public readonly selectionSet: GraphQlSelectionSet, + ) { + } +} diff --git a/packages/client/src/builder/nodes/GraphQlFragmentSpread.ts b/packages/client/src/builder/nodes/GraphQlFragmentSpread.ts new file mode 100644 index 0000000000..493a10af61 --- /dev/null +++ b/packages/client/src/builder/nodes/GraphQlFragmentSpread.ts @@ -0,0 +1,9 @@ +/** + * @internal + */ +export class GraphQlFragmentSpread { + constructor( + public readonly name: string, + ) { + } +} diff --git a/packages/client/src/builder/nodes/GraphQlInlineFragment.ts b/packages/client/src/builder/nodes/GraphQlInlineFragment.ts new file mode 100644 index 0000000000..73288364f4 --- /dev/null +++ b/packages/client/src/builder/nodes/GraphQlInlineFragment.ts @@ -0,0 +1,12 @@ +import { GraphQlSelectionSet } from './GraphQlField' + +/** + * @internal + */ +export class GraphQlInlineFragment { + constructor( + public readonly type: string, + public readonly selectionSet: GraphQlSelectionSet, + ) { + } +} diff --git a/packages/client/src/builder/nodes/index.ts b/packages/client/src/builder/nodes/index.ts new file mode 100644 index 0000000000..fe81db8c3a --- /dev/null +++ b/packages/client/src/builder/nodes/index.ts @@ -0,0 +1,4 @@ +export * from './GraphQlField' +export * from './GraphQlFragmentSpread' +export * from './GraphQlFragment' +export * from './GraphQlInlineFragment' diff --git a/packages/client/src/builder/types/json.ts b/packages/client/src/builder/types/json.ts new file mode 100644 index 0000000000..d0937c0c06 --- /dev/null +++ b/packages/client/src/builder/types/json.ts @@ -0,0 +1,4 @@ +export type JSONPrimitive = string | number | boolean | null | E +export type JSONValue = JSONPrimitive | JSONObject | JSONArray +export type JSONObject = { readonly [K in string]?: JSONValue } +export type JSONArray = readonly JSONValue[] diff --git a/packages/client/src/content/client/ContentClient.ts b/packages/client/src/content/client/ContentClient.ts new file mode 100644 index 0000000000..734d757bf5 --- /dev/null +++ b/packages/client/src/content/client/ContentClient.ts @@ -0,0 +1,185 @@ +import { ContentMutation, ContentQuery } from './nodes' +import { mutationFragments } from './utils/mutationFragments' +import { MutationResult, TransactionResult } from './types' +import { GraphQlClientRequestOptions } from '../../graphQlClient' +import { GraphQlField, GraphQlFragmentSpread, GraphQlQueryPrinter, GraphQlSelectionSetItem } from '../../builder' + +export type CommonMutationOptions = { + deferForeignKeyConstraints?: boolean + deferUniqueConstraints?: boolean +} + +export type MutationWithTransactionOptions = + & CommonMutationOptions + & { + transaction?: true + } + +export type MutationWithoutTransactionOptions = + & CommonMutationOptions + & { + transaction: false + } + +export type QueryExecutorOptions = { + variables?: Record + apiToken?: string + signal?: AbortSignal + headers?: Record + onResponse?: (response: Response) => void + onData?: (json: unknown) => void +} + + +export type QueryExecutor = (query: string, options: GraphQlClientRequestOptions) => Promise + +export class ContentClient { + constructor( + private readonly executor: QueryExecutor, + ) { + } + + + public async query(query: ContentQuery, options?: QueryExecutorOptions): Promise + public async query>(queries: {[K in keyof Values]: ContentQuery}, options?: QueryExecutorOptions): Promise + public async query(queries: Record> | ContentQuery, options?: QueryExecutorOptions): Promise { + const printer = new GraphQlQueryPrinter() + + const selectionSet = queries instanceof ContentQuery + ? [new GraphQlField('value', queries.queryFieldName, queries.args, queries.nodeSelection)] + : Object.entries(queries).map(([alias, query]) => new GraphQlField(alias, query.queryFieldName, query.args, query.nodeSelection)) + + const { query, variables } = printer.printDocument('query', selectionSet, {}) + const result = await this.executor(query, { variables, ...options }) + + if (queries instanceof ContentQuery) { + return queries.parse((result as any).value) + } + return Object.fromEntries(Object.entries(queries).map(([alias, query]) => [alias, query.parse((result as any)[alias])])) + } + + public async mutate( + mutation: ContentMutation, + options: MutationWithoutTransactionOptions & QueryExecutorOptions, + ): Promise> + public async mutate( + mutation: ContentMutation, + options?: MutationWithTransactionOptions & QueryExecutorOptions, + ): Promise>> + + public async mutate( + mutations: ContentMutation[], + options: MutationWithoutTransactionOptions & QueryExecutorOptions, + ): Promise[]> + public async mutate( + mutations: ContentMutation[], + options?: MutationWithTransactionOptions & QueryExecutorOptions + ): Promise[]>> + + public async mutate>>( + input: Input, + options: MutationWithoutTransactionOptions & QueryExecutorOptions, + ): Promise<{ + [K in keyof Input]: Input[K] extends ContentMutation ? MutationResult : never + }> + public async mutate>>( + input: Input, + options?: MutationWithTransactionOptions & QueryExecutorOptions + ): Promise ? MutationResult : never + }>> + + public async mutate | ContentQuery>>( + input: Input, + options: MutationWithoutTransactionOptions & QueryExecutorOptions, + ): Promise<{ + [K in keyof Input]: Input[K] extends ContentMutation ? MutationResult : Input[K] extends ContentQuery ? Value : never + }> + public async mutate | ContentQuery>>( + input: Input, + options?: MutationWithTransactionOptions & QueryExecutorOptions + ): Promise ? MutationResult : Input[K] extends ContentQuery ? Value : never + }>> + + public async mutate(input: Record | ContentQuery> | ContentMutation | ContentMutation[], options?: & QueryExecutorOptions & (MutationWithTransactionOptions | MutationWithoutTransactionOptions)): Promise { + const printer = new GraphQlQueryPrinter() + const fields: GraphQlField[] = [] + let transaction = options?.transaction ?? true + if (input instanceof ContentMutation) { + fields.push(this.createMutationField('mut', input)) + } else if (Array.isArray(input)) { + let i = 0 + for (const mutation of input) { + fields.push(this.createMutationField('mut_' + i++, mutation)) + } + } else { + for (const [alias, mutation] of Object.entries(input)) { + if (mutation instanceof ContentQuery) { + fields.push(new GraphQlField(alias, 'query', {}, [ + new GraphQlField('value', mutation.queryFieldName, mutation.args, mutation.nodeSelection), + ])) + } else { + fields.push(this.createMutationField(alias, mutation)) + } + } + } + + const selectionSet = transaction ? [new GraphQlField(null, 'transaction', { + ...(options?.deferForeignKeyConstraints ? { deferForeignKeyConstraints: { graphQlType: 'Boolean', value: options?.deferForeignKeyConstraints } } : {}), + ...(options?.deferUniqueConstraints ? { deferUniqueConstraints: { graphQlType: 'Boolean', value: options?.deferUniqueConstraints } } : {}), + }, fields)] : fields + + const { query, variables } = printer.printDocument('mutation', selectionSet, mutationFragments) + const result = await this.executor(query, { variables, ...options }) + + const innerResult = transaction ? (result as any).transaction : result + + let value: any + if (input instanceof ContentMutation) { + value = innerResult.mut + } else if (Array.isArray(input)) { + value = input.map((_, i) => innerResult['mut_' + i] ?? null) + } else { + value = {} + for (const [alias, mutation] of Object.entries(input)) { + if (mutation instanceof ContentQuery) { + value[alias] = mutation.parse(innerResult[alias].value) + } else { + value[alias] = innerResult[alias] + } + } + } + + if (transaction) { + return { + ok: innerResult.ok, + errorMessage: innerResult.errorMessage, + errors: innerResult.errors, + validation: innerResult.validation, + data: value, + } + } else { + return value + } + } + + private createMutationField(alias: string, mutation: ContentMutation): GraphQlField { + const items: GraphQlSelectionSetItem[] = [ + new GraphQlField(null, 'ok'), + new GraphQlField(null, 'errorMessage'), + new GraphQlField(null, 'errors', {}, [ + new GraphQlFragmentSpread('MutationError'), + ]), + ] + if (mutation.operation !== 'delete') { + items.push(new GraphQlField(null, 'validation', {}, [ + new GraphQlFragmentSpread('ValidationResult'), + ])) + } + if (mutation.nodeSelection) { + items.push(new GraphQlField(null, 'node', {}, mutation.nodeSelection)) + } + return new GraphQlField(alias, mutation.mutationFieldName, mutation.mutationArgs, items) + } +} diff --git a/packages/client/src/content/client/ContentQueryBuilder.ts b/packages/client/src/content/client/ContentQueryBuilder.ts new file mode 100644 index 0000000000..786eb554db --- /dev/null +++ b/packages/client/src/content/client/ContentQueryBuilder.ts @@ -0,0 +1,153 @@ +import { ContentClientInput, SchemaNames } from './types' +import { + ContentEntitySelection, + ContentEntitySelectionCallback, + ContentEntitySelectionContext, + ContentMutation, + ContentQuery, createEntitySelection, +} from './nodes' +import { createListArgs } from './utils/createListArgs' +import { createTypedArgs } from './utils/createTypedArgs' +import { Input } from '@contember/schema' +import { GraphQlField, GraphQlSelectionSet } from '../../builder' + +export type EntitySelectionOrCallback = + | ContentEntitySelection + | ContentEntitySelectionCallback + + +export class ContentQueryBuilder { + constructor(private readonly schema: SchemaNames) { + } + + + public fragment(name: string, fieldsCallback?: ContentEntitySelectionCallback): ContentEntitySelection { + const context = this.createContext(name) + + const entitySelection = createEntitySelection(context, []) + if (!fieldsCallback) { + return entitySelection + } + return fieldsCallback(entitySelection) + } + + + + public count(name: string, args: Pick): ContentQuery { + const context = this.createContext(name) + const fieldName = `paginate${name}` + const typedArgs = createTypedArgs(args, { + filter: `${context.entity.name}Where`, + }) + const selectionSet: GraphQlSelectionSet = [ + new GraphQlField(null, 'pageInfo', {}, [ + new GraphQlField(null, 'totalCount'), + ]), + ] + + return new ContentQuery(context.entity, fieldName, typedArgs, selectionSet, it => { + return it.pageInfo.totalCount + }) + } + + + public list(name: string, args: ContentClientInput.AnyListQueryInput, fields: EntitySelectionOrCallback): ContentQuery { + const context = this.createContext(name) + const fieldName = `list${name}` + const typedArgs = createListArgs(context.entity, args) + const selectionSet = this.resolveSelectionSet(fields, context) + + return new ContentQuery(context.entity, fieldName, typedArgs, selectionSet) + } + + + public get(name: string, args: Input.UniqueQueryInput, fields: EntitySelectionOrCallback): ContentQuery | null> { + const context = this.createContext(name) + const fieldName = `get${name}` + const typedArgs = createTypedArgs(args, { + by: `${context.entity.name}UniqueWhere!`, + filter: `${context.entity.name}Where`, + }) + const selectionSet = this.resolveSelectionSet(fields, context) + + return new ContentQuery(context.entity, fieldName, typedArgs, selectionSet) + } + + + public create(name: string, args: Input.CreateInput, fields?: EntitySelectionOrCallback): ContentMutation { + + const context = this.createContext(name) + const fieldName = `create${name}` + const typedArgs = createTypedArgs(args, { + data: `${context.entity.name}CreateInput!`, + }) + const selectionSet = fields ? this.resolveSelectionSet(fields, context) : undefined + + return new ContentMutation(context.entity, 'create', fieldName, typedArgs, selectionSet) + } + + + public update(name: string, args: Input.UpdateInput, fields?: EntitySelectionOrCallback): ContentMutation { + + const context = this.createContext(name) + const fieldName = `update${name}` + const typedArgs = createTypedArgs(args, { + data: `${context.entity.name}UpdateInput!`, + by: `${context.entity.name}UniqueWhere!`, + filter: `${context.entity.name}Where`, + }) + const selectionSet = fields ? this.resolveSelectionSet(fields, context) : undefined + + return new ContentMutation(context.entity, 'update', fieldName, typedArgs, selectionSet) + } + + + public upsert(name: string, args: Input.UpsertInput, fields?: EntitySelectionOrCallback): ContentMutation { + + const context = this.createContext(name) + const fieldName = `upsert${name}` + const typedArgs = createTypedArgs(args, { + update: `${context.entity.name}UpdateInput!`, + create: `${context.entity.name}CreateInput!`, + by: `${context.entity.name}UniqueWhere!`, + filter: `${context.entity.name}Where`, + }) + const selectionSet = fields ? this.resolveSelectionSet(fields, context) : undefined + + return new ContentMutation(context.entity, 'upsert', fieldName, typedArgs, selectionSet) + } + + + public delete(name: string, args: Input.UniqueQueryInput, fields?: EntitySelectionOrCallback): ContentMutation { + + const context = this.createContext(name) + const typedArgs = createTypedArgs(args, { + by: `${context.entity.name}UniqueWhere!`, + filter: `${context.entity.name}Where`, + }) + const fieldName = `delete${name}` + const selectionSet = fields ? this.resolveSelectionSet(fields, context) : undefined + + return new ContentMutation(context.entity, 'delete', fieldName, typedArgs, selectionSet) + } + + private resolveSelectionSet( + fields: EntitySelectionOrCallback, + context: ContentEntitySelectionContext, + ) { + return (typeof fields === 'function' ? fields(createEntitySelection(context, [])) : fields).selectionSet + } + + private createContext( + name: string, + ): ContentEntitySelectionContext { + const entity = this.schema.entities[name] + if (!entity) { + throw new Error(`Entity ${name} not found`) + } + return { + entity: entity, + schema: this.schema, + } + } +} diff --git a/packages/client/src/content/client/TypedContentQueryBuilder.ts b/packages/client/src/content/client/TypedContentQueryBuilder.ts new file mode 100644 index 0000000000..7becd0f2a4 --- /dev/null +++ b/packages/client/src/content/client/TypedContentQueryBuilder.ts @@ -0,0 +1,62 @@ +import { ContentClientInput, SchemaTypeLike } from './types' +import { ContentMutation, ContentQuery, TypedEntitySelection, TypedEntitySelectionCallback } from './nodes' + + +export type TypedContentEntitySelectionOrCallback = + | TypedEntitySelection + | TypedEntitySelectionCallback + + +export interface TypedContentQueryBuilder { + + fragment( + name: EntityName, + ): TypedEntitySelection + fragment( + name: EntityName, + fieldsCallback: TypedEntitySelectionCallback, + ): TypedEntitySelection + + count( + name: EntityName, + args: Pick, 'filter'>, + ): ContentQuery + + list( + name: EntityName, + args: ContentClientInput.ListQueryInput, + fields: TypedContentEntitySelectionOrCallback, + ): ContentQuery + + + get( + name: EntityName, + args: ContentClientInput.UniqueQueryInput, + fields: TypedContentEntitySelectionOrCallback, + ): ContentQuery + + create( + name: EntityName, + args: ContentClientInput.CreateInput, + fields?: TypedContentEntitySelectionOrCallback, + ): ContentMutation + + update( + name: EntityName, + args: ContentClientInput.UpdateInput, + fields?: TypedContentEntitySelectionOrCallback, + ): ContentMutation + + upsert( + name: EntityName, + args: ContentClientInput.UpsertInput, + fields?: TypedContentEntitySelectionOrCallback, + ): ContentMutation + + delete( + name: EntityName, + args: ContentClientInput.UniqueQueryInput, + fields?: TypedContentEntitySelectionOrCallback, + ): ContentMutation +} + diff --git a/packages/client/src/content/client/index.ts b/packages/client/src/content/client/index.ts new file mode 100644 index 0000000000..a468828c3b --- /dev/null +++ b/packages/client/src/content/client/index.ts @@ -0,0 +1,6 @@ +export * from './ContentClient' +export * from './TypedContentQueryBuilder' +export * from './ContentQueryBuilder' +export * from './nodes' +export * from './types' +export * from './utils/replaceGraphQlLiteral' diff --git a/packages/client/src/content/client/nodes/ContentEntitySelection.ts b/packages/client/src/content/client/nodes/ContentEntitySelection.ts new file mode 100644 index 0000000000..cfe0bc300b --- /dev/null +++ b/packages/client/src/content/client/nodes/ContentEntitySelection.ts @@ -0,0 +1,247 @@ +import { createListArgs } from '../utils/createListArgs' +import { ContentClientInput, SchemaEntityNames, SchemaNames } from '../types' +import { Input } from '@contember/schema' +import { GraphQlField, GraphQlFieldTypedArgs, GraphQlSelectionSet } from '../../../builder' + +/** + * @internal + */ +export type ContentEntitySelectionContext = { + entity: SchemaEntityNames + schema: SchemaNames +} + +export type ContentEntitySelectionCallback = (select: ContentEntitySelection) => ContentEntitySelection + +export type EntitySelectionCommonInput = { as?: Alias } +export type EntitySelectionManyArgs = ContentClientInput.AnyListQueryInput & EntitySelectionCommonInput +export type EntitySelectionManyByArgs = { by: Input.UniqueWhere; filter?: Input.Where } & EntitySelectionCommonInput +export type EntitySelectionOneArgs = { filter?: Input.Where } & EntitySelectionCommonInput +export type EntitySelectionColumnArgs = EntitySelectionCommonInput + +export type EntitySelectionAnyArgs = + | EntitySelectionColumnArgs + | EntitySelectionManyArgs + | EntitySelectionManyByArgs + | EntitySelectionOneArgs + +export const createEntitySelection = ( + context: ContentEntitySelectionContext, + selectionSet: GraphQlSelectionSet, +): ContentEntitySelection => { + return new ContentEntitySelection(context, selectionSet) +} + +type ContentEntitySelectionOrCallback = ContentEntitySelectionCallback | ContentEntitySelection + + +export class ContentEntitySelection { + + /** + * @internal + */ + constructor( + /** @internal */ + public readonly context: ContentEntitySelectionContext, + /** @internal */ + public readonly selectionSet: GraphQlSelectionSet, + ) { + } + + + $(field: string, args?: EntitySelectionColumnArgs): ContentEntitySelection + $(field: string, args: EntitySelectionManyArgs, selection: ContentEntitySelectionOrCallback): ContentEntitySelection + $(field: string, args: EntitySelectionManyByArgs, selection: ContentEntitySelectionOrCallback): ContentEntitySelection + $(field: string, args: EntitySelectionOneArgs, selection: ContentEntitySelectionOrCallback): ContentEntitySelection + $(field: string, selection: ContentEntitySelectionOrCallback): ContentEntitySelection + + $(field: string, argsOrSelection?: EntitySelectionAnyArgs | ContentEntitySelectionOrCallback, selectionIn?: ContentEntitySelectionOrCallback): ContentEntitySelection { + const [args, selection] = typeof argsOrSelection === 'function' || argsOrSelection instanceof ContentEntitySelection + ? [{}, argsOrSelection] + : [argsOrSelection ?? {}, selectionIn] + if (field === '__typename') { + return this.withField(new GraphQlField(args.as ?? null, '__typename')) + } + const fieldInfo = this.context.entity.fields[field] + if (!fieldInfo) { + if ('by' in args && field.includes('By')) { + if (!selection) { + throw new Error(`Selection is required for ${this.context.entity.name}.${field}.`) + } + return this._manyBy(field, args as EntitySelectionManyByArgs, selection) + } + + throw new Error(`Field ${this.context.entity.name}.${field} does not exist.`) + } + if (fieldInfo.type === 'column') { + return this._column(field, args as EntitySelectionColumnArgs) + } + if (fieldInfo.type === 'many') { + if (!selection) { + throw new Error(`Selection is required for ${this.context.entity.name}.${field}.`) + } + return this._many(field, args as EntitySelectionManyArgs, selection) + } + if (fieldInfo.type === 'one') { + if (!selection) { + throw new Error(`Selection is required for ${this.context.entity.name}.${field}.`) + } + return this._one(field, args as EntitySelectionOneArgs, selection) + } + throw new Error(`Unknown field type ${fieldInfo.type}.`) + } + + $$(): ContentEntitySelection { + const columns = this.context.entity.scalars + const nodes = [ + ...this.selectionSet, + ...columns.map(col => new GraphQlField(null, col)), + ] + return createEntitySelection(this.context, nodes) + } + + private _column( + name: string, + args: EntitySelectionCommonInput = {}, + ): ContentEntitySelection { + let fieldInfo = this.context.entity.fields[name] + if (!fieldInfo) { + throw new Error(`Field ${this.context.entity.name}.${name} does not exist.`) + } + if (fieldInfo?.type !== 'column') { + throw new Error(`Field ${this.context.entity.name}.${name} is not a column.`) + } + + const alias = args.as ?? name + + const field = new GraphQlField(alias, name, {}) + return this.withField(field) + } + + + private _many( + name: string, + args: EntitySelectionManyArgs, + fields: + | ContentEntitySelectionCallback + | ContentEntitySelection, + ): ContentEntitySelection{ + const alias = args.as ?? name + const fieldInfo = this.context.entity.fields[name] + if (!fieldInfo) { + throw new Error(`Field ${this.context.entity.name}.${name} does not exist.`) + } + if (fieldInfo?.type !== 'many') { + throw new Error(`Field ${this.context.entity.name}.${name} is not a has-many relation.`) + } + const entity = this.context.schema.entities[fieldInfo.entity] + + const newContext = { + entity: entity, + schema: this.context.schema, + } + + const entitySelection = typeof fields === 'function' ? fields(createEntitySelection(newContext, [])) : fields + const newObjectField = new GraphQlField( + alias, + name, + createListArgs(entity, args), + entitySelection.selectionSet, + ) + return this.withField(newObjectField) + } + + + private _manyBy( + name: string, + args: EntitySelectionManyByArgs, + fields: + | ContentEntitySelectionCallback + | ContentEntitySelection, + ): ContentEntitySelection { + const alias = args.as ?? name + const byField = Object.keys(args.by)[0] + const relationField = name.substring(0, name.length - byField.length - 2) + + const field = this.context.entity.fields[relationField] + if (!field) { + throw new Error(`Field ${this.context.entity.name}.${relationField} does not exist.`) + } + if (field?.type !== 'many') { + throw new Error(`Field ${this.context.entity.name}.${relationField} is not a has-many relation.`) + } + const entity = this.context.schema.entities[field.entity] + const newContext = { + entity: entity, + schema: this.context.schema, + } + const fieldUpperFirst = name.charAt(0).toUpperCase() + name.slice(1) + + const argsWithType: GraphQlFieldTypedArgs = { + filter: { + graphQlType: `${entity.name}Where`, + value: args.filter, + }, + by: { + graphQlType: `${this.context.entity.name}${fieldUpperFirst}UniqueWhere!`, + value: args.by, + }, + } + + const entitySelection = typeof fields === 'function' ? fields(createEntitySelection(newContext, []) as any) : fields + const newObjectField = new GraphQlField( + alias, + name, + argsWithType, + entitySelection.selectionSet, + ) + return this.withField(newObjectField) + } + + private _one( + name: string, + args: EntitySelectionOneArgs, + fields: + | ContentEntitySelectionCallback + | ContentEntitySelection, + ): ContentEntitySelection { + const alias = args.as ?? name + const fieldInfo = this.context.entity.fields[name] + if (!fieldInfo) { + throw new Error(`Field ${this.context.entity.name}.${name} does not exist.`) + } + if (fieldInfo?.type !== 'one') { + throw new Error(`Field ${this.context.entity.name}.${name} is not a has-one relation.`) + } + const entity = this.context.schema.entities[fieldInfo.entity] + + const newContext: ContentEntitySelectionContext<(typeof entity)['name']> = { + entity: entity, + schema: this.context.schema, + } + const argsWithType: GraphQlFieldTypedArgs = { + filter: { + graphQlType: `${entity.name}Where`, + value: args.filter, + }, + } + const entitySelection = typeof fields === 'function' + ? fields(createEntitySelection(newContext, []) as any) + : fields + const newObjectField = new GraphQlField( + alias, + name, + argsWithType, + entitySelection.selectionSet, + ) + return this.withField(newObjectField) + } + + + private withField(field: GraphQlField) { + return createEntitySelection(this.context, [ + ...this.selectionSet, + field, + ]) + } +} diff --git a/packages/client/src/content/client/nodes/ContentMutation.ts b/packages/client/src/content/client/nodes/ContentMutation.ts new file mode 100644 index 0000000000..bb2f7e27aa --- /dev/null +++ b/packages/client/src/content/client/nodes/ContentMutation.ts @@ -0,0 +1,23 @@ +import { SchemaEntityNames } from '../types' +import { GraphQlFieldTypedArgs, GraphQlSelectionSet } from '../../../builder' + +export class ContentMutation { + public readonly type = 'mutation' + + /** + * @internal + */ + constructor( + /** @internal */ + public readonly entity: SchemaEntityNames, + /** @internal */ + public readonly operation: 'create' | 'update' | 'delete' | 'upsert', + /** @internal */ + public readonly mutationFieldName: string, + /** @internal */ + public readonly mutationArgs: GraphQlFieldTypedArgs = {}, + /** @internal */ + public readonly nodeSelection?: GraphQlSelectionSet, + ) { + } +} diff --git a/packages/client/src/content/client/nodes/ContentQuery.ts b/packages/client/src/content/client/nodes/ContentQuery.ts new file mode 100644 index 0000000000..883a4cad54 --- /dev/null +++ b/packages/client/src/content/client/nodes/ContentQuery.ts @@ -0,0 +1,24 @@ +import { SchemaEntityNames } from '../types/Schema' +import { GraphQlFieldTypedArgs, GraphQlSelectionSet } from '../../../builder' + + +export class ContentQuery { + public readonly type = 'query' + + /** + * @internal + */ + constructor( + /** @internal */ + public readonly entity: SchemaEntityNames, + /** @internal */ + public readonly queryFieldName: string, + /** @internal */ + public readonly args: GraphQlFieldTypedArgs = {}, + /** @internal */ + public readonly nodeSelection?: GraphQlSelectionSet, + /** @internal */ + public readonly parse: (value: any) => TValue = it => it as TValue, + ) { + } +} diff --git a/packages/client/src/content/client/nodes/TypedEntitySelection.ts b/packages/client/src/content/client/nodes/TypedEntitySelection.ts new file mode 100644 index 0000000000..0594c7301f --- /dev/null +++ b/packages/client/src/content/client/nodes/TypedEntitySelection.ts @@ -0,0 +1,107 @@ +import { EntityTypeLike, SchemaTypeLike } from '../types/Schema' +import { ContentClientInput } from '../types' + +export type TypedEntitySelectionCallback< + TSchema extends SchemaTypeLike, + EntityName extends string, + TEntity extends EntityTypeLike, + TValue +> = (select: TypedEntitySelection) => TypedEntitySelection + +export interface TypedEntitySelection { + + $$(): TypedEntitySelection + + $< + TKey extends (keyof TEntity['columns']) & string, + TAlias extends string | null = null + >( + name: TKey, + args?: {as?: TAlias}, + ): TypedEntitySelection + + $< + TNestedValue, + TNestedKey extends keyof TEntity['hasMany'] & string, + TAlias extends string | null = null, + >( + name: TNestedKey, + args: ContentClientInput.HasManyRelationInput & { as?: TAlias }, + fields: + | TypedEntitySelectionCallback + | TypedEntitySelection, + ): TypedEntitySelection + + $< + TNestedValue, + TNestedKey extends keyof TEntity['hasMany'] & string, + TAlias extends string | null = null, + >( + name: TNestedKey, + fields: + | TypedEntitySelectionCallback + | TypedEntitySelection, + ): TypedEntitySelection + + $< + TNestedValue, + TNestedKey extends keyof TEntity['hasManyBy'] & string, + TAlias extends string | null = null, + >( + name: TNestedKey, + args: ContentClientInput.HasManyByRelationInput & { as?: TAlias }, + fields: + | TypedEntitySelectionCallback + | TypedEntitySelection, + ): TypedEntitySelection + + $< + TNestedValue, + TNestedKey extends keyof TEntity['hasManyBy'] & string, + TAlias extends string | null = null, + >( + name: TNestedKey, + fields: + | TypedEntitySelectionCallback + | TypedEntitySelection, + ): TypedEntitySelection + + $< + TNestedValue extends { [K in string]: unknown }, + TNestedKey extends keyof TEntity['hasOne'] & string, + TAlias extends string | null = null, + >( + name: TNestedKey, + args: ContentClientInput.HasOneRelationInput & { as?: TAlias }, + fields: + | TypedEntitySelectionCallback + | TypedEntitySelection, + ): TypedEntitySelection + + $< + TNestedValue extends { [K in string]: unknown }, + TNestedKey extends keyof TEntity['hasOne'] & string, + TAlias extends string | null = null, + >( + name: TNestedKey, + fields: + | TypedEntitySelectionCallback + | TypedEntitySelection, + ): TypedEntitySelection +} diff --git a/packages/client/src/content/client/nodes/index.ts b/packages/client/src/content/client/nodes/index.ts new file mode 100644 index 0000000000..d13d85b9ba --- /dev/null +++ b/packages/client/src/content/client/nodes/index.ts @@ -0,0 +1,4 @@ +export * from './ContentEntitySelection' +export * from './ContentMutation' +export * from './ContentQuery' +export * from './TypedEntitySelection' diff --git a/packages/client/src/content/client/types/Input.ts b/packages/client/src/content/client/types/Input.ts new file mode 100644 index 0000000000..02a1b7472c --- /dev/null +++ b/packages/client/src/content/client/types/Input.ts @@ -0,0 +1,213 @@ +import { Input, JSONObject } from '@contember/schema' +import { EntityTypeLike } from './Schema' + + +export namespace ContentClientInput { + export type ConnectRelationInput = { + readonly connect: UniqueWhere + } + + export type CreateRelationInput = { + readonly create: CreateDataInput + } + + export type ConnectOrCreateInput = { + readonly connect: UniqueWhere + readonly create: CreateDataInput + } + + export type ConnectOrCreateRelationInput = { + readonly connectOrCreate: ConnectOrCreateInput + } + + export type DisconnectSpecifiedRelationInput = { + readonly disconnect: UniqueWhere + } + + export type DeleteSpecifiedRelationInput = { + readonly delete: UniqueWhere + } + + export type UpdateSpecifiedRelationInput = { + readonly update: { + readonly by: UniqueWhere + readonly data: UpdateDataInput + } + } + + export type UpsertSpecifiedRelationInput = { + readonly upsert: { + readonly by: UniqueWhere + readonly update: UpdateDataInput + readonly create: CreateDataInput + } + } + + export type DisconnectRelationInput = { + readonly disconnect: true + } + + export type UpdateRelationInput = { + readonly update: UpdateDataInput + } + + export type DeleteRelationInput = { + readonly delete: true + } + + export type UpsertRelationInput = { + readonly upsert: { + readonly update: UpdateDataInput + readonly create: CreateDataInput + } + } + + export type CreateDataInput = + & { + readonly [key in keyof TEntity['columns']]?: TEntity['columns'][key]['tsType'] + } + & { + readonly [key in keyof TEntity['hasMany']]?: CreateManyRelationInput + } + & { + readonly [key in keyof TEntity['hasOne']]?: CreateOneRelationInput + } + + export type CreateOneRelationInput = + | ConnectRelationInput + | CreateRelationInput + | ConnectOrCreateRelationInput + + export type CreateManyRelationInput = readonly CreateOneRelationInput[] + + export type UpdateDataInput = + & { + readonly [key in keyof TEntity['columns']]?: TEntity['columns'][key]['tsType'] + } + & { + readonly [key in keyof TEntity['hasMany']]?: UpdateManyRelationInput + } + & { + readonly [key in keyof TEntity['hasOne']]?: UpdateOneRelationInput + } + + export type UpdateInput = { + readonly by: UniqueWhere + readonly filter?: Where + readonly data: UpdateDataInput + } + + export type UpsertInput = { + readonly by: UniqueWhere + readonly filter?: Where + readonly update: UpdateDataInput + readonly create: CreateDataInput + } + + export type CreateInput = { + readonly data: CreateDataInput + } + + export type DeleteInput = { + readonly by: UniqueWhere + readonly filter?: Where + } + + export type UniqueQueryInput = { + readonly by: UniqueWhere + readonly filter?: Where + } + + export type ListQueryInput = { + readonly filter?: Where + readonly orderBy?: readonly OrderBy[] + readonly offset?: number + readonly limit?: number + } + + export type PaginationQueryInput = { + readonly filter?: Where + readonly orderBy?: readonly OrderBy[] + readonly skip?: number + readonly first?: number + } + + export type HasOneRelationInput = { + readonly filter?: Where + } + + export type HasManyByRelationInput = { + readonly by: TUnique + readonly filter?: Where + } + + export type HasManyRelationInput = ListQueryInput + export type HasManyRelationPaginateInput = PaginationQueryInput + + + export type UpdateOneRelationInput = + | CreateRelationInput + | ConnectRelationInput + | ConnectOrCreateRelationInput + | DeleteRelationInput + | DisconnectRelationInput + | UpdateRelationInput + | UpsertRelationInput + + export type UpdateManyRelationInputItem = + | CreateRelationInput + | ConnectRelationInput + | ConnectOrCreateRelationInput + | DeleteSpecifiedRelationInput + | DisconnectSpecifiedRelationInput + | UpdateSpecifiedRelationInput + | UpsertSpecifiedRelationInput + + + export type UpdateManyRelationInput = Array> + + + export type FieldOrderBy = + & { + readonly [key in keyof TEntity['columns']]?: `${Input.OrderDirection}` | null + } + & { + readonly [key in keyof TEntity['hasMany']]?: FieldOrderBy | null + } + & { + readonly [key in keyof TEntity['hasOne']]?: FieldOrderBy | null + } + + export type OrderBy = + & { + readonly _random?: boolean + readonly _randomSeeded?: number + } + & FieldOrderBy + + + export type UniqueWhere = TEntity['unique'] + + + export type Where = + & { + readonly and?: (readonly (Where)[]) | null + readonly or?: (readonly (Where)[]) | null + readonly not?: Where | null + } + & { + readonly [key in keyof TEntity['columns']]?: Input.Condition | null + } + & { + readonly [key in keyof TEntity['hasMany']]?: Where | null + } + & { + readonly [key in keyof TEntity['hasOne']]?: Where | null + } + + + export type AnyOrderBy = Input.OrderBy<`${Input.OrderDirection}`>[] + export type AnyListQueryInput = + & Omit + & { readonly orderBy?: AnyOrderBy } +} diff --git a/packages/client/src/content/client/types/Result.ts b/packages/client/src/content/client/types/Result.ts new file mode 100644 index 0000000000..62ea1b50bb --- /dev/null +++ b/packages/client/src/content/client/types/Result.ts @@ -0,0 +1,43 @@ +import { Result } from '@contember/schema' + +export type TransactionResult = { + readonly ok: boolean + readonly errorMessage: string | null + readonly errors: MutationError[] + readonly validation: ValidationResult + readonly data: V +} + +export type MutationResult = { + readonly ok: boolean + readonly errorMessage: string | null + readonly errors: MutationError[] + readonly node: Value | null + readonly validation?: ValidationResult +} + +export type ValidationResult = { + readonly valid: boolean + readonly errors: ValidationError[] +} + +export type ValidationError = { + readonly path: Path + readonly message: { text: string } +} + +export type MutationError = { + readonly paths: Path[] + readonly message: string + readonly type: Result.ExecutionErrorType +} + +export type Path = Array + +export type FieldPath = { + readonly field: string +} +export type IndexPath = { + readonly index: number + readonly alias: string | null +} diff --git a/packages/client/src/content/client/types/Schema.ts b/packages/client/src/content/client/types/Schema.ts new file mode 100644 index 0000000000..d9278fa4b5 --- /dev/null +++ b/packages/client/src/content/client/types/Schema.ts @@ -0,0 +1,49 @@ +import { JSONObject } from '@contember/schema' + +export type SchemaEntityNames = { + readonly name: Name + readonly scalars: string[] + readonly fields: { + readonly [fieldName: string]: + | { + readonly type: 'column' + } + | { + readonly type: 'many' | 'one' + readonly entity: string + } + } +} + +export type SchemaNames = { + entities: { + [entityName: string]: SchemaEntityNames + } +} + +export type EntityTypeLike = { + name: string + unique: JSONObject + columns: { + [columnName: string]: any + } + hasOne: { + [relationName: string]: EntityTypeLike + } + hasMany: { + [relationName: string]: EntityTypeLike + } + hasManyBy: { + [relationName: string]: { + entity: EntityTypeLike + by: JSONObject + } + } +} + +export type SchemaTypeLike = { + entities: { + [entityName: string]: EntityTypeLike + } +} + diff --git a/packages/client/src/content/client/types/index.ts b/packages/client/src/content/client/types/index.ts new file mode 100644 index 0000000000..8af8ee7dfb --- /dev/null +++ b/packages/client/src/content/client/types/index.ts @@ -0,0 +1,3 @@ +export * from './Input' +export * from './Result' +export * from './Schema' diff --git a/packages/client/src/content/client/utils/createListArgs.ts b/packages/client/src/content/client/utils/createListArgs.ts new file mode 100644 index 0000000000..cb72da2996 --- /dev/null +++ b/packages/client/src/content/client/utils/createListArgs.ts @@ -0,0 +1,23 @@ +import { SchemaEntityNames } from '../types/Schema' +import { GraphQlFieldTypedArgs } from '../../../builder' + +export const createListArgs = (entity: SchemaEntityNames, args: { filter?: any, orderBy?: any, limit?: number, offset?: number }, type: 'list' | 'paginate' = 'list'): GraphQlFieldTypedArgs => { + return { + filter: { + graphQlType: `${entity.name}Where`, + value: args.filter, + }, + orderBy: { + graphQlType: `[${entity.name}OrderBy!]`, + value: args.orderBy, + }, + [type === 'list' ? 'limit' : 'first']: { + graphQlType: 'Int', + value: args.limit, + }, + [type === 'list' ? 'offset' : 'skip']: { + graphQlType: 'Int', + value: args.offset, + }, + } +} diff --git a/packages/client/src/content/client/utils/createTypedArgs.ts b/packages/client/src/content/client/utils/createTypedArgs.ts new file mode 100644 index 0000000000..394e169e62 --- /dev/null +++ b/packages/client/src/content/client/utils/createTypedArgs.ts @@ -0,0 +1,15 @@ +import { GraphQlFieldTypedArgs } from '../../../builder' + +export const createTypedArgs = >( + args: TArgs, + types: { [key in keyof TArgs]: string }, +): GraphQlFieldTypedArgs => { + const typedArgs: GraphQlFieldTypedArgs = {} + for (const key in args) { + typedArgs[key] = { + graphQlType: types[key], + value: args[key], + } + } + return typedArgs +} diff --git a/packages/client/src/content/client/utils/mutationFragments.ts b/packages/client/src/content/client/utils/mutationFragments.ts new file mode 100644 index 0000000000..97765c7c70 --- /dev/null +++ b/packages/client/src/content/client/utils/mutationFragments.ts @@ -0,0 +1,52 @@ +import { GraphQlField, GraphQlFragment, GraphQlFragmentSpread, GraphQlInlineFragment } from '../../../builder' + +export const mutationFragments: Record = { + MutationError: new GraphQlFragment('MutationError', '_MutationError', [ + new GraphQlField(null, 'paths', {}, [ + new GraphQlInlineFragment('_FieldPathFragment', [ + new GraphQlField(null, 'field'), + ]), + new GraphQlInlineFragment('_IndexPathFragment', [ + new GraphQlField(null, 'index'), + new GraphQlField(null, 'alias'), + ]), + ]), + new GraphQlField(null, 'message'), + new GraphQlField(null, 'type'), + ]), + TransactionResult: new GraphQlFragment('TransactionResult', 'TransactionResult', [ + new GraphQlField(null, 'ok'), + new GraphQlField(null, 'errorMessage'), + new GraphQlField(null, 'errors', {}, [ + new GraphQlField(null, 'paths', {}, [ + new GraphQlInlineFragment('_FieldPathFragment', [ + new GraphQlField(null, 'field'), + ]), + new GraphQlInlineFragment('_IndexPathFragment', [ + new GraphQlField(null, 'index'), + new GraphQlField(null, 'alias'), + ]), + ]), + ]), + new GraphQlField(null, 'validation', {}, [ + new GraphQlFragmentSpread('ValidationResult'), + ]), + ]), + ValidationResult: new GraphQlFragment('ValidationResult', '_ValidationResult', [ + new GraphQlField(null, 'valid'), + new GraphQlField(null, 'errors', {}, [ + new GraphQlField(null, 'path', {}, [ + new GraphQlInlineFragment('_FieldPathFragment', [ + new GraphQlField(null, 'field'), + ]), + new GraphQlInlineFragment('_IndexPathFragment', [ + new GraphQlField(null, 'index'), + new GraphQlField(null, 'alias'), + ]), + ]), + new GraphQlField(null, 'message', {}, [ + new GraphQlField(null, 'text'), + ]), + ]), + ]), +} diff --git a/packages/client/src/content/client/utils/replaceGraphQlLiteral.ts b/packages/client/src/content/client/utils/replaceGraphQlLiteral.ts new file mode 100644 index 0000000000..b919a46ffe --- /dev/null +++ b/packages/client/src/content/client/utils/replaceGraphQlLiteral.ts @@ -0,0 +1,23 @@ +import { GraphQlLiteral } from '../../../graphQlBuilder' +import { JSONPrimitive } from '../../../builder' + +export type ReplaceGraphQlLiteral = T extends GraphQlLiteral + ? Value + : T extends JSONPrimitive + ? T // Keep primitives as is + : T extends {} + ? { [K in keyof T]: ReplaceGraphQlLiteral } // Recursively apply to objects + : T extends any[] + ? ReplaceGraphQlLiteral[] // Recursively apply to array elements + : T; + +export const replaceGraphQlLiteral = (input: T): ReplaceGraphQlLiteral => { + if (input instanceof GraphQlLiteral) { + return input.value as any + } else if (Array.isArray(input)) { + return input.map(replaceGraphQlLiteral) as any + } else if (typeof input === 'object' && input !== null) { + return Object.fromEntries(Object.entries(input).map(([key, value]) => value !== undefined ? [key, replaceGraphQlLiteral(value)] : undefined).filter(Boolean) as any) as any + } + return input as any +} diff --git a/packages/client/src/content/index.ts b/packages/client/src/content/index.ts index 1510fbbc7c..54034eaa4d 100644 --- a/packages/client/src/content/index.ts +++ b/packages/client/src/content/index.ts @@ -1,4 +1,4 @@ export * from './upload' export * from './params' - +export * from './client' export * from './formatContentApiRelativeUrl' diff --git a/packages/client/src/graphQlClient/GraphQlClient.ts b/packages/client/src/graphQlClient/GraphQlClient.ts index fdd6053e8f..7978ad34f6 100644 --- a/packages/client/src/graphQlClient/GraphQlClient.ts +++ b/packages/client/src/graphQlClient/GraphQlClient.ts @@ -1,8 +1,15 @@ export interface GraphQlClientRequestOptions { variables?: GraphQlClientVariables - apiTokenOverride?: string + apiToken?: string signal?: AbortSignal headers?: Record + onResponse?: (response: Response) => void + onData?: (json: unknown) => void + + /** + * @deprecated use apiToken + */ + apiTokenOverride?: string } export interface GraphQlClientVariables { @@ -16,25 +23,75 @@ export type GraphQlClientFailedRequestMetadata = Pick( - query: string, - { apiTokenOverride, signal, variables, headers }: GraphQlClientRequestOptions = {}, - ): Promise { - const resolvedHeaders: Record = { 'Content-Type': 'application/json', ...headers } - const resolvedToken = apiTokenOverride ?? this.apiToken + async execute(query: string, options: GraphQlClientRequestOptions = {}): Promise { + let body: string | null = null + let response: Response | null = null + const createError = (type: GraphqlErrorType, errors?: any[], cause?: unknown) => { + const request = { + url: this.apiUrl, + query, + variables: options.variables ?? {}, + } - if (resolvedToken !== undefined) { - resolvedHeaders['Authorization'] = `Bearer ${resolvedToken}` + const details = `HTTP response: ${response ? (response.status + ' ' + response.statusText) : ''} +HTTP body: +${body !== null ? body : ''} + +GraphQL query: +${query}` + + return new GraphQlClientError(`GraphQL request failed: ${type}`, type, request, response ?? undefined, errors, details, cause) + } + try { + response = await this.doExecute(query, options) + } catch (e) { + const aborted = typeof e === 'object' && e !== null && (e as { name?: unknown }).name === 'AbortError' + throw createError(aborted ? 'aborted' : 'network error', undefined, e) } - console.debug(query) + options?.onResponse?.(response) - const response = await fetch(this.apiUrl, { - method: 'POST', - headers: resolvedHeaders, - signal, - body: JSON.stringify({ query, variables }), - }) + body = await response.text() + + let data: any + try { + data = JSON.parse(body) + } catch (e) { + throw createError('invalid response body', undefined, e) + } + options?.onData?.(data) + + if (response.status === 401) { + throw createError('unauthorized') + } + if (response.status === 403) { + throw createError('forbidden') + } + if (response.status >= 400 && response.status < 500) { + throw createError('bad request', data.errors) + } + if (response.status >= 500) { + throw createError('server error') + } + if (!(typeof data === 'object') || data === null) { + throw createError('invalid response body') + } + if ('errors' in data) { + throw createError('response errors', data.errors) + } + if (!('data' in data)) { + throw createError('invalid response body') + } + + return data.data + } + + /** + * @deprecated use execute + */ + async sendRequest(query: string, options: GraphQlClientRequestOptions = {}): Promise { + console.debug(query) + const response = await this.doExecute(query, options) if (response.ok) { // It may still have errors (e.g. unfilled fields) but as far as the request goes, it is ok. @@ -49,4 +106,54 @@ export class GraphQlClient { return Promise.reject(failedRequest) } + + + private async doExecute( + query: string, + { apiToken, apiTokenOverride, signal, variables, headers }: GraphQlClientRequestOptions = {}, + ): Promise { + const resolvedHeaders: Record = { + 'Content-Type': 'application/json', + ...headers, + } + const resolvedToken = apiToken ?? apiTokenOverride ?? this.apiToken + + if (resolvedToken !== undefined) { + resolvedHeaders['Authorization'] = `Bearer ${resolvedToken}` + } + + return await fetch(this.apiUrl, { + method: 'POST', + headers: resolvedHeaders, + signal, + body: JSON.stringify({ query, variables }), + }) + } +} + +export type GraphqlErrorRequest = { url: string, query: string, variables: Record }; + +export type GraphqlErrorType = + | 'aborted' + | 'network error' + | 'invalid response body' + | 'bad request' + | 'unauthorized' + | 'forbidden' + | 'server error' + | 'response errors' + +export class GraphQlClientError extends Error { + constructor( + message: string, + public readonly type: GraphqlErrorType, + public readonly request: GraphqlErrorRequest, + public readonly response?: Response, + public readonly errors?: readonly any[], + public readonly details?: string, + cause?: unknown, + ) { + super(message) + this.cause = cause + } } diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index fbafa0e50c..170f11362d 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -41,6 +41,7 @@ export namespace CrudQueryBuilder { export type WriteRelationOps = CrudQueryBuilderTmp.WriteRelationOps } +export * from './builder' export * from './content' export * from './graphQlClient' export * from './system' diff --git a/packages/client/tests/cases/unit/generateUploadUrlMutationBuilder.spec.ts b/packages/client/tests/cases/unit/generateUploadUrlMutationBuilder.spec.ts index 384f4db77d..1648e7a590 100644 --- a/packages/client/tests/cases/unit/generateUploadUrlMutationBuilder.spec.ts +++ b/packages/client/tests/cases/unit/generateUploadUrlMutationBuilder.spec.ts @@ -11,19 +11,23 @@ describe('generate upload url mutation builder', () => { }, }), ).toMatchInlineSnapshot(` - "mutation { - mySingleImage: generateUploadUrl(contentType: \\"image/png\\") { - __typename + { + "query": "mutation($contentType_String_0: String) { + mySingleImage: generateUploadUrl(contentType: $contentType_String_0) { url publicUrl method headers { - __typename key value } } - }" + } + ", + "variables": { + "contentType_String_0": "image/png", + }, + } `) }) @@ -41,30 +45,36 @@ describe('generate upload url mutation builder', () => { }, }), ).toMatchInlineSnapshot(` - "mutation { - myPng: generateUploadUrl(contentType: \\"image/png\\") { - __typename + { + "query": "mutation($contentType_String_0: String, $contentType_String_1: String, $expiration_Int_2: Int, $prefix_String_3: String, $acl_S3Acl_4: S3Acl) { + myPng: generateUploadUrl(contentType: $contentType_String_0) { url publicUrl method headers { - __typename key value } } - myMp3: generateUploadUrl(contentType: \\"audio/mpeg\\", expiration: 123456, prefix: \\"foo\\", acl: PUBLIC_READ) { - __typename + myMp3: generateUploadUrl(contentType: $contentType_String_1, expiration: $expiration_Int_2, prefix: $prefix_String_3, acl: $acl_S3Acl_4) { url publicUrl method headers { - __typename key value } } - }" + } + ", + "variables": { + "acl_S3Acl_4": "PUBLIC_READ", + "contentType_String_0": "image/png", + "contentType_String_1": "audio/mpeg", + "expiration_Int_2": 123456, + "prefix_String_3": "foo", + }, + } `) }) }) diff --git a/packages/client/tests/cases/unit/lib.ts b/packages/client/tests/cases/unit/lib.ts new file mode 100644 index 0000000000..976106b88f --- /dev/null +++ b/packages/client/tests/cases/unit/lib.ts @@ -0,0 +1,134 @@ +import { ContentClient, ContentQueryBuilder, TypedContentQueryBuilder } from '../../../src' + +export namespace Schema { + export type Author = { + name: 'Author' + unique: { id: number } + columns: { + id: number + name: string + email: string + } + hasMany: { + posts: Post + } + hasOne: {} + hasManyBy: {} + } + + + export type Post = { + name: 'Post' + unique: { id: number } + columns: { + id: number + title: string + content: string + } + hasMany: { + tags: Tag + } + hasOne: { + author: Author + } + hasManyBy: {} + } + + export type Tag = { + name: 'Tag' + unique: { id: number } + columns: { + id: number + name: string + } + hasMany: { + posts: Post + } + hasOne: {} + hasManyBy: {} + } + +} + +export const qb = new ContentQueryBuilder({ + entities: { + Author: { + name: 'Author', + fields: { + id: { + type: 'column', + }, + posts: { + type: 'many', + entity: 'Post', + }, + name: { + type: 'column', + }, + email: { + type: 'column', + }, + }, + scalars: ['name', 'email'], + }, + Post: { + name: 'Post', + + fields: { + id: { + type: 'column', + }, + author: { + type: 'one', + entity: 'Author', + }, + tags: { + type: 'many', + entity: 'Tag', + }, + title: { + type: 'column', + }, + content: { + type: 'column', + }, + }, + scalars: ['title', 'content'], + }, + Tag: { + name: 'Tag', + fields: { + id: { + type: 'column', + }, + posts: { + type: 'many', + entity: 'Post', + }, + name: { + type: 'column', + }, + }, + scalars: ['name'], + }, + }, +}) as unknown as TypedContentQueryBuilder<{ + entities: { + Post: Schema.Post, + Author: Schema.Author, + Tag: Schema.Tag, + }, +}> + +export const createClient = (result?: any) => { + const calls: { query: string, variables: Record }[] = [] + const client = new ContentClient((query: string, options: any): Promise => { + calls.push({ + query, + ...options, + }) + return Promise.resolve(result ?? {}) + }, + ) + return [client, calls] as const +} diff --git a/packages/client/tests/cases/unit/mutation.test.ts b/packages/client/tests/cases/unit/mutation.test.ts new file mode 100644 index 0000000000..416b380fb7 --- /dev/null +++ b/packages/client/tests/cases/unit/mutation.test.ts @@ -0,0 +1,670 @@ +import { describe, expect, test } from 'vitest' +import { createClient, qb } from './lib' +describe('mutations', () => { + test('create', async () => { + const [client, calls] = createClient({ + transaction: { + ok: true, + mut: { + ok: true, + }, + }, + }) + const result = await client.mutate(qb.create('Author', { + data: { + name: 'John', + email: 'xx@localhost', + }, + })) + + + expect(result.data.ok).toBe(true) + expect(calls).toHaveLength(1) + expect(calls[0].query).toMatchInlineSnapshot(` + "mutation($data_AuthorCreateInput_0: AuthorCreateInput!) { + transaction { + mut: createAuthor(data: $data_AuthorCreateInput_0) { + ok + errorMessage + errors { + ... MutationError + } + validation { + ... ValidationResult + } + } + } + } + fragment MutationError on _MutationError { + paths { + ... on _FieldPathFragment { + field + } + ... on _IndexPathFragment { + index + alias + } + } + message + type + } + fragment ValidationResult on _ValidationResult { + valid + errors { + path { + ... on _FieldPathFragment { + field + } + ... on _IndexPathFragment { + index + alias + } + } + message { + text + } + } + } + " + `) + expect(calls[0].variables).toMatchInlineSnapshot(` + { + "data_AuthorCreateInput_0": { + "email": "xx@localhost", + "name": "John", + }, + } + `) + }) + + test('mutation with a node', async () => { + const [client, calls] = createClient({ + transaction: { + ok: true, + mut: { + ok: true, + node: { id: 1 }, + }, + }, + }) + const result = await client.mutate(qb.create('Author', { + data: { + name: 'John', + email: 'xx@localhost', + }, + }, it => it.$('id'))) + expect(result.data.ok).toBe(true) + expect(result.data.node?.id).toBe(1) + expect(calls).toHaveLength(1) + expect(calls[0].query).toMatchInlineSnapshot(` + "mutation($data_AuthorCreateInput_0: AuthorCreateInput!) { + transaction { + mut: createAuthor(data: $data_AuthorCreateInput_0) { + ok + errorMessage + errors { + ... MutationError + } + validation { + ... ValidationResult + } + node { + id + } + } + } + } + fragment MutationError on _MutationError { + paths { + ... on _FieldPathFragment { + field + } + ... on _IndexPathFragment { + index + alias + } + } + message + type + } + fragment ValidationResult on _ValidationResult { + valid + errors { + path { + ... on _FieldPathFragment { + field + } + ... on _IndexPathFragment { + index + alias + } + } + message { + text + } + } + } + " + `) + expect(calls[0].variables).toMatchInlineSnapshot(` + { + "data_AuthorCreateInput_0": { + "email": "xx@localhost", + "name": "John", + }, + } + `) + }) + + test('update', async () => { + const [client, calls] = createClient({ + transaction: { + ok: true, + mut: { + ok: true, + }, + }, + }) + await client.mutate(qb.update('Author', { + by: { id: 1 }, + data: { + name: 'John', + email: 'xx@localhost', + }, + })) + expect(calls).toHaveLength(1) + expect(calls[0].query).toMatchInlineSnapshot(` + "mutation($by_AuthorUniqueWhere_0: AuthorUniqueWhere!, $data_AuthorUpdateInput_1: AuthorUpdateInput!) { + transaction { + mut: updateAuthor(by: $by_AuthorUniqueWhere_0, data: $data_AuthorUpdateInput_1) { + ok + errorMessage + errors { + ... MutationError + } + validation { + ... ValidationResult + } + } + } + } + fragment MutationError on _MutationError { + paths { + ... on _FieldPathFragment { + field + } + ... on _IndexPathFragment { + index + alias + } + } + message + type + } + fragment ValidationResult on _ValidationResult { + valid + errors { + path { + ... on _FieldPathFragment { + field + } + ... on _IndexPathFragment { + index + alias + } + } + message { + text + } + } + } + " + `) + expect(calls[0].variables).toMatchInlineSnapshot(` + { + "by_AuthorUniqueWhere_0": { + "id": 1, + }, + "data_AuthorUpdateInput_1": { + "email": "xx@localhost", + "name": "John", + }, + } + `) + }) + + test('delete', async () => { + const [client, calls] = createClient({ + transaction: { + ok: true, + mut: { + ok: true, + }, + }, + }) + await client.mutate(qb.delete('Author', { + by: { id: 1 }, + })) + expect(calls).toHaveLength(1) + expect(calls[0].query).toMatchInlineSnapshot(` + "mutation($by_AuthorUniqueWhere_0: AuthorUniqueWhere!) { + transaction { + mut: deleteAuthor(by: $by_AuthorUniqueWhere_0) { + ok + errorMessage + errors { + ... MutationError + } + } + } + } + fragment MutationError on _MutationError { + paths { + ... on _FieldPathFragment { + field + } + ... on _IndexPathFragment { + index + alias + } + } + message + type + } + " + `) + expect(calls[0].variables).toMatchInlineSnapshot(` + { + "by_AuthorUniqueWhere_0": { + "id": 1, + }, + } + `) + }) + + test('upsert', async () => { + const [client, calls] = createClient({ + transaction: { + ok: true, + mut: { + ok: true, + }, + }, + }) + await client.mutate(qb.upsert('Author', { + by: { id: 1 }, + create: { + name: 'John', + email: 'xx@localhost', + }, + update: { + name: 'John', + email: 'xx@localhost', + }, + })) + expect(calls).toHaveLength(1) + expect(calls[0].query).toMatchInlineSnapshot(` + "mutation($by_AuthorUniqueWhere_0: AuthorUniqueWhere!, $create_AuthorCreateInput_1: AuthorCreateInput!, $update_AuthorUpdateInput_2: AuthorUpdateInput!) { + transaction { + mut: upsertAuthor(by: $by_AuthorUniqueWhere_0, create: $create_AuthorCreateInput_1, update: $update_AuthorUpdateInput_2) { + ok + errorMessage + errors { + ... MutationError + } + validation { + ... ValidationResult + } + } + } + } + fragment MutationError on _MutationError { + paths { + ... on _FieldPathFragment { + field + } + ... on _IndexPathFragment { + index + alias + } + } + message + type + } + fragment ValidationResult on _ValidationResult { + valid + errors { + path { + ... on _FieldPathFragment { + field + } + ... on _IndexPathFragment { + index + alias + } + } + message { + text + } + } + } + " + `) + expect(calls[0].variables).toMatchInlineSnapshot(` + { + "by_AuthorUniqueWhere_0": { + "id": 1, + }, + "create_AuthorCreateInput_1": { + "email": "xx@localhost", + "name": "John", + }, + "update_AuthorUpdateInput_2": { + "email": "xx@localhost", + "name": "John", + }, + } + `) + }) + + test('multiple mutations as array', async () => { + const [client, calls] = createClient({ + transaction: { + ok: true, + mut_0: { + ok: true, + }, + mut_1: { + ok: true, + }, + }, + }) + const result = await client.mutate([ + qb.create('Author', { + data: { + name: 'John', + email: 'xx@localhost', + }, + }), + qb.create('Author', { + data: { + name: 'John', + email: 'xx@localhost', + }, + }), + ]) + expect(result.ok).toBe(true) + expect(result.data).toHaveLength(2) + expect(result.data[0].ok).toBe(true) + expect(result.data[1].ok).toBe(true) + expect(calls).toHaveLength(1) + expect(calls[0].query).toMatchInlineSnapshot(` + "mutation($data_AuthorCreateInput_0: AuthorCreateInput!, $data_AuthorCreateInput_1: AuthorCreateInput!) { + transaction { + mut_0: createAuthor(data: $data_AuthorCreateInput_0) { + ok + errorMessage + errors { + ... MutationError + } + validation { + ... ValidationResult + } + } + mut_1: createAuthor(data: $data_AuthorCreateInput_1) { + ok + errorMessage + errors { + ... MutationError + } + validation { + ... ValidationResult + } + } + } + } + fragment MutationError on _MutationError { + paths { + ... on _FieldPathFragment { + field + } + ... on _IndexPathFragment { + index + alias + } + } + message + type + } + fragment ValidationResult on _ValidationResult { + valid + errors { + path { + ... on _FieldPathFragment { + field + } + ... on _IndexPathFragment { + index + alias + } + } + message { + text + } + } + } + " + `) + expect(calls[0].variables).toMatchInlineSnapshot(` + { + "data_AuthorCreateInput_0": { + "email": "xx@localhost", + "name": "John", + }, + "data_AuthorCreateInput_1": { + "email": "xx@localhost", + "name": "John", + }, + } + `) + }) + + test('multiple named mutations', async () => { + const [client, calls] = createClient({ + transaction: { + ok: true, + createAuthor: { + ok: true, + }, + createPost: { + ok: true, + }, + }, + }) + const result = await client.mutate({ + createAuthor: qb.create('Author', { + data: { + name: 'John', + email: 'xx@localhost', + }, + }), + createPost: qb.create('Post', { + data: { + title: 'Hello', + content: 'World', + }, + }), + }) + expect(result.data.createAuthor.ok).toBe(true) + expect(result.data.createPost.ok).toBe(true) + expect(calls).toHaveLength(1) + expect(calls[0].query).toMatchInlineSnapshot(` + "mutation($data_AuthorCreateInput_0: AuthorCreateInput!, $data_PostCreateInput_1: PostCreateInput!) { + transaction { + createAuthor(data: $data_AuthorCreateInput_0) { + ok + errorMessage + errors { + ... MutationError + } + validation { + ... ValidationResult + } + } + createPost(data: $data_PostCreateInput_1) { + ok + errorMessage + errors { + ... MutationError + } + validation { + ... ValidationResult + } + } + } + } + fragment MutationError on _MutationError { + paths { + ... on _FieldPathFragment { + field + } + ... on _IndexPathFragment { + index + alias + } + } + message + type + } + fragment ValidationResult on _ValidationResult { + valid + errors { + path { + ... on _FieldPathFragment { + field + } + ... on _IndexPathFragment { + index + alias + } + } + message { + text + } + } + } + " + `) + expect(calls[0].variables).toMatchInlineSnapshot(` + { + "data_AuthorCreateInput_0": { + "email": "xx@localhost", + "name": "John", + }, + "data_PostCreateInput_1": { + "content": "World", + "title": "Hello", + }, + } + `) + }) + + test('mutation and query combo', async () => { + const [client, calls] = createClient({ + transaction: { + ok: true, + createPost: { + ok: true, + }, + post: { + value: { + id: 1, + title: 'Foo bar', + content: 'Hello world', + }, + }, + }, + }) + + const result = await client.mutate({ + createPost: qb.create('Post', { + data: { + title: 'Hello', + content: 'World', + }, + }), + post: qb.get('Post', { + by: { id: 1 }, + }, it => it.$$()), + }) + expect(result.data.createPost.ok).toBe(true) + expect(result.data.post?.title).toBe('Foo bar') + + expect(calls).toHaveLength(1) + expect(calls[0].query).toMatchInlineSnapshot(` + "mutation($data_PostCreateInput_0: PostCreateInput!, $by_PostUniqueWhere_1: PostUniqueWhere!) { + transaction { + createPost(data: $data_PostCreateInput_0) { + ok + errorMessage + errors { + ... MutationError + } + validation { + ... ValidationResult + } + } + post: query { + value: getPost(by: $by_PostUniqueWhere_1) { + title + content + } + } + } + } + fragment MutationError on _MutationError { + paths { + ... on _FieldPathFragment { + field + } + ... on _IndexPathFragment { + index + alias + } + } + message + type + } + fragment ValidationResult on _ValidationResult { + valid + errors { + path { + ... on _FieldPathFragment { + field + } + ... on _IndexPathFragment { + index + alias + } + } + message { + text + } + } + } + " + `) + expect(calls[0].variables).toMatchInlineSnapshot(` + { + "by_PostUniqueWhere_1": { + "id": 1, + }, + "data_PostCreateInput_0": { + "content": "World", + "title": "Hello", + }, + } + `) + }) + +}) diff --git a/packages/client/tests/cases/unit/query.test.ts b/packages/client/tests/cases/unit/query.test.ts new file mode 100644 index 0000000000..b49a8943c4 --- /dev/null +++ b/packages/client/tests/cases/unit/query.test.ts @@ -0,0 +1,192 @@ +import { describe, expect, test } from 'vitest' +import { createClient, qb } from './lib' +import { Input } from '@contember/schema' +import OrderDirection = Input.OrderDirection; + +describe('queries', () => { + + test('list', async () => { + const [client, calls] = createClient() + const result = await client.query({ + authors: qb.list('Author', {}, it => it.$$()), + }) + expect(calls).toHaveLength(1) + expect(calls[0].query).toMatchInlineSnapshot(` + "query { + authors: listAuthor { + name + email + } + } + " + `) + expect(calls[0].variables).toMatchInlineSnapshot('{}') + }) + + test('multiple queries', async () => { + const [client, calls] = createClient() + const result = await client.query({ + authors: qb.list('Author', {}, it => it.$$()), + posts: qb.list('Post', {}, it => it.$$()), + }) + expect(calls).toHaveLength(1) + expect(calls[0].query).toMatchInlineSnapshot(` + "query { + authors: listAuthor { + name + email + } + posts: listPost { + title + content + } + } + " + `) + expect(calls[0].variables).toMatchInlineSnapshot('{}') + }) + + + test('single query', async () => { + const [client, calls] = createClient() + await client.query(qb.list('Post', {}, it => it.$$())) + expect(calls).toHaveLength(1) + expect(calls[0].query).toMatchInlineSnapshot(` + "query { + value: listPost { + title + content + } + } + " + `) + expect(calls[0].variables).toMatchInlineSnapshot('{}') + }) + + + test('nested object', async () => { + const [client, calls] = createClient() + await client.query({ + authors: qb.list('Author', {}, it => it.$$().$('posts', {}, it => it.$$().$('tags', {}, it => it.$$()))), + }) + expect(calls).toHaveLength(1) + expect(calls[0].query).toMatchInlineSnapshot(` + "query { + authors: listAuthor { + name + email + posts { + title + content + tags { + name + } + } + } + } + " + `) + expect(calls[0].variables).toMatchInlineSnapshot('{}') + }) + test('nested object args', async () => { + const [client, calls] = createClient() + await client.query({ + authors: qb.list('Author', {}, it => it.$$().$('posts', { + limit: 10, + filter: { tags: { name: { eq: 'foo' } } }, + }, it => it.$$())), + }) + expect(calls).toHaveLength(1) + expect(calls[0].query).toMatchInlineSnapshot(` + "query($filter_PostWhere_0: PostWhere, $limit_Int_1: Int) { + authors: listAuthor { + name + email + posts(filter: $filter_PostWhere_0, limit: $limit_Int_1) { + title + content + } + } + } + " + `) + expect(calls[0].variables).toMatchInlineSnapshot(` + { + "filter_PostWhere_0": { + "tags": { + "name": { + "eq": "foo", + }, + }, + }, + "limit_Int_1": 10, + } + `) + }) + + + test('list with args', async () => { + const [client, calls] = createClient() + await client.query({ + authors: qb.list('Author', { + filter: { name: { eq: 'John' } }, + orderBy: [{ name: OrderDirection.asc }], + limit: 10, + offset: 20, + }, it => it.$$()), + }) + expect(calls).toHaveLength(1) + expect(calls[0].query).toMatchInlineSnapshot(` + "query($filter_AuthorWhere_0: AuthorWhere, $orderBy_AuthorOrderBy_1: [AuthorOrderBy!], $limit_Int_2: Int, $offset_Int_3: Int) { + authors: listAuthor(filter: $filter_AuthorWhere_0, orderBy: $orderBy_AuthorOrderBy_1, limit: $limit_Int_2, offset: $offset_Int_3) { + name + email + } + } + " + `) + expect(calls[0].variables).toMatchInlineSnapshot(` + { + "filter_AuthorWhere_0": { + "name": { + "eq": "John", + }, + }, + "limit_Int_2": 10, + "offset_Int_3": 20, + "orderBy_AuthorOrderBy_1": [ + { + "name": "asc", + }, + ], + } + `) + }) + + + test('get by id', async () => { + const [client, calls] = createClient() + const result = await client.query({ + authors: qb.get('Author', { + by: { id: 123 }, + }, it => it.$$()), + }) + expect(calls).toHaveLength(1) + expect(calls[0].query).toMatchInlineSnapshot(` + "query($by_AuthorUniqueWhere_0: AuthorUniqueWhere!) { + authors: getAuthor(by: $by_AuthorUniqueWhere_0) { + name + email + } + } + " + `) + expect(calls[0].variables).toMatchInlineSnapshot(` + { + "by_AuthorUniqueWhere_0": { + "id": 123, + }, + } + `) + }) +}) diff --git a/tsconfig.json b/tsconfig.json index bf896d5060..de2ba03424 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,6 +10,7 @@ { "path": "./packages/binding" }, { "path": "./packages/brand" }, { "path": "./packages/client" }, + { "path": "./packages/client-generator" }, { "path": "./packages/interface-tester" }, { "path": "./packages/layout" }, { "path": "./packages/react-auto" }, diff --git a/yarn.lock b/yarn.lock index 1163ff054c..a6601e0e86 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2786,6 +2786,20 @@ __metadata: languageName: unknown linkType: soft +"@contember/client-generator@workspace:packages/client-generator": + version: 0.0.0-use.local + resolution: "@contember/client-generator@workspace:packages/client-generator" + dependencies: + "@contember/client": "workspace:*" + "@contember/schema": ^1.2.0 + "@contember/schema-definition": ^1.2.0 + "@contember/schema-utils": ^1.2.0 + "@types/node": ^18 + bin: + contember-client-generator: ./dist/production/generate.js + languageName: unknown + linkType: soft + "@contember/client@workspace:*, @contember/client@workspace:packages/client": version: 0.0.0-use.local resolution: "@contember/client@workspace:packages/client" From f0522c1e57a291e8d7b9025588099a817dba32e8 Mon Sep 17 00:00:00 2001 From: David Matejka Date: Wed, 22 Nov 2023 15:53:43 +0100 Subject: [PATCH 06/17] refactor(binding): use new graphql content client --- .../renderers/FeedbackRenderer.tsx | 28 +- .../accessorTree/MutationRequestResponse.ts | 64 -- .../binding/src/accessorTree/PersistResult.ts | 3 +- .../binding/src/accessorTree/RequestError.ts | 22 - packages/binding/src/accessorTree/index.ts | 3 - .../accessorTree/metadataToRequestError.ts | 14 - .../binding/src/core/AccessorErrorManager.ts | 20 +- packages/binding/src/core/DataBinding.ts | 117 ++- .../binding/src/core/ErrorsPreprocessor.ts | 47 +- .../binding/src/core/MutationGenerator.ts | 825 +++++++++--------- packages/binding/src/core/QueryGenerator.ts | 170 ++-- packages/binding/src/core/index.ts | 1 + .../binding/src/core/schema/SchemaLoader.ts | 4 +- .../src/core/utils/createQueryBuilder.ts | 31 + .../src/accessorTree/AccessorTreeState.ts | 7 +- .../accessorTree/AccessorTreeStateAction.ts | 7 +- .../src/accessorTree/useDataBinding.ts | 26 +- 17 files changed, 615 insertions(+), 774 deletions(-) delete mode 100644 packages/binding/src/accessorTree/MutationRequestResponse.ts delete mode 100644 packages/binding/src/accessorTree/RequestError.ts delete mode 100644 packages/binding/src/accessorTree/metadataToRequestError.ts create mode 100644 packages/binding/src/core/utils/createQueryBuilder.ts diff --git a/packages/admin/src/components/bindingFacade/renderers/FeedbackRenderer.tsx b/packages/admin/src/components/bindingFacade/renderers/FeedbackRenderer.tsx index c9d36ef267..168f0e6ef1 100644 --- a/packages/admin/src/components/bindingFacade/renderers/FeedbackRenderer.tsx +++ b/packages/admin/src/components/bindingFacade/renderers/FeedbackRenderer.tsx @@ -19,28 +19,14 @@ export function FeedbackRenderer({ accessorTreeState, children }: FeedbackRender } if (accessorTreeState.name === 'error') { - switch (accessorTreeState.error.type) { - case 'unauthorized': - return null // This results in a redirect for now, and so the actual handling is in an effect - - case 'networkError': - if (import.meta.env.DEV) { - throw new Error(accessorTreeState.error.metadata.responseText) - } - - return Network error - - case 'gqlError': - if (import.meta.env.DEV) { - throw new Error(JSON.stringify(accessorTreeState.error.errors, null, ' ')) - } - - return Unknown error - - case 'unknownError': - default: - return Unknown error // TODO + if (accessorTreeState.error.type === 'unauthorized') { + return null // This results in a redirect for now, and so the actual handling is in an effect + } + if (import.meta.env.DEV) { + throw accessorTreeState.error } + + return {accessorTreeState.error.type} } return <>{children} diff --git a/packages/binding/src/accessorTree/MutationRequestResponse.ts b/packages/binding/src/accessorTree/MutationRequestResponse.ts deleted file mode 100644 index 6bc118ae8c..0000000000 --- a/packages/binding/src/accessorTree/MutationRequestResponse.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type { Result } from '@contember/client' -import type { ReceivedEntityData } from './QueryRequestResponse' - -export interface FieldPathErrorFragment { - __typename: '_FieldPathFragment' - field: string -} - -export interface IndexPathErrorFragment { - __typename: '_IndexPathFragment' - index: number - alias: string | null -} - -export type ErrorPathNodeType = FieldPathErrorFragment | IndexPathErrorFragment - -export type MutationErrorPath = ErrorPathNodeType[] - -export interface ValidationError { - path: MutationErrorPath - message: { - text: string - } -} - -export interface ExecutionError { - path: MutationErrorPath - type: Result.ExecutionErrorType - message: string | null -} - -export interface MutationResponse { - ok: boolean - errorMessage: string | null - errors: ExecutionError[] - validation: - | { - valid: boolean - errors: ValidationError[] - } - | undefined - node: ReceivedEntityData -} - -export interface MutationDataResponse { - [alias: string]: MutationResponse -} - - -export interface MutationTransactionResponse { - transaction: ( - & MutationDataResponse - & { - __typename: 'MutationTransaction' - ok: boolean - errorMessage: string | null - } - ) -} - -export interface MutationRequestResponse { - data: MutationTransactionResponse | null - errors?: { message: string, path?: string[] }[] -} diff --git a/packages/binding/src/accessorTree/PersistResult.ts b/packages/binding/src/accessorTree/PersistResult.ts index ab0afd8d94..c080c4e0f4 100644 --- a/packages/binding/src/accessorTree/PersistResult.ts +++ b/packages/binding/src/accessorTree/PersistResult.ts @@ -1,4 +1,3 @@ -import type { RequestError } from './RequestError' import { ErrorAccessor } from '../accessors' import { EntityId } from '../treeParameters' @@ -24,4 +23,4 @@ export interface InvalidResponseResult { export type SuccessfulPersistResult = NothingToPersistPersistResult | JustSuccessPersistResult -export type ErrorPersistResult = InvalidInputPersistResult | RequestError | InvalidResponseResult +export type ErrorPersistResult = InvalidInputPersistResult | InvalidResponseResult diff --git a/packages/binding/src/accessorTree/RequestError.ts b/packages/binding/src/accessorTree/RequestError.ts deleted file mode 100644 index ddc6623749..0000000000 --- a/packages/binding/src/accessorTree/RequestError.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { GraphQlClientFailedRequestMetadata } from '@contember/client' - -export interface UnauthorizedRequestError { - type: 'unauthorized' -} - -export interface NetworkErrorRequestError { - type: 'networkError' - metadata: GraphQlClientFailedRequestMetadata -} - -export interface UnknownErrorRequestError { - type: 'unknownError' -} - -export interface GqlError { - type: 'gqlError' - query: string, - errors: { message: string, path?: string[] }[] -} - -export type RequestError = UnauthorizedRequestError | NetworkErrorRequestError | GqlError | UnknownErrorRequestError diff --git a/packages/binding/src/accessorTree/index.ts b/packages/binding/src/accessorTree/index.ts index fd0758ef15..2eb2fe30ef 100644 --- a/packages/binding/src/accessorTree/index.ts +++ b/packages/binding/src/accessorTree/index.ts @@ -1,7 +1,4 @@ -export * from './metadataToRequestError' -export * from './MutationRequestResponse' export * from './NormalizedPersistedData' export * from './PersistResult' export * from './QueryRequestResponse' -export * from './RequestError' export * from './RuntimeId' diff --git a/packages/binding/src/accessorTree/metadataToRequestError.ts b/packages/binding/src/accessorTree/metadataToRequestError.ts deleted file mode 100644 index 585a309183..0000000000 --- a/packages/binding/src/accessorTree/metadataToRequestError.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { GraphQlClientFailedRequestMetadata } from '@contember/client' -import type { RequestError } from './RequestError' - -export const metadataToRequestError = (metadata: GraphQlClientFailedRequestMetadata): RequestError => { - if (metadata.status === 401) { - return { - type: 'unauthorized', - } - } - return { - type: 'networkError', - metadata: metadata, - } -} diff --git a/packages/binding/src/core/AccessorErrorManager.ts b/packages/binding/src/core/AccessorErrorManager.ts index 7102fb0be2..fe075f3205 100644 --- a/packages/binding/src/core/AccessorErrorManager.ts +++ b/packages/binding/src/core/AccessorErrorManager.ts @@ -1,10 +1,11 @@ import { ErrorAccessor } from '../accessors' -import type { ExecutionError, MutationDataResponse, ValidationError } from '../accessorTree' import { ErrorsPreprocessor } from './ErrorsPreprocessor' import { EventManager } from './EventManager' import { EntityListState, EntityRealmState, getEntityMarker, StateNode } from './state' import type { TreeStore } from './TreeStore' import { SubMutationOperation } from './MutationGenerator' +import { DataBindingTransactionResult } from './DataBinding' +import { MutationError, ValidationError } from '@contember/client' export class AccessorErrorManager { private errorsByState: Map = new Map() @@ -46,7 +47,7 @@ export class AccessorErrorManager { }) } - public replaceErrors(data: MutationDataResponse, operations: SubMutationOperation[]) { + public replaceErrors(data: DataBindingTransactionResult, operations: SubMutationOperation[]) { this.eventManager.syncOperation(() => { this.clearErrors() @@ -204,19 +205,20 @@ export class AccessorErrorManager { } } - private dumpErrorData(data: MutationDataResponse) { + private dumpErrorData(data: DataBindingTransactionResult) { // TODO this is just temporary - for (const subTreePlaceholder in data) { - const treeDatum = data[subTreePlaceholder] - const executionErrors: Array = treeDatum.errors + for (const subTreePlaceholder in data.data) { + const treeDatum = data.data[subTreePlaceholder] + const executionErrors: Array = treeDatum.errors const allErrors = treeDatum?.validation?.errors ? executionErrors.concat(treeDatum.validation.errors) : executionErrors - const normalizedErrors = allErrors.map((error: ExecutionError | ValidationError) => { + const normalizedErrors = allErrors.map((error: MutationError | ValidationError) => { + const path = 'paths' in error ? (error.paths[0] ?? []) : error.path return { - path: error.path + path: path .map(pathPart => { - if (pathPart.__typename === '_FieldPathFragment') { + if ('field' in pathPart) { return pathPart.field } if (pathPart.alias) { diff --git a/packages/binding/src/core/DataBinding.ts b/packages/binding/src/core/DataBinding.ts index 173e146c7c..6119689bdb 100644 --- a/packages/binding/src/core/DataBinding.ts +++ b/packages/binding/src/core/DataBinding.ts @@ -1,4 +1,11 @@ -import type { GraphQlClient, GraphQlClientFailedRequestMetadata, TreeFilter } from '@contember/client' +import { + ContentClient, ContentQueryBuilder, + GraphQlClient, + GraphQlClientError, + MutationResult, + TransactionResult, + TreeFilter, +} from '@contember/client' import { AsyncBatchUpdatesOptions, BatchUpdatesOptions, @@ -8,17 +15,10 @@ import { PersistSuccessOptions, TreeRootAccessor, } from '../accessors' -import { - ErrorPersistResult, - metadataToRequestError, MutationDataResponse, - MutationRequestResponse, - QueryRequestResponse, - RequestError, - SuccessfulPersistResult, -} from '../accessorTree' +import { ErrorPersistResult, ReceivedDataTree, ReceivedEntityData, SuccessfulPersistResult } from '../accessorTree' import type { Environment } from '../dao' import type { MarkerTreeRoot } from '../markers' -import type { TreeRootId } from '../treeParameters' +import type { EntityId, TreeRootId } from '../treeParameters' import { assertNever, generateEnumerabilityPreventingEntropy } from '../utils' import { AccessorErrorManager } from './AccessorErrorManager' import { Config } from './Config' @@ -35,6 +35,9 @@ import { TreeFilterGenerator } from './TreeFilterGenerator' import { TreeStore } from './TreeStore' import type { UpdateMetadata } from './UpdateMetadata' import { getCombinedSignal } from './utils' +import { createQueryBuilder } from './utils/createQueryBuilder' + +export type DataBindingTransactionResult = TransactionResult>> export class DataBinding { private readonly accessorErrorManager: AccessorErrorManager @@ -46,6 +49,8 @@ export class DataBinding { private readonly eventManager: EventManager private readonly stateInitializer: StateInitializer private readonly treeAugmenter: TreeAugmenter + private readonly queryBuilder: ContentQueryBuilder + private readonly contentClient: ContentClient // private treeRootListeners: { // eventListeners: {} @@ -62,7 +67,7 @@ export class DataBinding { private readonly createMarkerTree: (node: Node, environment: Environment) => MarkerTreeRoot, private readonly batchedUpdates: (callback: () => any) => void, private readonly onUpdate: (newData: TreeRootAccessor, binding: DataBinding) => void, - private readonly onError: (error: RequestError, binding: DataBinding) => void, + private readonly onError: (error: GraphQlClientError, binding: DataBinding) => void, private readonly onPersistSuccess: (result: SuccessfulPersistResult, binding: DataBinding) => void, private readonly options: { skipStateUpdateAfterPersist: boolean @@ -108,6 +113,8 @@ export class DataBinding { extendTree: async (...args) => await this.extendTree(...args), persist: async options => await this.persist(options), }) + this.queryBuilder = createQueryBuilder(this.environment.getSchema()) + this.contentClient = new ContentClient(contentApiClient.execute.bind(contentApiClient)) } private async persist({ onPersistError, onPersistSuccess, signal }: PersistOptions = {}) { @@ -121,7 +128,7 @@ export class DataBinding { await this.checkErrorsBeforePersist(onPersistError) - const generator = new MutationGenerator(this.treeStore) + const generator = new MutationGenerator(this.treeStore, this.queryBuilder) const mutationResult = generator.getPersistMutation() if (mutationResult === undefined) { @@ -131,36 +138,33 @@ export class DataBinding { } } - const { query, operations } = mutationResult + const { mutations, operations } = mutationResult - const mutationResponse: MutationRequestResponse = await this.contentApiClient.sendRequest(query, { - signal, - }) - - if (mutationResponse.errors !== undefined && mutationResponse.errors.length > 0) { - this.onError({ - type: 'gqlError', - query, - errors: mutationResponse.errors, - }, this) - } - - if (!mutationResponse.data?.transaction) { - this.persistFail({ - errors: [], - type: 'invalidResponse', + let response: DataBindingTransactionResult + try { + response = await this.contentClient.mutate(mutations, { + signal, }) + } catch (e) { + if (e instanceof GraphQlClientError) { + this.onError(e, this) + this.persistFail({ + errors: [e], + type: 'invalidResponse', + }) + } else { + throw e + } } - const { __typename, ok, errorMessage, ...mutationData } = mutationResponse.data.transaction - if (Object.values(mutationData).every(it => it.ok)) { - return await this.processSuccessfulPersistResult(mutationData, operations, onPersistSuccess) + if (Object.values(response.data).every(it => it.ok)) { + return await this.processSuccessfulPersistResult(response, operations, onPersistSuccess) } else { - if (errorMessage) { - console.error(errorMessage) + if (response.errorMessage) { + console.error(response.errorMessage) } - this.eventManager.syncTransaction(() => this.accessorErrorManager.replaceErrors(mutationData, operations)) + this.eventManager.syncTransaction(() => this.accessorErrorManager.replaceErrors(response, operations)) await this.eventManager.triggerOnPersistError(this.bindingOperations) await onPersistError?.(this.bindingOperations) @@ -195,8 +199,8 @@ export class DataBinding { await onPersistSuccess?.(persistSuccessOptions) } - private async processSuccessfulPersistResult(mutationData: MutationDataResponse, operations: SubMutationOperation[], onPersistSuccess: PersistOptions['onPersistSuccess']) { - const persistedEntityIds = Object.values(mutationData).map(it => it.node?.id).filter(id => id !== undefined) + private async processSuccessfulPersistResult(mutationData: DataBindingTransactionResult, operations: SubMutationOperation[], onPersistSuccess: PersistOptions['onPersistSuccess']) { + const persistedEntityIds = Object.values(mutationData.data).map(it => it.node?.id).filter((id): id is EntityId => id !== undefined) const result: SuccessfulPersistResult = { type: 'justSuccess', persistedEntityIds, @@ -207,7 +211,7 @@ export class DataBinding { this.resetTreeAfterSuccessfulPersist() this.treeAugmenter.updatePersistedData( Object.fromEntries( - Object.entries(mutationData).map(([placeholderName, subTreeResponse]) => [ + Object.entries(mutationData.data).map(([placeholderName, subTreeResponse]) => [ placeholderName, subTreeResponse.node, ]), @@ -330,7 +334,7 @@ export class DataBinding { } this.eventManager.syncOperation(() => { - this.treeAugmenter.extendPersistedData(aggregatePersistedData.data, aggregateMarkerTreeRoot) + this.treeAugmenter.extendPersistedData(aggregatePersistedData, aggregateMarkerTreeRoot) for (const extension of pendingExtensions) { this.treeAugmenter.extendTreeStates(extension.newTreeRootId, extension.markerTreeRoot) @@ -342,35 +346,22 @@ export class DataBinding { private async fetchPersistedData( tree: MarkerTreeRoot, signal?: AbortSignal, - ): Promise { - const queryGenerator = new QueryGenerator(tree) + ): Promise { + const queryGenerator = new QueryGenerator(tree, this.queryBuilder) const query = queryGenerator.getReadQuery() - let queryResponse: QueryRequestResponse | undefined = undefined - try { - queryResponse = - query === undefined - ? undefined - : await this.contentApiClient.sendRequest(query, { - signal, - }) - } catch (metadata) { - if (typeof metadata === 'object' && metadata !== null && (metadata as { name?: unknown }).name === 'AbortError') { - return + return await this.contentClient.query(query, { + signal, + }) + } catch (e) { + if (e instanceof GraphQlClientError) { + if (e.type === 'aborted') { + return undefined + } + this.onError(e, this) } - this.onError(metadataToRequestError(metadata as GraphQlClientFailedRequestMetadata), this) - } - - if (queryResponse && queryResponse.errors !== undefined && queryResponse.errors.length > 0) { - this.onError({ - type: 'gqlError', - query: query ?? 'unknown query', - errors: queryResponse.errors, - }, this) } - - return queryResponse } private resetTreeAfterSuccessfulPersist() { diff --git a/packages/binding/src/core/ErrorsPreprocessor.ts b/packages/binding/src/core/ErrorsPreprocessor.ts index 1fc5a9fd37..26c3bfeaac 100644 --- a/packages/binding/src/core/ErrorsPreprocessor.ts +++ b/packages/binding/src/core/ErrorsPreprocessor.ts @@ -1,13 +1,14 @@ import type { ErrorAccessor } from '../accessors' -import type { ExecutionError, MutationDataResponse, MutationResponse, ValidationError } from '../accessorTree' import type { EntityId, FieldName, PlaceholderName } from '../treeParameters' import { assertNever } from '../utils' import { MutationAlias } from './requestAliases' import { SubMutationOperation } from './MutationGenerator' +import { DataBindingTransactionResult } from './DataBinding' +import { MutationError, MutationResult, ValidationError } from '@contember/client' class ErrorsPreprocessor { public constructor( - private readonly requestResponse: MutationDataResponse, + private readonly requestResponse: DataBindingTransactionResult, private readonly operations: SubMutationOperation[], ) {} @@ -19,7 +20,7 @@ class ErrorsPreprocessor { } for (const operation of this.operations) { - const mutationResponse = this.requestResponse[operation.alias] + const mutationResponse = this.requestResponse.data[operation.alias] const processedResponse = this.processMutationResponse(mutationResponse) if (processedResponse === undefined) { @@ -53,7 +54,7 @@ class ErrorsPreprocessor { return treeRoot } - private processMutationResponse(mutationResponse: MutationResponse): ErrorsPreprocessor.ErrorNode | undefined { + private processMutationResponse(mutationResponse: MutationResult): ErrorsPreprocessor.ErrorNode | undefined { if (mutationResponse.ok && mutationResponse.validation?.valid && mutationResponse.errors.length === 0) { return undefined } @@ -66,7 +67,7 @@ class ErrorsPreprocessor { return undefined } - private getErrorNode(errors: ValidationError[] | ExecutionError[]): ErrorsPreprocessor.ErrorNode { + private getErrorNode(errors: ValidationError[] | MutationError[]): ErrorsPreprocessor.ErrorNode { const [head, ...tail] = errors let rootNode = this.getRootNode(head) @@ -74,23 +75,25 @@ class ErrorsPreprocessor { errorLoop: for (const error of tail) { let currentNode = rootNode - for (let i = 0, pathLength = error.path.length; i < pathLength; i++) { - const pathNode = error.path[i] + const path = 'paths' in error ? (error.paths[0] ?? []) : error.path + + for (let i = 0, pathLength = path.length; i < pathLength; i++) { + const pathNode = path[i] if (currentNode.nodeType === 'leaf') { (currentNode as any as ErrorsPreprocessor.ErrorINode).nodeType = 'iNode' ;(currentNode as any as ErrorsPreprocessor.ErrorINode).children = new Map() } - if (pathNode.__typename === '_FieldPathFragment') { + if ('field' in pathNode) { if (currentNode.nodeType === 'iNode') { let alias = pathNode.field let nextIndex = i + 1 - if (nextIndex in error.path) { - const nextPathNode = error.path[nextIndex] + if (nextIndex in path) { + const nextPathNode = path[nextIndex] if ( - nextPathNode.__typename === '_IndexPathFragment' && + 'index' in nextPathNode && typeof nextPathNode.alias === 'string' && nextPathNode.alias.startsWith(pathNode.field) ) { @@ -103,7 +106,7 @@ class ErrorsPreprocessor { if (!currentNode.children.has(alias)) { currentNode.children.set(alias, this.getRootNode(error, nextIndex)) - if (nextIndex <= error.path.length) { + if (nextIndex <= path.length) { // This path has been handled by getRootNode continue errorLoop } @@ -112,7 +115,7 @@ class ErrorsPreprocessor { } else { this.rejectCorruptData() } - } else if (pathNode.__typename === '_IndexPathFragment') { + } else if ('index' in path) { if (currentNode.nodeType === 'iNode') { const alias = pathNode.alias @@ -126,7 +129,7 @@ class ErrorsPreprocessor { if (!currentNode.children.has(entityId)) { currentNode.children.set(entityId, this.getRootNode(error, i + 1)) - if (i + 1 <= error.path.length) { + if (i + 1 <= path.length) { // This path has been handled by getRootNode continue errorLoop } @@ -135,8 +138,6 @@ class ErrorsPreprocessor { } else { this.rejectCorruptData() } - } else { - assertNever(pathNode) } } if (this.isExecutionError(error)) { @@ -149,23 +150,23 @@ class ErrorsPreprocessor { return rootNode } - private getRootNode(error: ValidationError | ExecutionError, startIndex: number = 0): ErrorsPreprocessor.ErrorNode { + private getRootNode(error: ValidationError | MutationError, startIndex: number = 0): ErrorsPreprocessor.ErrorNode { let rootNode: ErrorsPreprocessor.ErrorNode = { validation: this.isExecutionError(error) ? [] : [this.createValidationError(error.message.text)], execution: this.isExecutionError(error) ? [{ type: 'execution', code: error.type, developerMessage: error.message }] : [], nodeType: 'leaf', } - - for (let i = error.path.length - 1; i >= startIndex; i--) { - const pathNode = error.path[i] - if (pathNode.__typename === '_FieldPathFragment') { + const path = 'paths' in error ? (error.paths[0] ?? []) : error.path + for (let i = path.length - 1; i >= startIndex; i--) { + const pathNode = path[i] + if ('field' in pathNode) { rootNode = { validation: [], execution: [], nodeType: 'iNode', children: new Map([[pathNode.field, rootNode]]), } - } else if (pathNode.__typename === '_IndexPathFragment') { + } else if ('index' in pathNode) { const alias = pathNode.alias if (alias === null) { @@ -202,7 +203,7 @@ class ErrorsPreprocessor { return { type: 'validation', message, code: undefined } } - private isExecutionError(error: ValidationError | ExecutionError): error is ExecutionError { + private isExecutionError(error: ValidationError | MutationError): error is MutationError { return 'type' in error } diff --git a/packages/binding/src/core/MutationGenerator.ts b/packages/binding/src/core/MutationGenerator.ts index 1a394cd37d..abadc35489 100644 --- a/packages/binding/src/core/MutationGenerator.ts +++ b/packages/binding/src/core/MutationGenerator.ts @@ -1,26 +1,15 @@ -// noinspection JSVoidFunctionReturnValueUsed // IntelliJ seems to be super confused througouht this file. - -import { CrudQueryBuilder, GraphQlBuilder } from '@contember/client' -import { ClientGeneratedUuid, ServerId } from '../accessorTree' +import { ContentMutation, ContentQueryBuilder, GraphQlLiteral, Input, replaceGraphQlLiteral } from '@contember/client' +import { ClientGeneratedUuid, EntityFieldPersistedData, ReceivedEntityData, ServerId } from '../accessorTree' import { BindingError } from '../BindingError' import { PRIMARY_KEY_NAME, TYPENAME_KEY_NAME } from '../bindingTypes' import { EntityFieldMarkers, FieldMarker, HasManyRelationMarker, HasOneRelationMarker } from '../markers' -import type { EntityId, EntityName, FieldValue, PlaceholderName } from '../treeParameters' +import type { EntityId, EntityName, PlaceholderName, UniqueWhere } from '../treeParameters' import { assertNever, isEmptyObject } from '../utils' import { QueryGenerator } from './QueryGenerator' import { MutationAlias } from './requestAliases' -import { - EntityListState, - EntityRealmState, - EntityRealmStateStub, - EntityState, - FieldState, - getEntityMarker, - StateIterator, -} from './state' +import { EntityListState, EntityRealmState, EntityRealmStateStub, EntityState, FieldState, getEntityMarker, StateIterator } from './state' import type { TreeStore } from './TreeStore' -type QueryBuilder = Omit type ProcessedPlaceholdersByEntity = Map> @@ -37,7 +26,7 @@ export type SubMutationOperation = ) export type PersistMutationResult = { - query: string + mutations: Record> operations: SubMutationOperation[] } @@ -46,105 +35,97 @@ export class MutationGenerator { private aliasCounter = 1 - public constructor(private readonly treeStore: TreeStore) { + public constructor( + private readonly treeStore: TreeStore, + private readonly qb: ContentQueryBuilder, + ) { } public getPersistMutation(): PersistMutationResult | undefined { - try { - let builder: QueryBuilder = new CrudQueryBuilder.CrudQueryBuilder() - const operations: SubMutationOperation[] = [] - const processedPlaceholdersByEntity: ProcessedPlaceholdersByEntity = new Map() - const processedDeletes = new Set() - - - for (const [treeRootId, rootStates] of this.treeStore.subTreeStatesByRoot.entries()) { - for (const [placeholderName, subTreeState] of Array.from(rootStates.entries()).reverse()) { - if (subTreeState.unpersistedChangesCount <= 0) { + const mutations: Record> = {} + const operations: SubMutationOperation[] = [] + const processedPlaceholdersByEntity: ProcessedPlaceholdersByEntity = new Map() + const processedDeletes = new Set() + + for (const [treeRootId, rootStates] of this.treeStore.subTreeStatesByRoot.entries()) { + for (const [placeholderName, subTreeState] of Array.from(rootStates.entries()).reverse()) { + if (subTreeState.unpersistedChangesCount <= 0) { + continue + } + if (subTreeState.type === 'entityRealm') { + if (subTreeState.blueprint.type === 'subTree' && subTreeState.blueprint.marker.parameters.isCreating && subTreeState.blueprint.marker.parameters.isUnpersisted) { continue } - if (subTreeState.type === 'entityRealm') { - if (subTreeState.blueprint.type === 'subTree' && subTreeState.blueprint.marker.parameters.isCreating && subTreeState.blueprint.marker.parameters.isUnpersisted) { + const subMutation = this.createMutation( + processedPlaceholdersByEntity, + placeholderName, + 'single', + subTreeState.entity.id.value, + subTreeState, + ) + if (subMutation) { + mutations[subMutation[1].alias] = subMutation[0] + operations.push(subMutation[1]) + } + } else if (subTreeState.type === 'entityList') { + if (subTreeState.blueprint.type === 'subTree' && subTreeState.blueprint.marker.parameters.isCreating && subTreeState.blueprint.marker.parameters.isUnpersisted) { + continue + } + for (const childState of subTreeState.children.values()) { + if (childState.type === 'entityRealmStub') { + // TODO there can be a forceCreate somewhere in there that we're hereby ignoring. continue } - const subMutation = this.addSubMutation( + const subMutation = this.createMutation( processedPlaceholdersByEntity, placeholderName, - 'single', - subTreeState.entity.id.value, - subTreeState, - builder, + 'list', + childState.entity.id.value, + childState, ) if (subMutation) { - builder = subMutation[0] + mutations[subMutation[1].alias] = subMutation[0] operations.push(subMutation[1]) } - } else if (subTreeState.type === 'entityList') { - if (subTreeState.blueprint.type === 'subTree' && subTreeState.blueprint.marker.parameters.isCreating && subTreeState.blueprint.marker.parameters.isUnpersisted) { - continue - } - for (const childState of subTreeState.children.values()) { - if (childState.type === 'entityRealmStub') { - // TODO there can be a forceCreate somewhere in there that we're hereby ignoring. + } + if (subTreeState.plannedRemovals) { + for (const [removedId, removalType] of subTreeState.plannedRemovals) { + if (removalType === 'disconnect') { continue } - const subMutation = this.addSubMutation( - processedPlaceholdersByEntity, - placeholderName, - 'list', - childState.entity.id.value, - childState, - builder, - ) - if (subMutation) { - builder = subMutation[0] - operations.push(subMutation[1]) - } - } - if (subTreeState.plannedRemovals) { - for (const [removedId, removalType] of subTreeState.plannedRemovals) { - if (removalType === 'disconnect') { + if (removalType === 'delete') { + const key = `${subTreeState.entityName}#${removedId}` + if (processedDeletes.has(key)) { continue } - if (removalType === 'delete') { - const key = `${subTreeState.entityName}#${removedId}` - if (processedDeletes.has(key)) { - continue - } - processedDeletes.add(key) - const alias = this.createAlias() - builder = this.addDeleteMutation( - subTreeState.entityName, - removedId, - alias, - builder, - ) - operations.push({ alias, subTreePlaceholder: placeholderName, subTreeType: 'list', type: 'delete', id: removedId }) - } else { - assertNever(removalType) - } + processedDeletes.add(key) + const alias = this.createAlias() + mutations[alias] = this.createDeleteMutation( + subTreeState.entityName, + removedId, + ) + operations.push({ alias, subTreePlaceholder: placeholderName, subTreeType: 'list', type: 'delete', id: removedId }) + } else { + assertNever(removalType) } } - } else { - assertNever(subTreeState) } + } else { + assertNever(subTreeState) } } - - const query = builder.inTransaction(undefined, { deferForeignKeyConstraints: true }).getGql() - return { query, operations } - } catch (e) { - return undefined } + + return { mutations, operations } } - private addSubMutation( + private createMutation( processedPlaceholdersByEntity: ProcessedPlaceholdersByEntity, subTreePlaceholder: PlaceholderName, subTreeType: 'list' | 'single', entityId: EntityId, realmState: EntityRealmState, - queryBuilder: QueryBuilder, - ): [QueryBuilder, SubMutationOperation] | undefined { + ): [ContentMutation, SubMutationOperation] | undefined { if (realmState.unpersistedChangesCount === 0) { // Bail out early return undefined @@ -154,14 +135,12 @@ export class MutationGenerator { const alias = this.createAlias() if (realmState.entity.isScheduledForDeletion) { - const builder = this.addDeleteMutation( + const mutation = this.createDeleteMutation( realmState.entity.entityName, entityId, - alias, - queryBuilder, ) return [ - builder, + mutation, { alias, subTreePlaceholder, @@ -171,12 +150,9 @@ export class MutationGenerator { }, ] } else if (!realmState.entity.id.existsOnServer) { - const builder = this.addCreateMutation( + const builder = this.createCreateMutation( processedPlaceholdersByEntity, realmState, - alias, - this.getNodeFragmentName(subTreePlaceholder, subTreeType, realmState), - queryBuilder, fieldMarkers, ) if (!builder) { @@ -194,12 +170,9 @@ export class MutationGenerator { }, ] } - const builder = this.addUpdateMutation( + const builder = this.createUpdateMutation( processedPlaceholdersByEntity, realmState, - alias, - this.getNodeFragmentName(subTreePlaceholder, subTreeType, realmState), - queryBuilder, fieldMarkers, ) if (!builder) { @@ -218,51 +191,23 @@ export class MutationGenerator { ] } - private getNodeFragmentName( - subTreePlaceholder: PlaceholderName, - subTreeType: 'single' | 'list', - realmState: EntityRealmState, - ): string | undefined { - if (subTreeType !== 'list') { - return undefined - } - return `${realmState.entity.entityName}_${subTreePlaceholder}` - } - - private addDeleteMutation( + private createDeleteMutation( entityName: EntityName, deletedId: EntityId, - alias: string, - queryBuilder: QueryBuilder = new CrudQueryBuilder.CrudQueryBuilder(), - ): QueryBuilder { - return queryBuilder.delete( - entityName, - builder => - builder - .ok() - .node(builder => builder.column(PRIMARY_KEY_NAME)) - .by({ [PRIMARY_KEY_NAME]: deletedId }) - .errors() - .errorMessage(), - alias, - ) + ): ContentMutation { + return this.qb.delete(entityName, { + by: { [PRIMARY_KEY_NAME]: deletedId }, + }, it => it.$(PRIMARY_KEY_NAME).$('__typename')) } - private addUpdateMutation( + private createUpdateMutation( processedPlaceholdersByEntity: ProcessedPlaceholdersByEntity, entityRealm: EntityRealmState, - alias: string, - nodeFragmentName: string | undefined, - queryBuilder: QueryBuilder = new CrudQueryBuilder.CrudQueryBuilder(), fieldMarkers: EntityFieldMarkers, - ): QueryBuilder | undefined { - const updateBuilder: CrudQueryBuilder.WriteDataBuilder = - this.registerUpdateMutationPart( - processedPlaceholdersByEntity, - entityRealm, - new CrudQueryBuilder.WriteDataBuilder(), - ) - if (updateBuilder.data === undefined || isEmptyObject(updateBuilder.data)) { + ): ContentMutation | undefined { + const input = this.getUpdateDataInput(processedPlaceholdersByEntity, entityRealm) + + if (input === undefined) { return undefined } @@ -272,80 +217,36 @@ export class MutationGenerator { return undefined } - const readBuilder = QueryGenerator.registerQueryPart( - fieldMarkers, - CrudQueryBuilder.ReadBuilder.instantiate(), - ) - - if (nodeFragmentName) { - queryBuilder = queryBuilder.fragment(nodeFragmentName, entityRealm.entity.entityName, readBuilder) - } - - return queryBuilder.update( - entityRealm.entity.entityName, - builder => - builder - .node(nodeFragmentName ? builder => builder.applyFragment(nodeFragmentName) : readBuilder) - .data(updateBuilder) - .by({ [PRIMARY_KEY_NAME]: runtimeId.value }) - .ok() - .validation() - .errors() - .errorMessage(), - alias, - ) + return this.qb.update(entityRealm.entity.entityName, { + by: { + [PRIMARY_KEY_NAME]: runtimeId.value, + }, + data: input, + }, it => QueryGenerator.registerQueryPart(fieldMarkers, it).$(PRIMARY_KEY_NAME).$('__typename')) } - private addCreateMutation( + private createCreateMutation( processedPlaceholdersByEntity: ProcessedPlaceholdersByEntity, entityRealm: EntityRealmState, - alias: string, - nodeFragmentName: string | undefined, - queryBuilder: QueryBuilder = new CrudQueryBuilder.CrudQueryBuilder(), fieldMarkers: EntityFieldMarkers, - ): QueryBuilder | undefined { - const createBuilder: CrudQueryBuilder.WriteDataBuilder = - this.registerCreateMutationPart( - processedPlaceholdersByEntity, - entityRealm, - new CrudQueryBuilder.WriteDataBuilder(), - ) - if (createBuilder.data === undefined || isEmptyObject(createBuilder.data)) { - return undefined - } - const readBuilder = QueryGenerator.registerQueryPart( - fieldMarkers, - CrudQueryBuilder.ReadBuilder.instantiate(), + ): ContentMutation | undefined { + const input = this.getCreateDataInput( + processedPlaceholdersByEntity, + entityRealm, ) - if (nodeFragmentName) { - queryBuilder = queryBuilder.fragment(nodeFragmentName, entityRealm.entity.entityName, readBuilder) + if (input === undefined) { + return undefined } - return queryBuilder.create( - entityRealm.entity.entityName, - builder => { - // if (where && writeBuilder.data !== undefined && !isEmptyObject(writeBuilder.data)) { - // // Shallow cloning the parameters like this IS too naïve but it will likely last surprisingly long before we - // // run into issues. - // writeBuilder = new CrudQueryBuilder.WriteDataBuilder({ ...writeBuilder.data, ...where }) - // } - return builder - .node(nodeFragmentName ? builder => builder.applyFragment(nodeFragmentName) : readBuilder) - .data(createBuilder) - .ok() - .validation() - .errors() - .errorMessage() - }, - alias, - ) + return this.qb.create(entityRealm.entity.entityName, { + data: input, + }, it => QueryGenerator.registerQueryPart(fieldMarkers, it).$(PRIMARY_KEY_NAME).$('__typename')) } - private registerCreateMutationPart( + private getCreateDataInput( processedPlaceholdersByEntity: ProcessedPlaceholdersByEntity, currentState: EntityRealmState | EntityRealmStateStub, - builder: CrudQueryBuilder.WriteDataBuilder = new CrudQueryBuilder.WriteDataBuilder(), - ): CrudQueryBuilder.WriteDataBuilder { + ): Input.CreateDataInput | undefined { let processedPlaceholders = processedPlaceholdersByEntity.get(currentState.entity) if (processedPlaceholders === undefined) { @@ -354,9 +255,10 @@ export class MutationGenerator { if (currentState.type === 'entityRealmStub') { // TODO If there's a forceCreate, this is wrong. - return builder + return undefined } + const result: Input.CreateDataInput = {} for (const siblingState of StateIterator.eachSiblingRealm(currentState)) { const pathBack = this.treeStore.getPathBackToParent(siblingState) @@ -390,7 +292,10 @@ export class MutationGenerator { }) continue } - builder = this.registerCreateFieldPart(fieldState, marker, builder) + const value = fieldState.getAccessor().resolvedValue + if (value !== undefined && value !== null) { + result[marker.fieldName] = value + } break } case 'entityRealmStub': @@ -410,7 +315,10 @@ export class MutationGenerator { }) continue } - builder = this.registerCreateEntityPart(processedPlaceholdersByEntity, fieldState, marker, builder) + const input = this.getCreateOneRelationInput(processedPlaceholdersByEntity, fieldState, marker) + if (input !== undefined) { + result[marker.parameters.field] = input + } break } case 'entityList': { @@ -429,7 +337,10 @@ export class MutationGenerator { }) continue } - builder = this.registerCreateEntityListPart(processedPlaceholdersByEntity, fieldState, marker, builder) + const input = this.getCreateManyRelationInput(processedPlaceholdersByEntity, fieldState) + if (input !== undefined) { + result[marker.parameters.field] = input + } break } default: { @@ -438,39 +349,35 @@ export class MutationGenerator { } } - // if ( - // currentState.blueprint.creationParameters.forceCreation && - // (builder.data === undefined || isEmptyObject(builder.data)) - // ) { - // builder = builder.set('_dummy_field_', true) - // } - - if ( - (builder.data !== undefined && !isEmptyObject(builder.data)) || - !getEntityMarker(siblingState).fields.hasAtLeastOneBearingField - ) { + if ((!isEmptyObject(result)) || !getEntityMarker(siblingState).fields.hasAtLeastOneBearingField) { for (const field of nonbearingFields) { switch (field.type) { case 'field': { - builder = this.registerCreateFieldPart(field.fieldState, field.marker, builder) + const value = field.fieldState.getAccessor().resolvedValue + if (value !== undefined) { + result[field.marker.fieldName] = value + } break } case 'hasOne': { - builder = this.registerCreateEntityPart( + const input = this.getCreateOneRelationInput( processedPlaceholdersByEntity, field.fieldState, field.marker, - builder, ) + if (input !== undefined) { + result[field.marker.parameters.field] = input + } break } case 'hasMany': { - builder = this.registerCreateEntityListPart( + const input = this.getCreateManyRelationInput( processedPlaceholdersByEntity, field.fieldState, - field.marker, - builder, ) + if (input !== undefined) { + result[field.marker.parameters.field] = input + } break } default: @@ -480,7 +387,7 @@ export class MutationGenerator { } const setOnCreate = getEntityMarker(siblingState).parameters.setOnCreate - if (setOnCreate && builder.data !== undefined && !isEmptyObject(builder.data)) { + if (setOnCreate && !isEmptyObject(result)) { for (const key in setOnCreate) { const field = setOnCreate[key] @@ -488,79 +395,76 @@ export class MutationGenerator { typeof field === 'string' || typeof field === 'number' || field === null || - field instanceof GraphQlBuilder.GraphQlLiteral + field instanceof GraphQlLiteral ) { - builder = builder.set(key, field) + result[key] = field instanceof GraphQlLiteral ? field.value : field } else { - builder = builder.one(key, builder => builder.connect(field)) + result[key] = { connect: replaceGraphQlLiteral(field) } } } } } - return builder - } - - private registerCreateFieldPart( - fieldState: FieldState, - marker: FieldMarker, - builder: CrudQueryBuilder.WriteDataBuilder, - ) { - const resolvedValue = fieldState.getAccessor().resolvedValue - - if (resolvedValue !== undefined && resolvedValue !== null) { - return builder.set(marker.fieldName, this.transformFieldValue(fieldState, resolvedValue)) - } - return builder + return isEmptyObject(result) ? undefined : result } - private registerCreateEntityPart( + private getCreateOneRelationInput( processedPlaceholdersByEntity: ProcessedPlaceholdersByEntity, fieldState: EntityRealmState | EntityRealmStateStub, marker: HasOneRelationMarker, - builder: CrudQueryBuilder.WriteDataBuilder, - ) { + ): Input.CreateOneRelationInput | Input.CreateManyRelationInput | undefined { const reducedBy = marker.parameters.reducedBy const runtimeId = fieldState.entity.id if (reducedBy === undefined) { - return builder.one(marker.parameters.field, builder => { - if (this.shouldConnectInsteadOfCreate(processedPlaceholdersByEntity, fieldState)) { - // TODO also potentially update - return builder.connect({ [PRIMARY_KEY_NAME]: runtimeId.value }) - } - return builder.create(this.registerCreateMutationPart(processedPlaceholdersByEntity, fieldState)) - }) - } - return builder.many(marker.parameters.field, builder => { - const alias = MutationAlias.encodeEntityId(runtimeId) if (this.shouldConnectInsteadOfCreate(processedPlaceholdersByEntity, fieldState)) { - // TODO also potentially update - return builder.connect({ [PRIMARY_KEY_NAME]: runtimeId.value }, alias) + return { connect: { [PRIMARY_KEY_NAME]: runtimeId.value } } } - return builder.create(this.registerCreateMutationPart(processedPlaceholdersByEntity, fieldState), alias) - }) + + const createData = this.getCreateDataInput(processedPlaceholdersByEntity, fieldState) + if (createData === undefined) { + return undefined + } + return { create: createData } + } + + const alias = MutationAlias.encodeEntityId(runtimeId) + if (this.shouldConnectInsteadOfCreate(processedPlaceholdersByEntity, fieldState)) { + // TODO also potentially update + return [{ connect: { [PRIMARY_KEY_NAME]: runtimeId.value }, alias }] + } + const createData = this.getCreateDataInput(processedPlaceholdersByEntity, fieldState) + if (createData === undefined) { + return undefined + } + return [{ create: createData, alias }] } - private registerCreateEntityListPart( + private getCreateManyRelationInput( processedPlaceholdersByEntity: ProcessedPlaceholdersByEntity, fieldState: EntityListState, - marker: HasManyRelationMarker, - builder: CrudQueryBuilder.WriteDataBuilder, - ) { - return builder.many(marker.parameters.field, builder => { - for (const entityRealm of fieldState.children.values()) { - const runtimeId = entityRealm.entity.id - const alias = MutationAlias.encodeEntityId(runtimeId) - if (this.shouldConnectInsteadOfCreate(processedPlaceholdersByEntity, entityRealm)) { - // TODO also potentially update - builder = builder.connect({ [PRIMARY_KEY_NAME]: runtimeId.value }, alias) - } else { - builder = builder.create(this.registerCreateMutationPart(processedPlaceholdersByEntity, entityRealm), alias) + ): Input.CreateManyRelationInput | undefined { + const result: Input.CreateManyRelationInput = [] + for (const entityRealm of fieldState.children.values()) { + const runtimeId = entityRealm.entity.id + const alias = MutationAlias.encodeEntityId(runtimeId) + if (this.shouldConnectInsteadOfCreate(processedPlaceholdersByEntity, entityRealm)) { + result.push({ + alias, + connect: { [PRIMARY_KEY_NAME]: runtimeId.value }, + }) + } else { + const createData = this.getCreateDataInput(processedPlaceholdersByEntity, entityRealm) + if (createData === undefined) { + continue } + result.push({ + alias, + create: createData, + }) } - return builder - }) + } + return result.length === 0 ? undefined : result } private shouldConnectInsteadOfCreate( @@ -573,11 +477,10 @@ export class MutationGenerator { return runtimeId.existsOnServer || (runtimeId instanceof ClientGeneratedUuid && (processedPlaceholders?.has(PRIMARY_KEY_NAME) ?? false)) } - private registerUpdateMutationPart( + private getUpdateDataInput( processedPlaceholdersByEntity: ProcessedPlaceholdersByEntity, currentState: EntityRealmState, - builder: CrudQueryBuilder.WriteDataBuilder, - ): CrudQueryBuilder.WriteDataBuilder { + ): Input.UpdateDataInput | undefined { let processedPlaceholders = processedPlaceholdersByEntity.get(currentState.entity) if (processedPlaceholders === undefined) { @@ -587,6 +490,8 @@ export class MutationGenerator { const pathBack = this.treeStore.getPathBackToParent(currentState) const entityData = this.treeStore.persistedEntityData.get(currentState.entity.id.uniqueValue) + const result: Input.UpdateDataInput = {} + for (const [placeholderName, fieldState] of currentState.children) { if (placeholderName === PRIMARY_KEY_NAME || placeholderName === TYPENAME_KEY_NAME) { continue @@ -601,7 +506,7 @@ export class MutationGenerator { if (fieldState.persistedValue !== undefined) { const resolvedValue = fieldState.getAccessor().resolvedValue if (fieldState.persistedValue !== resolvedValue) { - builder = builder.set(placeholderName, this.transformFieldValue(fieldState, resolvedValue)) + result[placeholderName] = resolvedValue } } break @@ -615,105 +520,23 @@ export class MutationGenerator { if (pathBack?.fieldBackToParent === marker.parameters.field) { continue } - const runtimeId = fieldState.entity.id const reducedBy = marker.parameters.reducedBy + const persistedValue = entityData?.get?.(placeholderName) if (reducedBy === undefined) { - const subBuilder = (( - builder: CrudQueryBuilder.WriteOneRelationBuilder, - ) => { - const persistedValue = entityData?.get?.(placeholderName) - - if (persistedValue instanceof ServerId) { - if (persistedValue.value === runtimeId.value) { - // The persisted and currently referenced ids match, and so this is an update. - if (fieldState.type === 'entityRealmStub') { - return builder // …unless we're dealing with a stub. There cannot be any updates there. - } - return builder.update(builder => - this.registerUpdateMutationPart(processedPlaceholdersByEntity, fieldState, builder), - ) - } - // There was a referenced entity but currently, there is a different one. Let's investigate: - - const plannedDeletion = currentState.plannedHasOneDeletions?.get(placeholderName) - if (plannedDeletion !== undefined) { - // It's planned for deletion. - // TODO also potentially do something about the current entity - return builder.delete() - } - if (runtimeId.existsOnServer) { - // This isn't the persisted entity but it does exist on the server. Thus this is a connect. - // TODO also potentially update - return builder.connect({ - [PRIMARY_KEY_NAME]: runtimeId.value, - }) - } - // The currently present entity doesn't exist on the server. Try if creating yields anything… - const subBuilder = builder.create( - this.registerCreateMutationPart(processedPlaceholdersByEntity, fieldState), - ) - if (isEmptyObject(subBuilder.data)) { - // …and if it doesn't, we just disconnect. - return builder.disconnect() - } - // …but if it does, we return the create, disconnecting implicitly. - return subBuilder - } else if (runtimeId.existsOnServer) { - // There isn't a linked entity on the server but we're seeing one that exists there. - // Thus this is a connect. - // TODO also potentially update - return builder.connect({ - [PRIMARY_KEY_NAME]: runtimeId.value, - }) - } else { - return builder.create(this.registerCreateMutationPart(processedPlaceholdersByEntity, fieldState)) - } - })(CrudQueryBuilder.WriteOneRelationBuilder.instantiate()) - if (subBuilder.data) { - builder = builder.one(marker.parameters.field, subBuilder.data) + const relationData = this.getUpdateOneRelationInput(currentState, fieldState, persistedValue, processedPlaceholdersByEntity, placeholderName) + if (relationData !== undefined) { + result[marker.parameters.field] = relationData } + + } else { - // This is a reduced has many relation. - builder = builder.many(marker.parameters.field, builder => { - const persistedValue = entityData?.get?.(placeholderName) - const alias = MutationAlias.encodeEntityId(runtimeId) - - if (persistedValue instanceof ServerId) { - if (persistedValue.value === runtimeId.value) { - if (fieldState.type === 'entityRealmStub') { - return builder - } - return builder.update( - reducedBy, - builder => this.registerUpdateMutationPart(processedPlaceholdersByEntity, fieldState, builder), - alias, - ) - } - const plannedDeletion = currentState.plannedHasOneDeletions?.get(placeholderName) - if (plannedDeletion !== undefined) { - // TODO also potentially do something about the current entity - return builder.delete(reducedBy, alias) - } - if (runtimeId.existsOnServer) { - // TODO will re-using the alias like this work? - // TODO also potentially update - return builder.disconnect(reducedBy, alias).connect({ [PRIMARY_KEY_NAME]: runtimeId.value }, alias) - } - const subBuilder = builder.create( - this.registerCreateMutationPart(processedPlaceholdersByEntity, fieldState), - ) - if (isEmptyObject(subBuilder.data)) { - return builder.disconnect(reducedBy, alias) - } - return subBuilder - } else if (runtimeId.existsOnServer) { - return builder.connect({ [PRIMARY_KEY_NAME]: runtimeId.value }, alias) - } else { - return builder.create(this.registerCreateMutationPart(processedPlaceholdersByEntity, fieldState)) - } - }) + const relationData = this.getUpdateManyRelationForReducedInput(currentState, fieldState, persistedValue, processedPlaceholdersByEntity, placeholderName, reducedBy) + if (relationData !== undefined) { + result[marker.parameters.field] = relationData + } + } break } @@ -730,48 +553,10 @@ export class MutationGenerator { if (!(persistedEntityIds instanceof Set)) { continue } - - builder = builder.many(marker.parameters.field, builder => { - for (const childState of fieldState.children.values()) { - const runtimeId = childState.entity.id - const alias = MutationAlias.encodeEntityId(runtimeId) - - if (runtimeId.existsOnServer) { - if (persistedEntityIds.has(runtimeId.value)) { - if (childState.type !== 'entityRealmStub') { - // A stub cannot have any pending changes. - builder = builder.update( - { [PRIMARY_KEY_NAME]: runtimeId.value }, - builder => this.registerUpdateMutationPart(processedPlaceholdersByEntity, childState, builder), - alias, - ) - } - } else { - // TODO also potentially update - builder = builder.connect({ [PRIMARY_KEY_NAME]: runtimeId.value }, alias) - } - } else { - builder = builder.create( - this.registerCreateMutationPart(processedPlaceholdersByEntity, childState), - alias, - ) - } - } - if (fieldState.plannedRemovals) { - for (const [removedId, removalType] of fieldState.plannedRemovals) { - const alias = MutationAlias.encodeEntityId(new ServerId(removedId, fieldState.entityName)) - if (removalType === 'delete') { - builder = builder.delete({ [PRIMARY_KEY_NAME]: removedId }, alias) - } else if (removalType === 'disconnect') { - // TODO also potentially update - builder = builder.disconnect({ [PRIMARY_KEY_NAME]: removedId }, alias) - } else { - assertNever(removalType) - } - } - } - return builder - }) + const relationData = this.getUpdateManyRelationInput(fieldState, persistedEntityIds, processedPlaceholdersByEntity) + if (relationData !== undefined) { + result[marker.parameters.field] = relationData + } break } default: @@ -779,21 +564,229 @@ export class MutationGenerator { } } - return builder + return isEmptyObject(result) ? undefined : result } - private transformFieldValue(fieldState: FieldState, value: FieldValue): FieldValue | GraphQlBuilder.GraphQlLiteral { - if (typeof value !== 'string') { - return value + private getUpdateOneRelationInput( + currentState: EntityRealmState, + fieldState: EntityRealmState | EntityRealmStateStub, + persistedValue: EntityFieldPersistedData | undefined, + processedPlaceholdersByEntity: ProcessedPlaceholdersByEntity, + placeholderName: PlaceholderName, + ): Input.UpdateOneRelationInput | undefined { + const runtimeId = fieldState.entity.id + if (persistedValue instanceof ServerId) { + if (persistedValue.value === runtimeId.value) { + // The persisted and currently referenced ids match, and so this is an update. + if (fieldState.type === 'entityRealmStub') { + return undefined// …unless we're dealing with a stub. There cannot be any updates there. + } + const input = this.getUpdateDataInput(processedPlaceholdersByEntity, fieldState) + if (input === undefined) { + return undefined + } + return { + update: input, + } + } + + // There was a referenced entity but currently, there is a different one. Let's investigate: + + const plannedDeletion = currentState.plannedHasOneDeletions?.get(placeholderName) + if (plannedDeletion !== undefined) { + // It's planned for deletion. + // TODO also potentially do something about the current entity + return { + delete: true, + } + } + if (runtimeId.existsOnServer) { + // This isn't the persisted entity but it does exist on the server. Thus this is a connect. + // TODO also potentially update + return { + connect: { [PRIMARY_KEY_NAME]: runtimeId.value }, + } + + } + // The currently present entity doesn't exist on the server. Try if creating yields anything… + const createInput = this.getCreateDataInput(processedPlaceholdersByEntity, fieldState) + if (createInput !== undefined) { + return { + create: createInput, + } + } else { + // …but if it doesn't, we just disconnect. + return { + disconnect: true, + } + } + } else if (runtimeId.existsOnServer) { + // There isn't a linked entity on the server but we're seeing one that exists there. + // Thus this is a connect. + // TODO also potentially update + return { + connect: { [PRIMARY_KEY_NAME]: runtimeId.value }, + } + } else { + const input = this.getCreateDataInput(processedPlaceholdersByEntity, fieldState) + if (input === undefined) { + return undefined + } + return { + create: input, + } } - const fieldSchema = this.treeStore.schema.getEntityColumn( - fieldState.parent.entity.entityName, - fieldState.fieldMarker.fieldName, - ) + } - return fieldSchema.enumName === null ? value : new GraphQlBuilder.GraphQlLiteral(value) + private getUpdateManyRelationForReducedInput( + currentState: EntityRealmState, + fieldState: EntityRealmState | EntityRealmStateStub, + persistedValue: EntityFieldPersistedData | undefined, + processedPlaceholdersByEntity: ProcessedPlaceholdersByEntity, + placeholderName: PlaceholderName, + reducedBy: UniqueWhere, + ): Input.UpdateManyRelationInput | undefined { + + const runtimeId = fieldState.entity.id + const alias = MutationAlias.encodeEntityId(runtimeId) + + if (persistedValue instanceof ServerId) { + if (persistedValue.value === runtimeId.value) { + if (fieldState.type === 'entityRealmStub') { + return undefined + } + const updateData = this.getUpdateDataInput(processedPlaceholdersByEntity, fieldState) + if (updateData === undefined) { + return undefined + } + return [{ + alias, + update: { + by: replaceGraphQlLiteral(reducedBy), + data: updateData, + }, + }] + } + const plannedDeletion = currentState.plannedHasOneDeletions?.get(placeholderName) + if (plannedDeletion !== undefined) { + // TODO also potentially do something about the current entity + return [{ + alias, + delete: replaceGraphQlLiteral(reducedBy), + }] + } + if (runtimeId.existsOnServer) { + // TODO will re-using the alias like this work? + // TODO also potentially update + return [ + { + alias, + disconnect: replaceGraphQlLiteral(reducedBy), + }, + { + alias, + connect: { [PRIMARY_KEY_NAME]: runtimeId.value }, + }, + ] + } + const createInput = this.getCreateDataInput(processedPlaceholdersByEntity, fieldState) + + if (createInput === undefined) { + return [{ + alias, + disconnect: replaceGraphQlLiteral(reducedBy), + }] + } + return [{ + alias, + create: createInput, + }] + } else if (runtimeId.existsOnServer) { + return [{ + alias, + connect: { [PRIMARY_KEY_NAME]: runtimeId.value }, + }] + } else { + const createInput = this.getCreateDataInput(processedPlaceholdersByEntity, fieldState) + if (createInput === undefined) { + return undefined + } + return [{ + alias, + create: createInput, + }] + } } + private getUpdateManyRelationInput( + fieldState: EntityListState, + persistedEntityIds: Set, + processedPlaceholdersByEntity: ProcessedPlaceholdersByEntity, + ): Input.UpdateManyRelationInput | undefined { + + const input: Input.UpdateManyRelationInput = [] + for (const childState of fieldState.children.values()) { + const runtimeId = childState.entity.id + const alias = MutationAlias.encodeEntityId(runtimeId) + + if (runtimeId.existsOnServer) { + if (persistedEntityIds.has(runtimeId.value)) { + if (childState.type !== 'entityRealmStub') { + const dataInput = this.getUpdateDataInput(processedPlaceholdersByEntity, childState) + + if (dataInput !== undefined) { + input.push({ + alias, + update: { + by: { [PRIMARY_KEY_NAME]: runtimeId.value }, + data: dataInput, + }, + }) + } + } + } else { + input.push({ + alias, + connect: { [PRIMARY_KEY_NAME]: runtimeId.value }, + }) + } + } else { + const dataInput = this.getCreateDataInput(processedPlaceholdersByEntity, childState) + if (dataInput !== undefined) { + input.push({ + alias, + create: dataInput, + }) + } + } + } + if (fieldState.plannedRemovals) { + for (const [removedId, removalType] of fieldState.plannedRemovals) { + const alias = MutationAlias.encodeEntityId(new ServerId(removedId, fieldState.entityName)) + if (removalType === 'delete') { + + input.push({ + alias, + delete: { [PRIMARY_KEY_NAME]: removedId }, + }) + + } else if (removalType === 'disconnect') { + // TODO also potentially update + + input.push({ + alias, + disconnect: { [PRIMARY_KEY_NAME]: removedId }, + }) + } else { + assertNever(removalType) + } + } + } + + return input.length === 0 ? undefined : input + } + + private createAlias(): string { return `op_${this.aliasCounter++}` } diff --git a/packages/binding/src/core/QueryGenerator.ts b/packages/binding/src/core/QueryGenerator.ts index 93bbd2b0cf..92be60d8b4 100644 --- a/packages/binding/src/core/QueryGenerator.ts +++ b/packages/binding/src/core/QueryGenerator.ts @@ -1,4 +1,3 @@ -import { CrudQueryBuilder } from '@contember/client' import { PRIMARY_KEY_NAME } from '../bindingTypes' import { EntityFieldMarkers, @@ -10,164 +9,111 @@ import { MarkerTreeRoot, } from '../markers' import { assertNever, ucfirst } from '../utils' +import { ContentEntitySelection, ContentQuery, ContentQueryBuilder, replaceGraphQlLiteral } from '@contember/client' +import { Filter } from '../treeParameters' -type BaseQueryBuilder = Omit - -type ReadBuilder = CrudQueryBuilder.ReadBuilder.Builder export class QueryGenerator { - constructor(private tree: MarkerTreeRoot) {} - - public getReadQuery(): string | undefined { - try { - let baseQueryBuilder: BaseQueryBuilder = new CrudQueryBuilder.CrudQueryBuilder() - - for (const [, subTreeMarker] of this.tree.subTrees) { - baseQueryBuilder = this.addSubQuery(subTreeMarker, baseQueryBuilder) + constructor( + private tree: MarkerTreeRoot, + private qb: ContentQueryBuilder, + ) {} + + public getReadQuery(): Record> { + const result: Record> = {} + for (const [, subTreeMarker] of this.tree.subTrees) { + if (subTreeMarker.parameters.isCreating) { + continue + } + if (subTreeMarker instanceof EntitySubTreeMarker) { + result[subTreeMarker.placeholderName] = this.addGetQuery(subTreeMarker) + } else if (subTreeMarker instanceof EntityListSubTreeMarker) { + result[subTreeMarker.placeholderName] = this.createListQuery(subTreeMarker) } - return baseQueryBuilder.getGql() - } catch (e) { - return undefined } + return result } - private addSubQuery( - subTree: EntitySubTreeMarker | EntityListSubTreeMarker, - baseQueryBuilder: BaseQueryBuilder, - ): BaseQueryBuilder { + private addGetQuery(subTree: EntitySubTreeMarker): ContentQuery { if (subTree.parameters.isCreating) { - // Unconstrained trees, by definition, don't have any queries. - return baseQueryBuilder + throw new Error() } - if (subTree instanceof EntityListSubTreeMarker) { - return this.addListQuery(baseQueryBuilder, subTree) - } else if (subTree instanceof EntitySubTreeMarker) { - return this.addGetQuery(baseQueryBuilder, subTree) - } - - assertNever(subTree) - } - - private addGetQuery(baseQueryBuilder: BaseQueryBuilder, subTree: EntitySubTreeMarker): BaseQueryBuilder { - if (subTree.parameters.isCreating) { - return baseQueryBuilder - } - const populatedListQueryBuilder = QueryGenerator.registerQueryPart( - subTree.fields.markers, - CrudQueryBuilder.ReadBuilder.instantiate().by(subTree.parameters.where), - ) - return baseQueryBuilder.get( + return this.qb.get( subTree.entityName, - CrudQueryBuilder.ReadBuilder.instantiate( - populatedListQueryBuilder ? populatedListQueryBuilder.objectBuilder : undefined, - ), - subTree.placeholderName, + { + by: replaceGraphQlLiteral(subTree.parameters.where), + }, + it => QueryGenerator.registerQueryPart(subTree.fields.markers, it), ) } - private addListQuery(baseQueryBuilder: BaseQueryBuilder, subTree: EntityListSubTreeMarker): BaseQueryBuilder { + private createListQuery(subTree: EntityListSubTreeMarker): ContentQuery { if (subTree.parameters.isCreating) { - return baseQueryBuilder + throw new Error() } - - let finalBuilder: ReadBuilder - - if (subTree.parameters) { - const parameters = subTree.parameters - const withFilter: CrudQueryBuilder.ReadBuilder.Builder> = - parameters.filter - ? CrudQueryBuilder.ReadBuilder.instantiate().filter(parameters.filter) - : CrudQueryBuilder.ReadBuilder.instantiate() - - const withOrderBy: CrudQueryBuilder.ReadBuilder.Builder< - Exclude - > = parameters.orderBy ? withFilter.orderBy(parameters.orderBy) : withFilter - - const withOffset: CrudQueryBuilder.ReadBuilder.Builder< - Exclude - > = parameters.offset === undefined ? withOrderBy : withOrderBy.offset(parameters.offset) - - finalBuilder = parameters.limit === undefined ? withOffset : withOffset.limit(parameters.limit) - } else { - finalBuilder = CrudQueryBuilder.ReadBuilder.instantiate() - } - - // const fullyPopulated = withAllParams - // .anyRelation('pageInfo', builder => builder.column('totalCount')) - // .anyRelation('edges', builder => - // builder.anyRelation('node', builder => QueryGenerator.registerQueryPart(subTree.fields.markers, builder)), - // ) - - return baseQueryBuilder.list( + return this.qb.list( subTree.entityName, - QueryGenerator.registerQueryPart(subTree.fields.markers, finalBuilder), - subTree.placeholderName, + { + filter: resolveFilter(subTree.parameters.filter), + orderBy: replaceGraphQlLiteral(subTree.parameters.orderBy), + offset: subTree.parameters.offset, + limit: subTree.parameters.limit, + }, + it => QueryGenerator.registerQueryPart(subTree.fields.markers, it), ) } - public static registerQueryPart(fields: EntityFieldMarkers, builder: ReadBuilder): ReadBuilder { - builder = builder.column(PRIMARY_KEY_NAME) + public static registerQueryPart(fields: EntityFieldMarkers, selection: ContentEntitySelection): ContentEntitySelection { + selection = selection.$(PRIMARY_KEY_NAME).$('__typename') for (const [, fieldValue] of fields) { if (fieldValue instanceof FieldMarker) { if (fieldValue.fieldName !== PRIMARY_KEY_NAME) { - builder = builder.column(fieldValue.fieldName) + selection = selection.$(fieldValue.fieldName) } } else if (fieldValue instanceof HasOneRelationMarker) { const relation = fieldValue.parameters - const builderWithBody = CrudQueryBuilder.ReadBuilder.instantiate( - this.registerQueryPart(fieldValue.fields.markers, CrudQueryBuilder.ReadBuilder.instantiate()).objectBuilder, - ) - - const filteredBuilder: CrudQueryBuilder.ReadBuilder.Builder> = - relation.filter ? builderWithBody.filter(relation.filter) : builderWithBody if (relation.reducedBy) { // Assuming there's exactly one reducer field as enforced by MarkerTreeGenerator const relationField = `${relation.field}By${ucfirst(Object.keys(relation.reducedBy)[0])}` - builder = builder.reductionRelation( + selection = selection.$( relationField, - filteredBuilder.by(relation.reducedBy), - fieldValue.placeholderName, + { by: replaceGraphQlLiteral(relation.reducedBy), as: fieldValue.placeholderName }, + it => QueryGenerator.registerQueryPart(fieldValue.fields.markers, it), ) } else { - builder = builder.hasOneRelation( + selection = selection.$( relation.field, - filteredBuilder, - // TODO this will currently always go to the latter condition, resulting in less than ideal queries. - fieldValue.placeholderName === relation.field ? undefined : fieldValue.placeholderName, + { filter: resolveFilter(relation.filter), as: fieldValue.placeholderName }, + it => QueryGenerator.registerQueryPart(fieldValue.fields.markers, it), ) } } else if (fieldValue instanceof HasManyRelationMarker) { const relation = fieldValue.parameters - const builderWithBody = CrudQueryBuilder.ReadBuilder.instantiate( - this.registerQueryPart(fieldValue.fields.markers, CrudQueryBuilder.ReadBuilder.instantiate()).objectBuilder, - ) - - const withFilter: CrudQueryBuilder.ReadBuilder.Builder> = - relation.filter ? builderWithBody.filter(relation.filter) : builderWithBody - - const withOrderBy: CrudQueryBuilder.ReadBuilder.Builder< - Exclude - > = relation.orderBy ? withFilter.orderBy(relation.orderBy) : withFilter - const withOffset: CrudQueryBuilder.ReadBuilder.Builder< - Exclude - > = relation.offset === undefined ? withOrderBy : withOrderBy.offset(relation.offset) - const withLimit = relation.limit === undefined ? withOffset : withOffset.limit(relation.limit) - - builder = builder.anyRelation( + selection = selection.$( relation.field, - withLimit, - fieldValue.placeholderName === relation.field ? undefined : fieldValue.placeholderName, + { + as: fieldValue.placeholderName, + filter: resolveFilter(relation.filter), + orderBy: replaceGraphQlLiteral(relation.orderBy), + offset: relation.offset, + limit: relation.limit, + }, + it => QueryGenerator.registerQueryPart(fieldValue.fields.markers, it), ) } else { assertNever(fieldValue) } } - return builder + return selection } } + +const resolveFilter = (input?: Filter): Filter => { + return replaceGraphQlLiteral(input) as Filter +} diff --git a/packages/binding/src/core/index.ts b/packages/binding/src/core/index.ts index 275936d24b..2913650c60 100644 --- a/packages/binding/src/core/index.ts +++ b/packages/binding/src/core/index.ts @@ -9,3 +9,4 @@ export * from './TreeAugmenter' export * from './TreeStore' export * from './TreeParameterMerger' export * from './VariableInputTransformer' +export * from './utils/createQueryBuilder' diff --git a/packages/binding/src/core/schema/SchemaLoader.ts b/packages/binding/src/core/schema/SchemaLoader.ts index 2710ad5f77..ba07d51176 100644 --- a/packages/binding/src/core/schema/SchemaLoader.ts +++ b/packages/binding/src/core/schema/SchemaLoader.ts @@ -55,8 +55,8 @@ export class SchemaLoader { return existing } const schemaPromise = (async () => { - const raw: { data: { schema: RawSchema } } = await client.sendRequest(this.schemaQuery, options) - return new Schema(SchemaPreprocessor.processRawSchema(raw.data.schema)) + const raw = await client.execute<{ schema: RawSchema }>(this.schemaQuery, options) + return new Schema(SchemaPreprocessor.processRawSchema(raw.schema)) })() this.schemaLoadCache.set(client.apiUrl, schemaPromise) return await schemaPromise diff --git a/packages/binding/src/core/utils/createQueryBuilder.ts b/packages/binding/src/core/utils/createQueryBuilder.ts new file mode 100644 index 0000000000..55b8184453 --- /dev/null +++ b/packages/binding/src/core/utils/createQueryBuilder.ts @@ -0,0 +1,31 @@ +import { Schema, SchemaRelation } from '../schema' +import { ContentQueryBuilder, SchemaEntityNames, SchemaNames } from '@contember/client' + +export const createQueryBuilder = (schema: Schema) => { + const schemaNames: SchemaNames = { + entities: Object.fromEntries(schema.getEntityNames().map((it): [string, SchemaEntityNames] => { + const entity = schema.getEntity(it) + return [it, { + name: it, + scalars: Array.from(entity.fields.values()).filter(it => it.__typename === '_Column').map(it => it.name), + fields: Object.fromEntries(Array.from(entity.fields.values()).map(it => { + if (it.__typename === '_Column') { + return [it.name, { type: 'column' }] + } + if (it.__typename === '_Relation') { + return [ + it.name, + { + type: it.type === 'ManyHasMany' || it.type === 'OneHasMany' ? 'many' : 'one', + entity: (it as SchemaRelation).targetEntity, + }, + ] + } + throw new Error() + })), + }] + })), + } + + return new ContentQueryBuilder(schemaNames) +} diff --git a/packages/react-binding/src/accessorTree/AccessorTreeState.ts b/packages/react-binding/src/accessorTree/AccessorTreeState.ts index 133b4c3410..e92aeb6f1f 100644 --- a/packages/react-binding/src/accessorTree/AccessorTreeState.ts +++ b/packages/react-binding/src/accessorTree/AccessorTreeState.ts @@ -1,8 +1,7 @@ import type { TreeRootAccessor } from '@contember/binding' -import type { RequestError } from '@contember/binding' -import { DataBinding } from '@contember/binding' -import { Environment } from '@contember/binding' +import { DataBinding, Environment } from '@contember/binding' import { ReactNode } from 'react' +import { GraphQlClientError } from '@contember/react-client' export interface InitializingAccessorTreeState { name: 'initializing' @@ -21,7 +20,7 @@ export interface ErrorAccessorTreeState { name: 'error' environment: Environment binding: DataBinding - error: RequestError + error: GraphQlClientError } export type AccessorTreeState = diff --git a/packages/react-binding/src/accessorTree/AccessorTreeStateAction.ts b/packages/react-binding/src/accessorTree/AccessorTreeStateAction.ts index c37f520dc0..304836238b 100644 --- a/packages/react-binding/src/accessorTree/AccessorTreeStateAction.ts +++ b/packages/react-binding/src/accessorTree/AccessorTreeStateAction.ts @@ -1,8 +1,7 @@ import type { TreeRootAccessor } from '@contember/binding' -import type { RequestError } from '@contember/binding' -import { DataBinding } from '@contember/binding' -import { Environment } from '@contember/binding' +import { DataBinding, Environment } from '@contember/binding' import { ReactNode } from 'react' +import { GraphQlClientError } from '@contember/react-client' export type AccessorTreeStateAction = | { @@ -12,7 +11,7 @@ export type AccessorTreeStateAction = } | { type: 'failWithError' - error: RequestError + error: GraphQlClientError binding: DataBinding } | { diff --git a/packages/react-binding/src/accessorTree/useDataBinding.ts b/packages/react-binding/src/accessorTree/useDataBinding.ts index bc40bce34a..7771c406ee 100644 --- a/packages/react-binding/src/accessorTree/useDataBinding.ts +++ b/packages/react-binding/src/accessorTree/useDataBinding.ts @@ -1,5 +1,5 @@ import { - GraphQlClientFailedRequestMetadata, + GraphQlClientError, useCurrentContentGraphQlClient, useCurrentSystemGraphQlClient, useTenantGraphQlClient, @@ -7,15 +7,10 @@ import { import { ReactNode, useCallback, useEffect, useReducer, useRef, useState } from 'react' import { useEnvironment } from '../accessorPropagation' import type { TreeRootAccessor } from '@contember/binding' -import { DataBinding } from '@contember/binding' +import { DataBinding, Environment, Schema, SchemaLoader, TreeStore } from '@contember/binding' import type { AccessorTreeState } from './AccessorTreeState' import type { AccessorTreeStateOptions } from './AccessorTreeStateOptions' import { accessorTreeStateReducer } from './accessorTreeStateReducer' -import { metadataToRequestError } from '@contember/binding' -import type { RequestError } from '@contember/binding' -import { Environment } from '@contember/binding' -import { Schema, SchemaLoader } from '@contember/binding' -import { TreeStore } from '@contember/binding' import { useIsMounted } from '@contember/react-utils' import { MarkerTreeGenerator } from '../MarkerTreeGenerator' import ReactDOM from 'react-dom' @@ -40,7 +35,7 @@ export const useDataBinding = ({ } } - const onError = (error: RequestError, binding: DataBinding) => { + const onError = (error: GraphQlClientError, binding: DataBinding) => { if (isMountedRef.current) { dispatch({ type: 'failWithError', error, binding }) } @@ -91,17 +86,18 @@ export const useDataBinding = ({ try { setSchema(await SchemaLoader.loadSchema(contentClient)) - } catch (metadata) { - if (typeof metadata === 'object' && metadata !== null && (metadata as { name?: unknown }).name === 'AbortError') { - return - } - - if (isMountedRef.current) { + } catch (e) { + if (e instanceof GraphQlClientError) { + if (e.type === 'aborted') { + return + } dispatch({ type: 'failWithError', - error: metadataToRequestError(metadata as GraphQlClientFailedRequestMetadata), + error: e, binding: state.binding!, }) + } else { + throw e } } })() From f6a91e16665b48bcb1e0c230097834ac0c17db7a Mon Sep 17 00:00:00 2001 From: David Matejka Date: Thu, 23 Nov 2023 15:48:32 +0100 Subject: [PATCH 07/17] refactor(react-datagrid): use new content client --- .../src/internal/useDataGridTotalCount.ts | 47 ++++++++++--------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/packages/react-datagrid/src/internal/useDataGridTotalCount.ts b/packages/react-datagrid/src/internal/useDataGridTotalCount.ts index 6cc2747d49..4abb4d4dd7 100644 --- a/packages/react-datagrid/src/internal/useDataGridTotalCount.ts +++ b/packages/react-datagrid/src/internal/useDataGridTotalCount.ts @@ -1,43 +1,44 @@ -import type { EntityName, Filter } from '@contember/react-binding' -import { GraphQlBuilder } from '@contember/client' -import { useContentApiRequest } from '@contember/react-client' -import { useEffect } from 'react' +import { createQueryBuilder, EntityName, Filter, useEnvironment } from '@contember/react-binding' +import { ContentClient, GraphQlClientError, replaceGraphQlLiteral } from '@contember/client' +import { useCurrentContentGraphQlClient } from '@contember/react-client' +import { useEffect, useState } from 'react' import { useAbortController } from '@contember/react-utils' export const useDataGridTotalCount = (entityName: EntityName, filter: Filter | undefined): number | undefined => { - const [queryState, sendQuery] = useContentApiRequest<{ - data: { paginate: { pageInfo: { totalCount: number } } } - }>() + const client = useCurrentContentGraphQlClient() + const env = useEnvironment() const abortController = useAbortController() - const query = new GraphQlBuilder.QueryBuilder().query(builder => - builder.object('paginate', builder => { - builder = builder.name(`paginate${entityName}`).object('pageInfo', builder => builder.field('totalCount')) - if (filter) { - builder = builder.argument('filter', filter) - } - return builder - }), - ) + const [count, setCount] = useState(undefined) + const schema = env.getSchema() useEffect(() => { (async () => { + const contentClient = new ContentClient(client.execute.bind(client)) + const qb = createQueryBuilder(schema) + const query = qb.count(entityName, { + filter: resolveFilter(filter), + }) try { - await sendQuery(query, undefined, { + const result = await contentClient.query(query, { signal: abortController(), }) + setCount(result) } catch (e) { - if (e instanceof Error && e.name === 'AbortError') { + setCount(undefined) + if (e instanceof GraphQlClientError && e.type === 'aborted') { return } throw e } + })() - }, [abortController, query, sendQuery]) + }, [abortController, client, entityName, filter, schema]) + + return count +} - if (queryState.readyState !== 'networkSuccess') { - return undefined - } - return queryState.data.data.paginate.pageInfo.totalCount ?? undefined +const resolveFilter = (input?: Filter): Filter => { + return replaceGraphQlLiteral(input) as Filter } From a3c1188f273f6278e145824a5c233f4e91bfac16 Mon Sep 17 00:00:00 2001 From: David Matejka Date: Thu, 23 Nov 2023 15:49:10 +0100 Subject: [PATCH 08/17] refactor(client): use new client in s3 uploader --- .../GenerateUploadUrlMutationBuilder.ts | 82 +++++++++++-------- .../src/content/upload/S3FileUploader.ts | 3 +- 2 files changed, 51 insertions(+), 34 deletions(-) diff --git a/packages/client/src/content/upload/GenerateUploadUrlMutationBuilder.ts b/packages/client/src/content/upload/GenerateUploadUrlMutationBuilder.ts index 6fc08d2327..158e2707b1 100644 --- a/packages/client/src/content/upload/GenerateUploadUrlMutationBuilder.ts +++ b/packages/client/src/content/upload/GenerateUploadUrlMutationBuilder.ts @@ -1,45 +1,63 @@ -import { GraphQlLiteral, ObjectBuilder, QueryBuilder } from '../../graphQlBuilder' +import { GraphQlLiteral } from '../../graphQlBuilder' +import { GraphQlField, GraphQlPrintResult, GraphQlQueryPrinter, GraphQlSelectionSet, GraphQlSelectionSetItem } from '../../builder' +import { replaceGraphQlLiteral } from '../client' class GenerateUploadUrlMutationBuilder { - private static generateUploadUrlFields = new ObjectBuilder() - .name('generateUploadUrl') - .field('url') - .field('publicUrl') - .field('method') - .object('headers', builder => builder.field('key').field('value')) - - public static buildQuery(parameters: GenerateUploadUrlMutationBuilder.MutationParameters): string { - return new QueryBuilder().mutation(builder => { - for (const alias in parameters) { - const fileParameters = parameters[alias] - if (fileParameters.suffix || fileParameters.fileName || fileParameters.extension) { - builder = builder.object( - alias, - GenerateUploadUrlMutationBuilder.generateUploadUrlFields - .argument('input', fileParameters), - ) - } else { - // BC - builder = builder.object( - alias, - GenerateUploadUrlMutationBuilder.generateUploadUrlFields - .argument('contentType', fileParameters.contentType) - .argument('expiration', fileParameters.expiration) - .argument('prefix', fileParameters.prefix) - .argument('acl', fileParameters.acl), - ) - } + private static generateUploadUrlFields = [ + new GraphQlField(null, 'url'), + new GraphQlField(null, 'publicUrl'), + new GraphQlField(null, 'method'), + new GraphQlField(null, 'headers', {}, [ + new GraphQlField(null, 'key'), + new GraphQlField(null, 'value'), + ]), + ] + /** + * @internal + */ + public static buildQuery(parameters: GenerateUploadUrlMutationBuilder.MutationParameters): GraphQlPrintResult { + const selectionItems: GraphQlSelectionSetItem[] = [] + for (const alias in parameters) { + const fileParameters = parameters[alias] + if (fileParameters.suffix || fileParameters.fileName || fileParameters.extension) { + const value = replaceGraphQlLiteral(fileParameters) + selectionItems.push(new GraphQlField(alias, 'generateUploadUrl', { + input: { + value: value, + graphQlType: 'S3GenerateSignedUploadInput', + }, + }, GenerateUploadUrlMutationBuilder.generateUploadUrlFields)) + } else { + selectionItems.push(new GraphQlField(alias, 'generateUploadUrl', { + contentType: { + graphQlType: 'String', + value: fileParameters.contentType, + }, + expiration: { + graphQlType: 'Int', + value: fileParameters.expiration, + }, + prefix: { + graphQlType: 'String', + value: fileParameters.prefix, + }, + acl: { + graphQlType: 'S3Acl', + value: fileParameters.acl?.value, + }, + }, GenerateUploadUrlMutationBuilder.generateUploadUrlFields)) } + } - return builder - }) + const printer = new GraphQlQueryPrinter() + return printer.printDocument('mutation', selectionItems, {}) } } namespace GenerateUploadUrlMutationBuilder { export type Acl = GraphQlLiteral<'PUBLIC_READ' | 'PRIVATE' | 'NONE'>; - export interface FileParameters { + export type FileParameters = { contentType: string expiration?: number size?: number diff --git a/packages/client/src/content/upload/S3FileUploader.ts b/packages/client/src/content/upload/S3FileUploader.ts index 54c709a1f7..5b740ea01f 100644 --- a/packages/client/src/content/upload/S3FileUploader.ts +++ b/packages/client/src/content/upload/S3FileUploader.ts @@ -63,8 +63,7 @@ class S3FileUploader implements FileUploader { const mutation = GenerateUploadUrlMutationBuilder.buildQuery(parameters) try { - const response = await options.contentApiClient.sendRequest<{ data: GenerateUploadUrlMutationBuilder.MutationResponse }>(mutation) - const responseData = response.data + const responseData = await options.contentApiClient.execute(mutation.query, { variables: mutation.variables }) const limit = pLimit(this.options.concurrency ?? 5) const promises: Promise[] = [] for (const [file] of files) { From ccbc0b83aca1c5256cc1131fe93b0d553baf83fb Mon Sep 17 00:00:00 2001 From: David Matejka Date: Thu, 23 Nov 2023 15:49:34 +0100 Subject: [PATCH 09/17] refactor(client): remove legacy graphql query builder --- .../src/crudQueryBuilder/CrudQueryBuilder.ts | 168 --------------- .../crudQueryBuilder/CrudQueryBuilderError.ts | 1 - .../crudQueryBuilder/ErrorsRelationBuilder.ts | 16 -- .../src/crudQueryBuilder/ReadBuilder.ts | 106 ---------- .../ValidationRelationBuilder.ts | 17 -- .../src/crudQueryBuilder/WriteBuilder.ts | 79 ------- .../src/crudQueryBuilder/WriteDataBuilder.ts | 153 -------------- .../WriteManyRelationBuilder.ts | 141 ------------- .../WriteOneRelationBuilder.ts | 101 --------- packages/client/src/crudQueryBuilder/index.ts | 6 - .../src/graphQlBuilder/GraphQlBuilderError.ts | 1 - .../src/graphQlBuilder/ObjectBuilder.ts | 87 -------- .../client/src/graphQlBuilder/QueryBuilder.ts | 25 +-- .../src/graphQlBuilder/QueryCompiler.ts | 115 ----------- .../src/graphQlBuilder/RootObjectBuilder.ts | 36 ---- packages/client/src/graphQlBuilder/index.ts | 3 - packages/client/src/index.ts | 11 - .../tests/cases/unit/crudQueryBuilder.spec.ts | 195 ------------------ .../tests/cases/unit/graphQlBuilder.spec.ts | 58 ------ 19 files changed, 2 insertions(+), 1317 deletions(-) delete mode 100644 packages/client/src/crudQueryBuilder/CrudQueryBuilder.ts delete mode 100644 packages/client/src/crudQueryBuilder/CrudQueryBuilderError.ts delete mode 100644 packages/client/src/crudQueryBuilder/ErrorsRelationBuilder.ts delete mode 100644 packages/client/src/crudQueryBuilder/ReadBuilder.ts delete mode 100644 packages/client/src/crudQueryBuilder/ValidationRelationBuilder.ts delete mode 100644 packages/client/src/crudQueryBuilder/WriteBuilder.ts delete mode 100644 packages/client/src/crudQueryBuilder/WriteDataBuilder.ts delete mode 100644 packages/client/src/crudQueryBuilder/WriteManyRelationBuilder.ts delete mode 100644 packages/client/src/crudQueryBuilder/WriteOneRelationBuilder.ts delete mode 100644 packages/client/src/graphQlBuilder/GraphQlBuilderError.ts delete mode 100644 packages/client/src/graphQlBuilder/ObjectBuilder.ts delete mode 100644 packages/client/src/graphQlBuilder/QueryCompiler.ts delete mode 100644 packages/client/src/graphQlBuilder/RootObjectBuilder.ts delete mode 100644 packages/client/tests/cases/unit/crudQueryBuilder.spec.ts delete mode 100644 packages/client/tests/cases/unit/graphQlBuilder.spec.ts diff --git a/packages/client/src/crudQueryBuilder/CrudQueryBuilder.ts b/packages/client/src/crudQueryBuilder/CrudQueryBuilder.ts deleted file mode 100644 index 2a861a034d..0000000000 --- a/packages/client/src/crudQueryBuilder/CrudQueryBuilder.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { ObjectBuilder, QueryBuilder, RootObjectBuilder } from '../graphQlBuilder' -import { isEmptyObject } from '../utils' -import { CrudQueryBuilderError } from './CrudQueryBuilderError' -import { ReadBuilder } from './ReadBuilder' -import type { - CreateMutationArguments, - CreateMutationFields, - DeleteMutationArguments, - DeleteMutationFields, - GetQueryArguments, - ListQueryArguments, - Mutations, - PaginateQueryArguments, - Queries, - UpdateMutationArguments, - UpdateMutationFields, - WriteOperation, -} from './types' -import { WriteBuilder } from './WriteBuilder' - -export class CrudQueryBuilder { - constructor( - private type: undefined | 'query' | 'mutation' = undefined, - private rootObjectBuilder: RootObjectBuilder = new RootObjectBuilder(), - ) {} - - public fragment(name: string, typeName: string, query: ReadBuilder.BuilderFactory): CrudQueryBuilder { - const readBuilder = ReadBuilder.instantiateFromFactory(query) - const objectBuilder = readBuilder.objectBuilder.name(typeName) - - return new CrudQueryBuilder(this.type, this.rootObjectBuilder.fragment(name, objectBuilder)) - } - - public list( - name: string, - query: ReadBuilder.BuilderFactory, - alias?: string, - ): Omit { - if (this.type === 'mutation') { - throw new CrudQueryBuilderError('Cannot combine queries and mutations') - } - name = `list${name}` - query = ReadBuilder.instantiateFromFactory(query) - - const [objectName, objectBuilder] = - typeof alias === 'string' ? [alias, query.objectBuilder.name(name)] : [name, query.objectBuilder] - - return new CrudQueryBuilder('query', this.rootObjectBuilder.object(objectName, objectBuilder)) - } - - public paginate( - name: string, - query: ReadBuilder.BuilderFactory, - alias?: string, - ): Omit { - if (this.type === 'mutation') { - throw new CrudQueryBuilderError('Cannot combine queries and mutations') - } - name = `paginate${name}` - query = ReadBuilder.instantiateFromFactory(query) - - const [objectName, objectBuilder] = - typeof alias === 'string' ? [alias, query.objectBuilder.name(name)] : [name, query.objectBuilder] - - return new CrudQueryBuilder('query', this.rootObjectBuilder.object(objectName, objectBuilder)) - } - - public get( - name: string, - query: ReadBuilder.BuilderFactory, - alias?: string, - ): Omit { - if (this.type === 'mutation') { - throw new CrudQueryBuilderError('Cannot combine queries and mutations') - } - name = `get${name}` - query = ReadBuilder.instantiateFromFactory(query) - - const [objectName, objectBuilder] = - typeof alias === 'string' ? [alias, query.objectBuilder.name(name)] : [name, query.objectBuilder] - - return new CrudQueryBuilder('query', this.rootObjectBuilder.object(objectName, objectBuilder)) - } - - public update( - name: string, - query: WriteBuilder.BuilderFactory, - alias?: string, - ): Omit { - if (this.type === 'query') { - throw new CrudQueryBuilderError('Cannot combine queries and mutations') - } - name = `update${name}` - query = WriteBuilder.instantiateFromFactory(query) - - if (isEmptyObject(query.objectBuilder.args.data)) { - return this - } - - const [objectName, objectBuilder] = - typeof alias === 'string' ? [alias, query.objectBuilder.name(name)] : [name, query.objectBuilder] - - return new CrudQueryBuilder('mutation', this.rootObjectBuilder.object(objectName, objectBuilder)) - } - - public create( - name: string, - query: WriteBuilder.BuilderFactory, - alias?: string, - ): Omit { - if (this.type === 'query') { - throw new CrudQueryBuilderError('Cannot combine queries and mutations') - } - name = `create${name}` - query = WriteBuilder.instantiateFromFactory(query) - - if (isEmptyObject(query.objectBuilder.args.data)) { - return this - } - - const [objectName, objectBuilder] = - typeof alias === 'string' ? [alias, query.objectBuilder.name(name)] : [name, query.objectBuilder] - - return new CrudQueryBuilder('mutation', this.rootObjectBuilder.object(objectName, objectBuilder)) - } - - public delete( - name: string, - query: WriteBuilder.BuilderFactory, - alias?: string, - ): Omit { - if (this.type === 'query') { - throw new CrudQueryBuilderError('Cannot combine queries and mutations') - } - name = `delete${name}` - query = WriteBuilder.instantiateFromFactory(query) - - const [objectName, objectBuilder] = - typeof alias === 'string' ? [alias, query.objectBuilder.name(name)] : [name, query.objectBuilder] - - return new CrudQueryBuilder('mutation', this.rootObjectBuilder.object(objectName, objectBuilder)) - } - - public inTransaction(alias: string = 'transaction', options: { deferForeignKeyConstraints?: boolean } = {}): CrudQueryBuilder { - const objectBuilder = new ObjectBuilder( - ['ok', 'errorMessage'], - { ...this.rootObjectBuilder.objects }, - { options }, - undefined, - undefined, - 'transaction', - ) - - return new CrudQueryBuilder(this.type, new RootObjectBuilder({ [alias]: objectBuilder }, this.rootObjectBuilder.fragmentDefinitions)) - } - - getGql(): string { - const builder = new QueryBuilder() - switch (this.type) { - case 'mutation': - return builder.mutation(this.rootObjectBuilder) - case 'query': - return builder.query(this.rootObjectBuilder) - default: - throw new CrudQueryBuilderError(`Invalid type ${this.type}`) - } - } -} diff --git a/packages/client/src/crudQueryBuilder/CrudQueryBuilderError.ts b/packages/client/src/crudQueryBuilder/CrudQueryBuilderError.ts deleted file mode 100644 index fafe312d1a..0000000000 --- a/packages/client/src/crudQueryBuilder/CrudQueryBuilderError.ts +++ /dev/null @@ -1 +0,0 @@ -export class CrudQueryBuilderError extends Error {} diff --git a/packages/client/src/crudQueryBuilder/ErrorsRelationBuilder.ts b/packages/client/src/crudQueryBuilder/ErrorsRelationBuilder.ts deleted file mode 100644 index 43d4409f05..0000000000 --- a/packages/client/src/crudQueryBuilder/ErrorsRelationBuilder.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { ObjectBuilder } from '../graphQlBuilder' - -export class ErrorsRelationBuilder { - public static errorsRelation(objectBuilder: ObjectBuilder): ObjectBuilder { - return objectBuilder.object('errors', builder => - builder - .field('type') - .field('message') - .object('path', builder => - builder - .inlineFragment('_FieldPathFragment', builder => builder.field('field')) - .inlineFragment('_IndexPathFragment', builder => builder.field('index').field('alias')), - ), - ) - } -} diff --git a/packages/client/src/crudQueryBuilder/ReadBuilder.ts b/packages/client/src/crudQueryBuilder/ReadBuilder.ts deleted file mode 100644 index cf46be8015..0000000000 --- a/packages/client/src/crudQueryBuilder/ReadBuilder.ts +++ /dev/null @@ -1,106 +0,0 @@ -import type { Input } from '@contember/schema' -import { GraphQlLiteral, ObjectBuilder } from '../graphQlBuilder' -import type { HasManyArguments, HasOneArguments, OrderDirection, ReadArguments, ReductionArguments } from './types' - -class ReadBuilder { - protected constructor(public readonly objectBuilder: ObjectBuilder = new ObjectBuilder()) {} - - public static instantiate( - objectBuilder: ObjectBuilder = new ObjectBuilder(), - ): ReadBuilder.Builder { - return new ReadBuilder(objectBuilder) - } - - public static instantiateFromFactory( - builder: ReadBuilder.BuilderFactory, - ): ReadBuilder.Builder { - if (typeof builder === 'function') { - return builder(ReadBuilder.instantiate()) - } - return builder - } - - protected instantiate( - objectBuilder: ObjectBuilder = new ObjectBuilder(), - ): ReadBuilder.Builder { - return ReadBuilder.instantiate(objectBuilder) - } - - public by(by: Input.UniqueWhere) { - return this.instantiate>(this.objectBuilder.argument('by', by)) - } - - public filter(where: Input.Where>>) { - return this.instantiate>(this.objectBuilder.argument('filter', where)) - } - - public orderBy(orderBy: Input.OrderBy[]) { - return this.instantiate>(this.objectBuilder.argument('orderBy', orderBy)) - } - - public offset(offset: number) { - return this.instantiate>(this.objectBuilder.argument('offset', offset)) - } - - public limit(limit: number) { - return this.instantiate>(this.objectBuilder.argument('limit', limit)) - } - - public skip(offset: number) { - return this.instantiate>(this.objectBuilder.argument('skip', offset)) - } - - public first(limit: number) { - return this.instantiate>(this.objectBuilder.argument('first', limit)) - } - - public column(name: string) { - return this.instantiate(this.objectBuilder.field(name)) - } - - public inlineFragment(typeName: string, builder: ReadBuilder.BuilderFactory) { - builder = ReadBuilder.instantiateFromFactory(builder) - return this.instantiate(this.objectBuilder.inlineFragment(typeName, builder.objectBuilder)) - } - - public applyFragment(fragmentName: string) { - return this.instantiate(this.objectBuilder.applyFragment(fragmentName)) - } - - public reductionRelation(name: string, builder: ReadBuilder.BuilderFactory, alias?: string) { - return this.relation(name, builder, alias) - } - - public hasOneRelation(name: string, builder: ReadBuilder.BuilderFactory, alias?: string) { - return this.relation(name, builder, alias) - } - - public hasManyRelation(name: string, builder: ReadBuilder.BuilderFactory, alias?: string) { - return this.relation(name, builder, alias) - } - - public anyRelation(name: string, builder: ReadBuilder.BuilderFactory, alias?: string) { - return this.relation(name, builder, alias) - } - - protected relation(name: string, builder: ReadBuilder.BuilderFactory, alias?: string) { - builder = ReadBuilder.instantiateFromFactory(builder) - - const [objectName, objectBuilder] = - typeof alias === 'string' ? [alias, builder.objectBuilder.name(name)] : [name, builder.objectBuilder] - - return this.instantiate(this.objectBuilder.object(objectName, objectBuilder)) - } -} - -namespace ReadBuilder { - export type Builder = Omit< - ReadBuilder, - Exclude - > - export type BuilderFactory = - | Builder - | ((builder: Builder) => Builder) -} - -export { ReadBuilder } diff --git a/packages/client/src/crudQueryBuilder/ValidationRelationBuilder.ts b/packages/client/src/crudQueryBuilder/ValidationRelationBuilder.ts deleted file mode 100644 index fecc0c9e95..0000000000 --- a/packages/client/src/crudQueryBuilder/ValidationRelationBuilder.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { ObjectBuilder } from '../graphQlBuilder' - -export class ValidationRelationBuilder { - public static validationRelation(objectBuilder: ObjectBuilder): ObjectBuilder { - return objectBuilder.object('validation', builder => - builder.field('valid').object('errors', builder => - builder - .object('path', builder => - builder - .inlineFragment('_FieldPathFragment', builder => builder.field('field')) - .inlineFragment('_IndexPathFragment', builder => builder.field('index').field('alias')), - ) - .object('message', builder => builder.field('text')), - ), - ) - } -} diff --git a/packages/client/src/crudQueryBuilder/WriteBuilder.ts b/packages/client/src/crudQueryBuilder/WriteBuilder.ts deleted file mode 100644 index 1c66dc944b..0000000000 --- a/packages/client/src/crudQueryBuilder/WriteBuilder.ts +++ /dev/null @@ -1,79 +0,0 @@ -import type { Input } from '@contember/schema' -import { GraphQlLiteral, ObjectBuilder } from '../graphQlBuilder' -import { ErrorsRelationBuilder } from './ErrorsRelationBuilder' -import { ReadBuilder } from './ReadBuilder' -import type { WriteArguments, WriteFields, WriteOperation } from './types' -import { ValidationRelationBuilder } from './ValidationRelationBuilder' -import { WriteDataBuilder } from './WriteDataBuilder' - -class WriteBuilder { - protected constructor(public readonly objectBuilder: ObjectBuilder = new ObjectBuilder()) {} - - public static instantiate( - objectBuilder: ObjectBuilder = new ObjectBuilder(), - ): WriteBuilder.Builder { - return new WriteBuilder(objectBuilder) - } - - public static instantiateFromFactory< - AA extends WriteArguments, - AF extends WriteFields, - Op extends WriteOperation.Operation, - >(builder: WriteBuilder.BuilderFactory): WriteBuilder.Builder { - if (typeof builder === 'function') { - return builder(WriteBuilder.instantiate()) - } - return builder - } - - public data(data: WriteDataBuilder.DataLike) { - const resolvedData = WriteDataBuilder.resolveData(data) - return WriteBuilder.instantiate, AF, Op>( - resolvedData === undefined ? this.objectBuilder : this.objectBuilder.argument('data', resolvedData), - ) - } - - public by(by: Input.UniqueWhere) { - return WriteBuilder.instantiate, AF, Op>(this.objectBuilder.argument('by', by)) - } - - public ok() { - return WriteBuilder.instantiate, Op>(this.objectBuilder.field('ok')) - } - - public errorMessage() { - return WriteBuilder.instantiate, Op>(this.objectBuilder.field('errorMessage')) - } - - public validation() { - return WriteBuilder.instantiate, Op>( - ValidationRelationBuilder.validationRelation(this.objectBuilder), - ) - } - - public errors() { - return WriteBuilder.instantiate, Op>( - ErrorsRelationBuilder.errorsRelation(this.objectBuilder), - ) - } - - public node(builder: ReadBuilder.BuilderFactory) { - const readBuilder = ReadBuilder.instantiateFromFactory(builder) - return WriteBuilder.instantiate, Op>( - this.objectBuilder.object('node', readBuilder.objectBuilder), - ) - } -} - -namespace WriteBuilder { - export type Builder = Omit< - Omit, Exclude>, - Exclude - > - - export type BuilderFactory = - | Builder - | ((builder: Builder) => Builder) -} - -export { WriteBuilder } diff --git a/packages/client/src/crudQueryBuilder/WriteDataBuilder.ts b/packages/client/src/crudQueryBuilder/WriteDataBuilder.ts deleted file mode 100644 index f1e8f2b166..0000000000 --- a/packages/client/src/crudQueryBuilder/WriteDataBuilder.ts +++ /dev/null @@ -1,153 +0,0 @@ -import type { Input, Value } from '@contember/schema' -import { GraphQlLiteral } from '../graphQlBuilder' -import { isEmptyObject } from '../utils' -import { CrudQueryBuilderError } from './CrudQueryBuilderError' -import type { WriteOperation } from './types' -import { WriteManyRelationBuilder } from './WriteManyRelationBuilder' -import { WriteOneRelationBuilder } from './WriteOneRelationBuilder' - -class WriteDataBuilder { - public readonly data: WriteDataBuilder.DataFormat[Op['op']] - - public constructor(data?: WriteDataBuilder.DataFormat[Op['op']]) { - this.data = data || {} - } - - public static resolveData( - dataLike: WriteDataBuilder.DataLike, - ): WriteDataBuilder.DataFormat[Op['op']] | undefined { - let resolvedData: WriteDataBuilder.DataFormat[Op['op']] - - if (dataLike instanceof WriteDataBuilder) { - resolvedData = dataLike.data - } else if (typeof dataLike === 'function') { - resolvedData = dataLike(new WriteDataBuilder()).data - } else { - resolvedData = dataLike - } - - if (isEmptyObject(resolvedData)) { - return undefined - } - return resolvedData - } - - public set(fieldName: string, value: Input.ColumnValue) { - return new WriteDataBuilder({ ...this.data, [fieldName]: value }) - } - - public many(fieldName: string, data: WriteManyRelationBuilder.BuilderFactory): WriteDataBuilder { - const resolvedData = WriteManyRelationBuilder.instantiateFromFactory(data).data - return resolvedData === undefined || resolvedData.length === 0 - ? this - : new WriteDataBuilder(this.mergeInFreshData(this.data, fieldName, resolvedData)) - } - - public one(fieldName: string, data: WriteOneRelationBuilder.BuilderFactory): WriteDataBuilder { - const resolvedData = WriteOneRelationBuilder.instantiateFromFactory(data).data - return resolvedData === undefined || isEmptyObject(resolvedData) - ? this - : new WriteDataBuilder(this.mergeInFreshData(this.data, fieldName, resolvedData)) - } - - private mergeInFreshData( - original: WriteDataBuilder.DataFormat[Op['op']], - fieldName: string, - fresh: WriteManyRelationBuilder.DataFormat[Op['op']] | WriteOneRelationBuilder.DataFormat[Op['op']], - ): WriteDataBuilder.DataFormat[Op['op']] { - if (fieldName in original) { - const existingValue = original[fieldName] - if (Array.isArray(existingValue)) { - if (Array.isArray(fresh)) { - return { ...original, [fieldName]: [...existingValue, ...fresh] } - } - throw new CrudQueryBuilderError(`Inconsistent data.`) - } - if (Array.isArray(fresh)) { - throw new CrudQueryBuilderError(`Inconsistent data.`) - } - return { ...original, [fieldName]: this.mergeUpdateOne(existingValue, fresh) } - } - return { ...original, [fieldName]: fresh } - } - - private mergeUpdateOne( - original: - | Value.FieldValue - | Input.CreateOneRelationInput - | Input.UpdateOneRelationInput, - fresh: - | Value.FieldValue - | Input.CreateOneRelationInput - | Input.UpdateOneRelationInput, - ): - | Value.FieldValue - | Input.CreateOneRelationInput - | Input.UpdateOneRelationInput { - // TODO This implementation pretty bad but it will have to do for now. - if (original instanceof GraphQlLiteral) { - if (fresh instanceof GraphQlLiteral) { - if (original.value === fresh.value) { - return original - } - } - throw new CrudQueryBuilderError(`Inconsistent data.`) - } - if (fresh instanceof GraphQlLiteral) { - throw new CrudQueryBuilderError(`Inconsistent data.`) - } - - if (Array.isArray(original)) { - if (Array.isArray(fresh)) { - return [...original, ...fresh] - } - throw new CrudQueryBuilderError(`Inconsistent data.`) - } - if (Array.isArray(fresh)) { - throw new CrudQueryBuilderError(`Inconsistent data.`) - } - - if (original === null) { - if (fresh === null) { - return original - } - } - if (fresh === null) { - throw new CrudQueryBuilderError(`Inconsistent data.`) - } - - if (typeof original === 'object') { - if (typeof fresh === 'object') { - const composite: any = { ...original } - for (const field in fresh) { - const fromFresh = (fresh as any)[field] - composite[field] = field in composite ? this.mergeUpdateOne(composite[field], fromFresh) : fromFresh - } - return composite - } - throw new CrudQueryBuilderError(`Inconsistent data.`) - } - if (typeof fresh === 'object') { - throw new CrudQueryBuilderError(`Inconsistent data.`) - } - - if (original === fresh) { - return original - } - throw new CrudQueryBuilderError(`Inconsistent data.`) - } -} - -namespace WriteDataBuilder { - export interface DataFormat { - create: Input.CreateDataInput - update: Input.UpdateDataInput - } - - export type DataLike = - | DataFormat[Op['op']] - | WriteDataBuilder - | ((builder: WriteDataBuilder) => WriteDataBuilder) -} - -export { WriteDataBuilder } diff --git a/packages/client/src/crudQueryBuilder/WriteManyRelationBuilder.ts b/packages/client/src/crudQueryBuilder/WriteManyRelationBuilder.ts deleted file mode 100644 index ae564c3058..0000000000 --- a/packages/client/src/crudQueryBuilder/WriteManyRelationBuilder.ts +++ /dev/null @@ -1,141 +0,0 @@ -import type { Input } from '@contember/schema' -import type { GraphQlLiteral } from '../graphQlBuilder' -import type { WriteOperation, WriteRelationOps } from './types' -import { WriteDataBuilder } from './WriteDataBuilder' - -class WriteManyRelationBuilder< - Op extends WriteOperation.ContentfulOperation, - Allowed extends WriteRelationOps[Op['op']], -> { - private constructor(public readonly data: WriteManyRelationBuilder.DataFormat[Op['op']] = []) {} - - public static instantiate< - Op extends WriteOperation.ContentfulOperation, - Allowed extends WriteRelationOps[Op['op']] = WriteRelationOps[Op['op']], - >(data: WriteManyRelationBuilder.DataFormat[Op['op']] = []): WriteManyRelationBuilder.Builder { - return new WriteManyRelationBuilder(data) - } - - public static instantiateFromFactory< - Op extends WriteOperation.ContentfulOperation, - Allowed extends WriteRelationOps[Op['op']], - >(builder: WriteManyRelationBuilder.BuilderFactory): WriteManyRelationBuilder.Builder { - if (typeof builder === 'function') { - return builder(WriteManyRelationBuilder.instantiate()) - } - if ('data' in builder) { - return WriteManyRelationBuilder.instantiate(builder.data) - } - return WriteManyRelationBuilder.instantiate(builder) - } - - public create( - data: WriteDataBuilder.DataLike, - alias?: string, - ): WriteManyRelationBuilder.Builder { - const resolvedData = WriteDataBuilder.resolveData(data) - return ( - resolvedData === undefined - ? this - : WriteManyRelationBuilder.instantiate([ - ...this.data, - this.withAlias({ create: resolvedData }, alias), - ] as WriteManyRelationBuilder.DataFormat[WriteOperation.Create['op']]) - ) as WriteManyRelationBuilder.Builder - } - - public connect(where: Input.UniqueWhere, alias?: string) { - return WriteManyRelationBuilder.instantiate([ - ...this.data, - this.withAlias({ connect: where }, alias), - ] as WriteManyRelationBuilder.DataFormat[Op['op']]) - } - - public delete(where: Input.UniqueWhere, alias?: string) { - return WriteManyRelationBuilder.instantiate([ - ...this.data, - this.withAlias({ delete: where }, alias), - ]) - } - - public disconnect(where: Input.UniqueWhere, alias?: string) { - return WriteManyRelationBuilder.instantiate([ - ...this.data, - this.withAlias({ disconnect: where }, alias), - ]) - } - - public update( - where: Input.UniqueWhere, - data: WriteDataBuilder.DataLike, - alias?: string, - ): WriteManyRelationBuilder.Builder { - const resolvedData = WriteDataBuilder.resolveData(data) - return ( - resolvedData === undefined - ? this - : WriteManyRelationBuilder.instantiate([ - ...this.data, - this.withAlias({ update: { by: where, data: resolvedData } }, alias), - ]) - ) as WriteManyRelationBuilder.Builder - } - - public upsert( - where: Input.UniqueWhere, - update: WriteDataBuilder.DataLike, - create: WriteDataBuilder.DataLike, - alias?: string, - ): WriteManyRelationBuilder.Builder { - const resolvedUpdate = WriteDataBuilder.resolveData(update) - const resolvedCreate = WriteDataBuilder.resolveData(create) - return ( - resolvedUpdate === undefined && resolvedCreate === undefined - ? this - : WriteManyRelationBuilder.instantiate([ - ...this.data, - this.withAlias( - { - upsert: { - by: where, - update: resolvedUpdate || {}, - create: resolvedCreate || {}, - }, - }, - alias, - ), - ]) - ) as WriteManyRelationBuilder.Builder - } - - private withAlias< - D extends Input.CreateOneRelationInput | Input.UpdateManyRelationInputItem, - >(data: D, alias?: string): D { - if (alias !== undefined) { - data.alias = alias - } - return data - } -} - -namespace WriteManyRelationBuilder { - export interface DataFormat { - create: Input.CreateManyRelationInput - update: Input.UpdateManyRelationInput - } - - export type Builder< - Op extends WriteOperation.ContentfulOperation, - Allowed extends WriteRelationOps[Op['op']] = WriteRelationOps[Op['op']], - > = Omit< - WriteManyRelationBuilder, - Exclude - > - - export type BuilderFactory< - Op extends WriteOperation.ContentfulOperation, - Allowed extends WriteRelationOps[Op['op']] = WriteRelationOps[Op['op']], - > = DataFormat[Op['op']] | Builder | ((builder: Builder) => Builder) -} - -export { WriteManyRelationBuilder } diff --git a/packages/client/src/crudQueryBuilder/WriteOneRelationBuilder.ts b/packages/client/src/crudQueryBuilder/WriteOneRelationBuilder.ts deleted file mode 100644 index 97e4f9f278..0000000000 --- a/packages/client/src/crudQueryBuilder/WriteOneRelationBuilder.ts +++ /dev/null @@ -1,101 +0,0 @@ -import type { Input } from '@contember/schema' -import type { GraphQlLiteral } from '../graphQlBuilder' -import type { WriteOperation, WriteRelationOps } from './types' -import { WriteDataBuilder } from './WriteDataBuilder' - -class WriteOneRelationBuilder< - Op extends WriteOperation.ContentfulOperation, - Allowed extends WriteRelationOps[Op['op']] = WriteRelationOps[Op['op']], - D extends WriteOneRelationBuilder.DataFormat[Op['op']] | undefined = WriteOneRelationBuilder.DataFormat[Op['op']], -> { - protected constructor(public readonly data: D = undefined as D) {} - - public static instantiate< - Op extends WriteOperation.ContentfulOperation, - Allowed extends WriteRelationOps[Op['op']] = WriteRelationOps[Op['op']], - D extends WriteOneRelationBuilder.DataFormat[Op['op']] | undefined = WriteOneRelationBuilder.DataFormat[Op['op']], - >(data: D = undefined as D): WriteOneRelationBuilder.Builder { - return new WriteOneRelationBuilder(data) - } - - public static instantiateFromFactory< - Op extends WriteOperation.ContentfulOperation, - Allowed extends WriteRelationOps[Op['op']] = WriteRelationOps[Op['op']], - D extends WriteOneRelationBuilder.DataFormat[Op['op']] | undefined = WriteOneRelationBuilder.DataFormat[Op['op']], - >(builder: WriteOneRelationBuilder.BuilderFactory): WriteOneRelationBuilder.Builder { - if (typeof builder === 'function') { - return builder(WriteOneRelationBuilder.instantiate()) - } - if (builder && 'data' in builder!) { - return WriteOneRelationBuilder.instantiate(builder.data) - } - return WriteOneRelationBuilder.instantiate(builder) - } - - public create(data: WriteDataBuilder.DataLike) { - const resolvedData = WriteDataBuilder.resolveData(data) - return resolvedData === undefined - ? this - : WriteOneRelationBuilder.instantiate({ - create: resolvedData, - }) - } - - public connect(where: Input.UniqueWhere) { - return WriteOneRelationBuilder.instantiate({ connect: where }) - } - - public delete() { - return WriteOneRelationBuilder.instantiate({ delete: true }) - } - - public disconnect() { - return WriteOneRelationBuilder.instantiate({ disconnect: true }) - } - - public update(data: WriteDataBuilder.DataLike) { - const resolvedData = WriteDataBuilder.resolveData(data) - return resolvedData === undefined ? this : new WriteOneRelationBuilder({ update: resolvedData }) - } - - public upsert( - update: WriteDataBuilder.DataLike, - create: WriteDataBuilder.DataLike, - ) { - const resolvedCreate = WriteDataBuilder.resolveData(create) - const resolvedUpdate = WriteDataBuilder.resolveData(update) - - return resolvedUpdate === undefined && resolvedCreate === undefined - ? this - : WriteOneRelationBuilder.instantiate({ - upsert: { - update: resolvedUpdate || {}, - create: resolvedCreate || {}, - }, - }) - } -} - -namespace WriteOneRelationBuilder { - export interface DataFormat { - create: Input.CreateOneRelationInput - update: Input.UpdateOneRelationInput - } - - export type Builder< - Op extends WriteOperation.ContentfulOperation, - Allowed extends WriteRelationOps[Op['op']] = WriteRelationOps[Op['op']], - D extends WriteOneRelationBuilder.DataFormat[Op['op']] | undefined = WriteOneRelationBuilder.DataFormat[Op['op']], - > = Omit< - WriteOneRelationBuilder, - Exclude - > - - export type BuilderFactory< - Op extends WriteOperation.ContentfulOperation, - Allowed extends WriteRelationOps[Op['op']] = WriteRelationOps[Op['op']], - D extends WriteOneRelationBuilder.DataFormat[Op['op']] | undefined = WriteOneRelationBuilder.DataFormat[Op['op']], - > = D | Builder | ((builder: Builder) => Builder) -} - -export { WriteOneRelationBuilder } diff --git a/packages/client/src/crudQueryBuilder/index.ts b/packages/client/src/crudQueryBuilder/index.ts index 2fe44656bf..c9f6f047dc 100644 --- a/packages/client/src/crudQueryBuilder/index.ts +++ b/packages/client/src/crudQueryBuilder/index.ts @@ -1,7 +1 @@ -export * from './CrudQueryBuilder' -export * from './ReadBuilder' export * from './types' -export * from './WriteBuilder' -export * from './WriteDataBuilder' -export * from './WriteManyRelationBuilder' -export * from './WriteOneRelationBuilder' diff --git a/packages/client/src/graphQlBuilder/GraphQlBuilderError.ts b/packages/client/src/graphQlBuilder/GraphQlBuilderError.ts deleted file mode 100644 index be52f57e68..0000000000 --- a/packages/client/src/graphQlBuilder/GraphQlBuilderError.ts +++ /dev/null @@ -1 +0,0 @@ -export class GraphQlBuilderError extends Error {} diff --git a/packages/client/src/graphQlBuilder/ObjectBuilder.ts b/packages/client/src/graphQlBuilder/ObjectBuilder.ts deleted file mode 100644 index 40cf27830d..0000000000 --- a/packages/client/src/graphQlBuilder/ObjectBuilder.ts +++ /dev/null @@ -1,87 +0,0 @@ -export class ObjectBuilder { - constructor( - public readonly fields: string[] = [], - public readonly objects: { [name: string]: ObjectBuilder } = {}, - public readonly args: { [name: string]: any } = {}, - public readonly fragmentApplications: string[] = [], - public readonly inlineFragments: { [typeName: string]: ObjectBuilder } = {}, - public readonly objectName?: string, - ) {} - - public argument(name: string, value: any): ObjectBuilder { - return new ObjectBuilder( - this.fields, - this.objects, - { ...this.args, [name]: value }, - this.fragmentApplications, - this.inlineFragments, - this.objectName, - ) - } - - public name(name: string): ObjectBuilder { - return new ObjectBuilder( - this.fields, - this.objects, - this.args, - this.fragmentApplications, - this.inlineFragments, - name, - ) - } - - public field(name: string): ObjectBuilder { - return new ObjectBuilder( - [...this.fields, name], - this.objects, - this.args, - this.fragmentApplications, - this.inlineFragments, - this.objectName, - ) - } - - public object(name: string, builder: ((builder: ObjectBuilder) => ObjectBuilder) | ObjectBuilder): ObjectBuilder { - if (!(builder instanceof ObjectBuilder)) { - builder = builder(new ObjectBuilder()) - } - - return new ObjectBuilder( - this.fields, - { ...this.objects, [name]: builder }, - this.args, - this.fragmentApplications, - this.inlineFragments, - this.objectName, - ) - } - - public inlineFragment( - typeName: string, - builder: ((builder: ObjectBuilder) => ObjectBuilder) | ObjectBuilder, - ): ObjectBuilder { - if (!(builder instanceof ObjectBuilder)) { - builder = builder(new ObjectBuilder()) - } - - return new ObjectBuilder( - this.fields, - this.objects, - this.args, - this.fragmentApplications, - { ...this.inlineFragments, [typeName]: builder }, - this.objectName, - ) - } - - public applyFragment(fragmentName: string): ObjectBuilder { - return new ObjectBuilder( - this.fields, - this.objects, - this.args, - [...this.fragmentApplications, fragmentName], - this.inlineFragments, - this.objectName, - ) - } -} diff --git a/packages/client/src/graphQlBuilder/QueryBuilder.ts b/packages/client/src/graphQlBuilder/QueryBuilder.ts index 8fd963e2cf..0ef9677bce 100644 --- a/packages/client/src/graphQlBuilder/QueryBuilder.ts +++ b/packages/client/src/graphQlBuilder/QueryBuilder.ts @@ -1,26 +1,7 @@ import type { GraphQlLiteral } from './GraphQlLiteral' -import { QueryCompiler } from './QueryCompiler' -import { RootObjectBuilder } from './RootObjectBuilder' -class QueryBuilder { - query(builder: ((builder: RootObjectBuilder) => RootObjectBuilder) | RootObjectBuilder): string { - if (!(builder instanceof RootObjectBuilder)) { - builder = builder(new RootObjectBuilder()) - } - const compiler = new QueryCompiler('query', builder) - return compiler.create() - } - mutation(builder: ((builder: RootObjectBuilder) => RootObjectBuilder) | RootObjectBuilder): string { - if (!(builder instanceof RootObjectBuilder)) { - builder = builder(new RootObjectBuilder()) - } - const compiler = new QueryCompiler('mutation', builder) - return compiler.create() - } -} - -namespace QueryBuilder { +export namespace QueryBuilder { export interface Object { [key: string]: Value } @@ -28,7 +9,5 @@ namespace QueryBuilder { export interface List extends Array {} export type AtomicValue = string | null | number | boolean | GraphQlLiteral - export type Value = AtomicValue | QueryBuilder.Object | List + export type Value = AtomicValue | Object | List } - -export { QueryBuilder } diff --git a/packages/client/src/graphQlBuilder/QueryCompiler.ts b/packages/client/src/graphQlBuilder/QueryCompiler.ts deleted file mode 100644 index d9f695c204..0000000000 --- a/packages/client/src/graphQlBuilder/QueryCompiler.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { GraphQlLiteral } from './GraphQlLiteral' -import type { ObjectBuilder } from './ObjectBuilder' -import type { RootObjectBuilder } from './RootObjectBuilder' - -export class QueryCompiler { - constructor(private operation: 'query' | 'mutation', private builder: RootObjectBuilder) {} - - public create(): string { - const rootObjects = this.formatRootObjects(this.builder.objects) - const fragmentDefinitions = this.formatFragmentDefinitions(this.builder.fragmentDefinitions) - - return `${this.operation} {\n${rootObjects}\n}${fragmentDefinitions ? `\n${fragmentDefinitions}` : ''}` - } - - private formatFragmentDefinitions(fragments: RootObjectBuilder['fragmentDefinitions']): string { - const lines: string[] = [] - - for (const name in fragments) { - const object = fragments[name] - - lines.push(`fragment ${name} on ${object.objectName!} {`) - lines.push(...this.formatObjectBody(object)) - lines.push('}') - } - - return lines.join('\n') - } - - private formatRootObjects(objects: RootObjectBuilder['objects']): string { - const lines: string[] = [] - - for (const alias in objects) { - lines.push(...this.formatObject(alias, objects[alias]).map(val => `\t${val}`)) - } - - return lines.join('\n') - } - - private formatObject(alias: string, builder: ObjectBuilder): string[] { - const result = [] - - result.push(`${alias}${builder.objectName ? `: ${builder.objectName}` : ''}${this.formatArgs(builder.args, 0)} {`) - - result.push(...this.formatObjectBody(builder)) - - result.push('}') - - return result - } - - private formatObjectBody(builder: ObjectBuilder): string[] { - const result = ['__typename'] - for (const fieldName of builder.fields) { - result.push(fieldName) - } - for (const fragmentName of builder.fragmentApplications) { - result.push(`... ${fragmentName}`) - } - for (const typeName in builder.inlineFragments) { - const fragment = builder.inlineFragments[typeName] - result.push(`... on ${typeName} {`, ...this.formatObjectBody(fragment), '}') - } - for (const alias in builder.objects) { - result.push(...this.formatObject(alias, builder.objects[alias])) - } - return result.map(val => `\t${val}`) - } - - private formatArgs(args: any, level: number): string { - if (args === null) { - return 'null' - } - - if (typeof args === 'number') { - return args.toString() - } - - if (typeof args === 'boolean') { - return args ? 'true' : 'false' - } - if (typeof args === 'string') { - return JSON.stringify(args) - } - - if (Array.isArray(args)) { - const vals = args.map(val => this.formatArgs(val, level + 1)) - return `[${vals.join(', ')}]` - } - if (args instanceof GraphQlLiteral) { - return args.value - } - - if (typeof args === 'object') { - let result = '' - for (let key in args) { - const argValue = args[key] - if (argValue === undefined) { - continue - } - result += `${key}: ${this.formatArgs(argValue, level + 1)}, ` - } - if (result.length > 0) { - result = result.substring(0, result.length - 2) - } - if (level > 0) { - return `{${result}}` - } else if (result.length > 0) { - return `(${result})` - } - return '' - } - - throw new Error(typeof args) - } -} diff --git a/packages/client/src/graphQlBuilder/RootObjectBuilder.ts b/packages/client/src/graphQlBuilder/RootObjectBuilder.ts deleted file mode 100644 index f1e3876177..0000000000 --- a/packages/client/src/graphQlBuilder/RootObjectBuilder.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { isEmptyObject } from '../utils' -import { GraphQlBuilderError } from './GraphQlBuilderError' -import { ObjectBuilder } from './ObjectBuilder' - -export class RootObjectBuilder { - constructor( - public readonly objects: { [name: string]: ObjectBuilder } = {}, - public readonly fragmentDefinitions: { [name: string]: ObjectBuilder } = {}, - ) {} - - public fragment( - name: string, - builder: ((builder: ObjectBuilder) => ObjectBuilder) | ObjectBuilder, - ): RootObjectBuilder { - if (!(builder instanceof ObjectBuilder)) { - builder = builder(new ObjectBuilder()) - } - - if (!isEmptyObject(builder.args)) { - throw new GraphQlBuilderError(`Cannot supply args to Graph QL fragments!`) - } - if (!builder.objectName) { - throw new GraphQlBuilderError(`Object names are mandatory for Graph QL fragments!`) - } - - return new RootObjectBuilder(this.objects, { ...this.fragmentDefinitions, [name]: builder }) - } - - public object(name: string, builder: ((builder: ObjectBuilder) => ObjectBuilder) | ObjectBuilder): RootObjectBuilder { - if (!(builder instanceof ObjectBuilder)) { - builder = builder(new ObjectBuilder()) - } - - return new RootObjectBuilder({ ...this.objects, [name]: builder }, this.fragmentDefinitions) - } -} diff --git a/packages/client/src/graphQlBuilder/index.ts b/packages/client/src/graphQlBuilder/index.ts index 7715e411d3..ac9ea51373 100644 --- a/packages/client/src/graphQlBuilder/index.ts +++ b/packages/client/src/graphQlBuilder/index.ts @@ -1,5 +1,2 @@ export * from './GraphQlLiteral' -export * from './ObjectBuilder' export * from './QueryBuilder' -export * from './QueryCompiler' -export * from './RootObjectBuilder' diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index 170f11362d..ae11f144cb 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -6,20 +6,9 @@ export { GraphQlLiteral } from './graphQlBuilder' export namespace GraphQlBuilder { export import GraphqlLiteral = GraphQlBuilderTmp.GraphQlLiteral export import GraphQlLiteral = GraphQlBuilderTmp.GraphQlLiteral - export import ObjectBuilder = GraphQlBuilderTmp.ObjectBuilder - export import QueryCompiler = GraphQlBuilderTmp.QueryCompiler - export import QueryBuilder = GraphQlBuilderTmp.QueryBuilder - export import RootObjectBuilder = GraphQlBuilderTmp.RootObjectBuilder } export namespace CrudQueryBuilder { - export import CrudQueryBuilder = CrudQueryBuilderTmp.CrudQueryBuilder - export import ReadBuilder = CrudQueryBuilderTmp.ReadBuilder - export import WriteBuilder = CrudQueryBuilderTmp.WriteBuilder - export import WriteDataBuilder = CrudQueryBuilderTmp.WriteDataBuilder - export import WriteManyRelationBuilder = CrudQueryBuilderTmp.WriteManyRelationBuilder - export import WriteOneRelationBuilder = CrudQueryBuilderTmp.WriteOneRelationBuilder - export import WriteOperation = CrudQueryBuilderTmp.WriteOperation export type CreateMutationArguments = CrudQueryBuilderTmp.CreateMutationArguments export type CreateMutationFields = CrudQueryBuilderTmp.CreateMutationFields export type DeleteMutationArguments = CrudQueryBuilderTmp.DeleteMutationArguments diff --git a/packages/client/tests/cases/unit/crudQueryBuilder.spec.ts b/packages/client/tests/cases/unit/crudQueryBuilder.spec.ts deleted file mode 100644 index c865afaa94..0000000000 --- a/packages/client/tests/cases/unit/crudQueryBuilder.spec.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { expect, it, describe } from 'vitest' -import { CrudQueryBuilder } from '../../../src' -import type { DeleteMutationArguments } from '../../../src/crudQueryBuilder' - -describe('crud query builder', () => { - it('complex mutation', () => { - const builder = new CrudQueryBuilder.CrudQueryBuilder() - .update('Post', builder => - builder - .data(data => - data - .set('name', 'John') - .many('locales', builder => builder.connect({ id: '1' }).update({ locale: 'cs' }, { title: 'foo' })) - .many('tags', b => - b.connect({ id: '1' }, 'connectId1').create({ name: 'foo' }, 'createNameFoo').disconnect({ id: 2 }), - ) - .many('locales', [{ update: { by: { id: '123' }, data: { foo: 'bar' } } }]) - .one('author', { create: { name: 'John' } }), - ) - .by({ id: '123' }) - .node(builder => - builder - .column('id') - .inlineFragment('Foo', builder => builder.column('bar')) - .hasOneRelation('author', o => o.column('name')), - ), - ) - .delete( - 'Category', - CrudQueryBuilder.ReadBuilder.instantiate().by({ id: '123' }).column('id'), - ) - .fragment('authorSnippet', 'Author', builder => - builder.column('nickName').hasOneRelation('favoritePet', builder => builder.column('name')), - ) - .create('Author', builder => - builder - .node(builder => builder.column('name').applyFragment('authorSnippet')) - .data(builder => - builder - .set('name', 'John') - .many('posts', builder => builder.connect({ id: '456' }).create(builder => builder.set('title', 'Abcd'))), - ), - ) - - expect(builder.getGql()).toMatchInlineSnapshot(` - "mutation { - updatePost(data: {name: \\"John\\", locales: [{connect: {id: \\"1\\"}}, {update: {by: {locale: \\"cs\\"}, data: {title: \\"foo\\"}}}, {update: {by: {id: \\"123\\"}, data: {foo: \\"bar\\"}}}], tags: [{connect: {id: \\"1\\"}, alias: \\"connectId1\\"}, {create: {name: \\"foo\\"}, alias: \\"createNameFoo\\"}, {disconnect: {id: 2}}], author: {create: {name: \\"John\\"}}}, by: {id: \\"123\\"}) { - __typename - node { - __typename - id - ... on Foo { - __typename - bar - } - author { - __typename - name - } - } - } - deleteCategory(by: {id: \\"123\\"}) { - __typename - id - } - createAuthor(data: {name: \\"John\\", posts: [{connect: {id: \\"456\\"}}, {create: {title: \\"Abcd\\"}}]}) { - __typename - node { - __typename - name - ... authorSnippet - } - } - } - fragment authorSnippet on Author { - __typename - nickName - favoritePet { - __typename - name - } - }" - `) - }) - - it('mutation part merging', () => { - const builder = new CrudQueryBuilder.CrudQueryBuilder().update('Post', builder => - builder - .data(data => - data - .set('name', 'John') - .many('locales', builder => builder.connect({ id: '1' }).update({ locale: 'cs' }, { title: 'foo' })) - .many('tags', b => b.connect({ id: '1' }, 'connectId1')) - .many('locales', [{ update: { by: { id: '123' }, data: { foo: 'bar' } } }]) - .many('tags', b => b.create({ name: 'foo' }, 'createNameFoo')) - .many('tags', b => b.disconnect({ id: 2 })) - .one('author', { create: { surname: 'Smith' } }) - .many('locales', [{ update: { by: { id: '456' }, data: { foo: 'baz' } } }]) - .one('author', { create: { name: 'John' } }), - ) - .by({ id: '123' }) - .node(builder => builder.column('id')), - ) - - expect(builder.getGql()).toMatchInlineSnapshot(` - "mutation { - updatePost(data: {name: \\"John\\", locales: [{connect: {id: \\"1\\"}}, {update: {by: {locale: \\"cs\\"}, data: {title: \\"foo\\"}}}, {update: {by: {id: \\"123\\"}, data: {foo: \\"bar\\"}}}, {update: {by: {id: \\"456\\"}, data: {foo: \\"baz\\"}}}], tags: [{connect: {id: \\"1\\"}, alias: \\"connectId1\\"}, {create: {name: \\"foo\\"}, alias: \\"createNameFoo\\"}, {disconnect: {id: 2}}], author: {create: {surname: \\"Smith\\", name: \\"John\\"}}}, by: {id: \\"123\\"}) { - __typename - node { - __typename - id - } - } - }" - `) - }) - - it('query', () => { - const builder = new CrudQueryBuilder.CrudQueryBuilder().list( - 'Posts', - q => - q - .filter({ foo: { eq: 'bar' } }) - .column('title') - .hasOneRelation('author', o => o.column('name')), - 'myPostsAlias', - ) - expect(builder.getGql()).toMatchInlineSnapshot(` - "query { - myPostsAlias: listPosts(filter: {foo: {eq: \\"bar\\"}}) { - __typename - title - author { - __typename - name - } - } - }" - `) - }) - - it('validation & errors relation builders', () => { - const builder = new CrudQueryBuilder.CrudQueryBuilder().create('Foo', builder => - builder.data({ bar: '123' }).ok().errors().validation(), - ) - expect(builder.getGql()).toMatchInlineSnapshot(` - "mutation { - createFoo(data: {bar: \\"123\\"}) { - __typename - ok - errors { - __typename - type - message - path { - __typename - ... on _FieldPathFragment { - __typename - field - } - ... on _IndexPathFragment { - __typename - index - alias - } - } - } - validation { - __typename - valid - errors { - __typename - path { - __typename - ... on _FieldPathFragment { - __typename - field - } - ... on _IndexPathFragment { - __typename - index - alias - } - } - message { - __typename - text - } - } - } - } - }" - `) - }) -}) diff --git a/packages/client/tests/cases/unit/graphQlBuilder.spec.ts b/packages/client/tests/cases/unit/graphQlBuilder.spec.ts deleted file mode 100644 index 274b578551..0000000000 --- a/packages/client/tests/cases/unit/graphQlBuilder.spec.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { expect, it, describe } from 'vitest' -import { GraphQlBuilder } from '../../../src' - -describe('GraphQlQueryBuilder', () => { - it('construct simple query', () => { - const query = new GraphQlBuilder.QueryBuilder().query(builder => - builder - .object('Post', object => - object - .argument('where', { id: '123' }) - .field('id') - .field('publishedAt') - .inlineFragment('Article', new GraphQlBuilder.ObjectBuilder().field('leadParagraph')) - .inlineFragment( - 'BlogPost', - new GraphQlBuilder.ObjectBuilder().object('comments', new GraphQlBuilder.ObjectBuilder().field('id')), - ) - .object( - 'locales', - new GraphQlBuilder.ObjectBuilder() - .argument('where', { locale: { eq: new GraphQlBuilder.GraphQlLiteral('cs') } }) - .field('id') - .field('title'), - ), - ) - .object('Authors', o => o.field('id')), - ) - expect(query).toMatchInlineSnapshot(` - "query { - Post(where: {id: \\"123\\"}) { - __typename - id - publishedAt - ... on Article { - __typename - leadParagraph - } - ... on BlogPost { - __typename - comments { - __typename - id - } - } - locales(where: {locale: {eq: cs}}) { - __typename - id - title - } - } - Authors { - __typename - id - } - }" - `) - }) -}) From f68ca260f583d6e57a35cbbe681cd7120d534384 Mon Sep 17 00:00:00 2001 From: David Matejka Date: Thu, 23 Nov 2023 17:11:31 +0100 Subject: [PATCH 10/17] chore: update contember-engine packages (schema etc) to 1.3.6 --- .github/workflows/ci.yaml | 2 +- docker-compose.yaml | 6 +- ee/admin-server/package.json | 2 +- packages/admin-sandbox/api/index.ts | 3 +- packages/admin-sandbox/package.json | 8 +- packages/admin/package.json | 8 +- .../admin/tests/playwright/admin/index.tsx | 3 +- packages/admin/tests/playwright/utils.ts | 2 +- packages/client-generator/package.json | 6 +- packages/client/package.json | 2 +- packages/interface-tester/package.json | 4 +- yarn.lock | 147 +++++++++++------- 12 files changed, 113 insertions(+), 80 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 73a0e72d58..18a4f6dab9 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -85,7 +85,7 @@ jobs: --health-retries 5 contember-engine: - image: contember/engine:1.2.0 + image: contember/engine:1.3.6 env: NODE_ENV: 'development' CONTEMBER_PORT: '4000' diff --git a/docker-compose.yaml b/docker-compose.yaml index f4e8b6c432..c1ad0daf8b 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -82,7 +82,7 @@ services: - .:/src:cached contember-engine: - image: contember/engine:1.2.0 + image: contember/engine:1.3.6 environment: NODE_ENV: 'development' @@ -120,7 +120,7 @@ services: condition: service_healthy playwright-contember-engine: - image: contember/engine:1.2.0 + image: contember/engine:1.3.6 environment: NODE_ENV: 'development' @@ -159,7 +159,7 @@ services: contember-cli: - image: contember/cli:1.2.0 + image: contember/cli:1.3.6 deploy: replicas: 0 diff --git a/ee/admin-server/package.json b/ee/admin-server/package.json index 782c7a29fc..93772613b0 100644 --- a/ee/admin-server/package.json +++ b/ee/admin-server/package.json @@ -20,7 +20,7 @@ "dependencies": { "@aws-sdk/client-s3": "^3.37.0", "@aws-sdk/signature-v4-crt": "^3.37.0", - "@contember/dic": "^1.2.0", + "@contember/dic": "^1.3.6", "cookie": "^0.4.1", "ipaddr.js": "^2.0.1", "mime": "^2.5.2", diff --git a/packages/admin-sandbox/api/index.ts b/packages/admin-sandbox/api/index.ts index 2d5ab16931..12374cde3b 100644 --- a/packages/admin-sandbox/api/index.ts +++ b/packages/admin-sandbox/api/index.ts @@ -1,11 +1,12 @@ import { Schema } from '@contember/schema' import { InputValidation, PermissionsBuilder, SchemaDefinition } from '@contember/schema-definition' import * as modelDefinition from './model' +import { emptySchema } from '@contember/schema-utils' const model = SchemaDefinition.createModel(modelDefinition) const schema: Schema = { - settings: {}, + ...emptySchema, acl: { roles: { admin: { diff --git a/packages/admin-sandbox/package.json b/packages/admin-sandbox/package.json index c6c27c212f..1a3fcd1ca8 100644 --- a/packages/admin-sandbox/package.json +++ b/packages/admin-sandbox/package.json @@ -15,11 +15,13 @@ "@contember/brand": "workspace:*", "@contember/interface-tester": "workspace:*", "@contember/layout": "workspace:*", - "@contember/ui": "workspace:*" + "@contember/ui": "workspace:*", + "graphql": "^16.8.1" }, "dependencies": { - "@contember/schema": "^1.2.0", - "@contember/schema-definition": "^1.2.0", + "@contember/schema": "^1.3.6", + "@contember/schema-definition": "^1.3.6", + "@contember/schema-utils": "^1.3.6", "lucide-react": "^0.268.0", "react": "^17 || ^18", "react-content-loader": "^6.0.3", diff --git a/packages/admin/package.json b/packages/admin/package.json index ca059293c4..6c88142f0f 100644 --- a/packages/admin/package.json +++ b/packages/admin/package.json @@ -90,10 +90,10 @@ }, "devDependencies": { "@babel/core": "7.17.10", - "@contember/schema": "^1.2.0", - "@contember/schema-definition": "^1.2.0", - "@contember/schema-migrations": "^1.2.0", - "@contember/schema-utils": "^1.2.0", + "@contember/schema": "^1.3.6", + "@contember/schema-definition": "^1.3.6", + "@contember/schema-migrations": "^1.3.6", + "@contember/schema-utils": "^1.3.6", "@playwright/test": "1.32.3", "@types/blueimp-md5": "2.18.0", "@types/is-hotkey": "0.1.5", diff --git a/packages/admin/tests/playwright/admin/index.tsx b/packages/admin/tests/playwright/admin/index.tsx index c7a47a21b0..ab6506d9b6 100644 --- a/packages/admin/tests/playwright/admin/index.tsx +++ b/packages/admin/tests/playwright/admin/index.tsx @@ -14,6 +14,7 @@ import { useCurrentRequest, } from '../../../src' import './index.css' +import { emptySchema } from '@contember/schema-utils' const projectSlug = window.location.pathname.split('/')[1] const pages = import.meta.glob('../cases/**/*.tsx') @@ -34,7 +35,7 @@ function buildSchema(definitions: SchemaDefinition.ModelDefinition<{}>): Schema } const validation = InputValidation.parseDefinition(definitions) - return { acl, model, validation, settings: {} } + return { ...emptySchema, acl, model, validation } } const SetProjectSlugContext = createContext<(slug: string | undefined) => void>(() => { diff --git a/packages/admin/tests/playwright/utils.ts b/packages/admin/tests/playwright/utils.ts index 117412fd06..7d108e7b96 100644 --- a/packages/admin/tests/playwright/utils.ts +++ b/packages/admin/tests/playwright/utils.ts @@ -26,7 +26,7 @@ export function buildSchema(definitions: SchemaDefinition.ModelDefinition<{}>): } const validation = InputValidation.parseDefinition(definitions) - return { acl, model, validation, settings: {} } + return { ...emptySchema, acl, model, validation } } export function buildMigration(schema: Schema): Migration { diff --git a/packages/client-generator/package.json b/packages/client-generator/package.json index 59a2f58b95..ba46713bb9 100644 --- a/packages/client-generator/package.json +++ b/packages/client-generator/package.json @@ -40,11 +40,11 @@ }, "dependencies": { "@contember/client": "workspace:*", - "@contember/schema": "^1.2.0", - "@contember/schema-utils": "^1.2.0" + "@contember/schema": "^1.3.6", + "@contember/schema-utils": "^1.3.6" }, "devDependencies": { - "@contember/schema-definition": "^1.2.0", + "@contember/schema-definition": "^1.3.6", "@types/node": "^18" }, "repository": { diff --git a/packages/client/package.json b/packages/client/package.json index 889b081622..1463de1c9d 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -35,7 +35,7 @@ "test": "vitest" }, "dependencies": { - "@contember/schema": "^1.2.0", + "@contember/schema": "^1.3.6", "p-limit": "^4.0.0" } } diff --git a/packages/interface-tester/package.json b/packages/interface-tester/package.json index 00aa8b4729..5f034b2a40 100644 --- a/packages/interface-tester/package.json +++ b/packages/interface-tester/package.json @@ -35,8 +35,8 @@ }, "dependencies": { "@contember/admin": "workspace:*", - "@contember/schema": "^1.2.0", - "@contember/schema-utils": "^1.2.0", + "@contember/schema": "^1.3.6", + "@contember/schema-utils": "^1.3.6", "fast-glob": "^3.2.12", "micromatch": "^4.0.5" }, diff --git a/yarn.lock b/yarn.lock index a6601e0e86..d5cccc6d10 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2669,9 +2669,11 @@ __metadata: "@contember/brand": "workspace:*" "@contember/interface-tester": "workspace:*" "@contember/layout": "workspace:*" - "@contember/schema": ^1.2.0 - "@contember/schema-definition": ^1.2.0 + "@contember/schema": ^1.3.6 + "@contember/schema-definition": ^1.3.6 + "@contember/schema-utils": ^1.3.6 "@contember/ui": "workspace:*" + graphql: ^16.8.1 lucide-react: ^0.268.0 react: ^17 || ^18 react-content-loader: ^6.0.3 @@ -2686,7 +2688,7 @@ __metadata: "@aws-sdk/client-s3": ^3.37.0 "@aws-sdk/signature-v4-crt": ^3.37.0 "@contember/admin": "workspace:*" - "@contember/dic": ^1.2.0 + "@contember/dic": ^1.3.6 "@types/cookie": ^0.4.1 "@types/mime": ^2.0.3 "@types/node": ^18 @@ -2720,10 +2722,10 @@ __metadata: "@contember/react-leaflet-fields-ui": "workspace:*" "@contember/react-multipass-rendering": "workspace:*" "@contember/react-utils": "workspace:*" - "@contember/schema": ^1.2.0 - "@contember/schema-definition": ^1.2.0 - "@contember/schema-migrations": ^1.2.0 - "@contember/schema-utils": ^1.2.0 + "@contember/schema": ^1.3.6 + "@contember/schema-definition": ^1.3.6 + "@contember/schema-migrations": ^1.3.6 + "@contember/schema-utils": ^1.3.6 "@contember/ui": "workspace:*" "@contember/utilities": "workspace:*" "@playwright/test": 1.32.3 @@ -2791,9 +2793,9 @@ __metadata: resolution: "@contember/client-generator@workspace:packages/client-generator" dependencies: "@contember/client": "workspace:*" - "@contember/schema": ^1.2.0 - "@contember/schema-definition": ^1.2.0 - "@contember/schema-utils": ^1.2.0 + "@contember/schema": ^1.3.6 + "@contember/schema-definition": ^1.3.6 + "@contember/schema-utils": ^1.3.6 "@types/node": ^18 bin: contember-client-generator: ./dist/production/generate.js @@ -2804,38 +2806,47 @@ __metadata: version: 0.0.0-use.local resolution: "@contember/client@workspace:packages/client" dependencies: - "@contember/schema": ^1.2.0 + "@contember/schema": ^1.3.6 p-limit: ^4.0.0 languageName: unknown linkType: soft -"@contember/database-migrations@npm:^1.2.0": - version: 1.2.0 - resolution: "@contember/database-migrations@npm:1.2.0" +"@contember/database-migrations@npm:1.3.6": + version: 1.3.6 + resolution: "@contember/database-migrations@npm:1.3.6" dependencies: - "@contember/database": ^1.2.0 + "@contember/database": 1.3.6 node-pg-migrate: ^5.9.0 peerDependencies: pg: ^8.9.0 - checksum: ed0242111bc32ac6cd1228e7635743dc8f46c7247082899c2b3c4c7b6a5702c0973e116dff3ac439b000982faab29ed66563721a7b328da19d7b25fc212b1323 + checksum: 1f13c3ba3995f2c2a99b22383ba71302a3db8ca2d2a62dc66e85365759c307b3475e9940d6227e6a6c9267616e87f0167ccf087ff760286f6ed3f39f8b029c49 languageName: node linkType: hard -"@contember/database@npm:^1.2.0": - version: 1.2.0 - resolution: "@contember/database@npm:1.2.0" +"@contember/database@npm:1.3.6": + version: 1.3.6 + resolution: "@contember/database@npm:1.3.6" dependencies: - "@contember/queryable": ^1.2.0 + "@contember/queryable": 1.3.6 peerDependencies: pg: ^8.9.0 - checksum: 4a1bf69c65fa439626a7fd8445f85232ac89f35baf624a3c0fd876c654d003f254dcf3e0e8bda2f63c6a6d3a95d40fce8513185bc36db5b403ba8ef9ae76a4f4 + checksum: ffabe934de90c04d24c0f42ad6af063291fb6db81c1ae2e88699f288cbef17b058df8547a517542867890cc595c7825cc0a3d3ea04c0bc2dd38e23532fbbd350 languageName: node linkType: hard -"@contember/dic@npm:^1.2.0": - version: 1.2.0 - resolution: "@contember/dic@npm:1.2.0" - checksum: 616bf9297bf3cef946ab60ebe73cfb97dd1ff47c98bb1331ab0cce9222cc8ce75356f9b6b4015b30a3c7caa9da1567997f4cedfd85024644d17606460521248a +"@contember/dic@npm:^1.3.6": + version: 1.3.6 + resolution: "@contember/dic@npm:1.3.6" + checksum: 66c30c2bc9641b039a7493f1c6b0b13e10387eaaf0033debb5199e8e047fa873854b9deb9b9c27ce6dabc02989efa6e16019db7733ba629e7ae310516f42e17e + languageName: node + linkType: hard + +"@contember/engine-common@npm:1.3.6": + version: 1.3.6 + resolution: "@contember/engine-common@npm:1.3.6" + dependencies: + "@contember/logger": 1.3.6 + checksum: 892a34da24e5eacef309cc4918037432954524ccd01ea656fac0597483ca96220a59af23fa7f64c3d542b22e47f33aebe379c36b5364ca2d8cb7cdf09f5b2655 languageName: node linkType: hard @@ -2844,8 +2855,8 @@ __metadata: resolution: "@contember/interface-tester@workspace:packages/interface-tester" dependencies: "@contember/admin": "workspace:*" - "@contember/schema": ^1.2.0 - "@contember/schema-utils": ^1.2.0 + "@contember/schema": ^1.3.6 + "@contember/schema-utils": ^1.3.6 "@types/micromatch": ^4.0.2 "@types/node": ^18 fast-glob: ^3.2.12 @@ -2876,10 +2887,19 @@ __metadata: languageName: unknown linkType: soft -"@contember/queryable@npm:^1.2.0": - version: 1.2.0 - resolution: "@contember/queryable@npm:1.2.0" - checksum: cb94b4a3dbb219c85102b6f3e22da2ab952e41ab2d5066ed3615fdaf2db0710ffec49da01cf369ff1fa8f88a00faca2b2c55815c5ab1041787dca0f7874f64b6 +"@contember/logger@npm:1.3.6": + version: 1.3.6 + resolution: "@contember/logger@npm:1.3.6" + dependencies: + chalk: ^4.1.2 + checksum: 22bf79cde6c92310d198e85aac444bc1f01fd99142fa041128fa55ccbf974635ea1b899c4a8e28991cd03a89730a1d5dbd927c251b75231bad57a0fd7c3c173c + languageName: node + linkType: hard + +"@contember/queryable@npm:1.3.6": + version: 1.3.6 + resolution: "@contember/queryable@npm:1.3.6" + checksum: 7d4683a6098c2b0b638c9fc7a3be7adf4a1b93227f7c7c175e63879ddc303afc814f9f730139b90a4a3c0a1827ba3affa9c7edb3a15e9f8e3d6db7910d5a3025 languageName: node linkType: hard @@ -3096,54 +3116,56 @@ __metadata: languageName: unknown linkType: soft -"@contember/schema-definition@npm:^1.2.0": - version: 1.2.0 - resolution: "@contember/schema-definition@npm:1.2.0" +"@contember/schema-definition@npm:^1.3.6": + version: 1.3.6 + resolution: "@contember/schema-definition@npm:1.3.6" dependencies: - "@contember/schema": ^1.2.0 - "@contember/schema-utils": ^1.2.0 + "@contember/schema": 1.3.6 + "@contember/schema-utils": 1.3.6 reflect-metadata: ^0.1.13 - checksum: f2e5263652ede86c9b6b200cf83fe20d78bfe78d8fc28055d5b5b090d6f76d5a7c9a3efce9459d34900af1bf64ca14db9dd2ee22cf1403b4b9d80918408ecb30 + peerDependencies: + graphql: ">= 14.6.0" + checksum: 1b90b12c7a3921206fb039c362dba2befb321277c98a16c6fbfc9ac198648ceac9ef510b02fa6d3cab113127304803dd380271066e75c3a4c3a7761937db7fca languageName: node linkType: hard -"@contember/schema-migrations@npm:^1.2.0": - version: 1.2.0 - resolution: "@contember/schema-migrations@npm:1.2.0" +"@contember/schema-migrations@npm:^1.3.6": + version: 1.3.6 + resolution: "@contember/schema-migrations@npm:1.3.6" dependencies: - "@contember/database": ^1.2.0 - "@contember/database-migrations": ^1.2.0 - "@contember/schema": ^1.2.0 - "@contember/schema-utils": ^1.2.0 + "@contember/database-migrations": 1.3.6 + "@contember/engine-common": 1.3.6 + "@contember/schema": 1.3.6 + "@contember/schema-utils": 1.3.6 fast-deep-equal: ^3.1.3 rfc6902: ^5.0.1 peerDependencies: pg: ^8.9.0 - checksum: e3b43f0d9c636c28852e8d488d5e67ebd071210de24461e4f9f622de76dcb6017f26e0555bf95617d4995fc53c790f85ff451f866b17ae90593fd58e84155c49 + checksum: 1ce11b7721984a9f18431d685ba2aea58be5f3ea11ba0d82f1306ffbe37010d01a5f9d2de0b7b6931ceff7ed074458a8ad6cef4d21ecf77a2006894c52be0bd7 languageName: node linkType: hard -"@contember/schema-utils@npm:^1.2.0": - version: 1.2.0 - resolution: "@contember/schema-utils@npm:1.2.0" +"@contember/schema-utils@npm:1.3.6, @contember/schema-utils@npm:^1.3.6": + version: 1.3.6 + resolution: "@contember/schema-utils@npm:1.3.6" dependencies: - "@contember/schema": ^1.2.0 - "@contember/typesafe": ^1.2.0 - checksum: 5f39783e9ec0dd908a6200179eb919c7ec744784092697da5e1d38d2bc7c0ac2abb97dc80d730aaa4bcce020533c029edd3a74603bae3b510f7db392f3371098 + "@contember/schema": 1.3.6 + "@contember/typesafe": 1.3.6 + checksum: 5812ff9ddd140af49b312e03a05c93bb09bc4857ad3b2f9573a331bfc7fe24f8f4c36b427a80147f1c7e15d9a5e4208c9c239069115ba2baca76f9af3634042d languageName: node linkType: hard -"@contember/schema@npm:^1.2.0": - version: 1.2.0 - resolution: "@contember/schema@npm:1.2.0" - checksum: da1f04ada08d33d209a24e7938ce1955121bdcb709ca5794ac6580afea11b4bbcafa4b7807e3410852ec7db065678e8c53ea5254eb1672b9ae1bd357aca49d2b +"@contember/schema@npm:1.3.6, @contember/schema@npm:^1.3.6": + version: 1.3.6 + resolution: "@contember/schema@npm:1.3.6" + checksum: 69f5f5ca3ce43b4c191b75d26a29f5208e6cbe2f0a97825194d37fc787a69ae5dab56e06b97f008888a8c4505e6efdaa1f8d9fb1e3b08ed936c575e5f0f4af83 languageName: node linkType: hard -"@contember/typesafe@npm:^1.2.0": - version: 1.2.0 - resolution: "@contember/typesafe@npm:1.2.0" - checksum: 84f4fd70834327f4dac3babec97386eeb09f8a2b0808532b9c92c88d45af40d841e47c5669e8e731460060a2dfcc37597ada73e2f6078c1b30ca17fe362f0c08 +"@contember/typesafe@npm:1.3.6": + version: 1.3.6 + resolution: "@contember/typesafe@npm:1.3.6" + checksum: 7019af67b48ed6a2e2942f562a496a44cacb43d58e4b03bf8690cad35e41b4411c5cc64b3b20ce62db0df6dd9178edd3349fa7e6004bd3bd76b05cd6d4d8982a languageName: node linkType: hard @@ -10906,6 +10928,13 @@ __metadata: languageName: node linkType: hard +"graphql@npm:^16.8.1": + version: 16.8.1 + resolution: "graphql@npm:16.8.1" + checksum: 8d304b7b6f708c8c5cc164b06e92467dfe36aff6d4f2cf31dd19c4c2905a0e7b89edac4b7e225871131fd24e21460836b369de0c06532644d15b461d55b1ccc0 + languageName: node + linkType: hard + "gunzip-maybe@npm:^1.4.2": version: 1.4.2 resolution: "gunzip-maybe@npm:1.4.2" From 3979a80488cea10077f8338f28ad97500bac44f6 Mon Sep 17 00:00:00 2001 From: David Matejka Date: Thu, 23 Nov 2023 17:19:40 +0100 Subject: [PATCH 11/17] refactor: deprecate graphql literal --- packages/binding/src/queryLanguage/Parser.ts | 35 ++++++++----------- .../cases/unit/queryLanguage/general.spec.tsx | 4 +-- .../cases/unit/queryLanguage/orderBy.spec.tsx | 24 ++++++------- .../singleRelativeField.spec.tsx | 4 +-- .../GenerateUploadUrlMutationBuilder.ts | 11 ++++-- packages/client/src/crudQueryBuilder/types.ts | 6 +++- .../src/graphQlBuilder/GraphQlLiteral.ts | 9 ++++- .../generateUploadUrlMutationBuilder.spec.ts | 2 +- .../accessorSorting/SetOrderFieldOnCreate.tsx | 9 +++-- .../react-datagrid/src/cells/EnumCell.tsx | 6 ++-- 10 files changed, 60 insertions(+), 50 deletions(-) diff --git a/packages/binding/src/queryLanguage/Parser.ts b/packages/binding/src/queryLanguage/Parser.ts index cf079868fe..d570e52416 100644 --- a/packages/binding/src/queryLanguage/Parser.ts +++ b/packages/binding/src/queryLanguage/Parser.ts @@ -1,4 +1,4 @@ -import { CrudQueryBuilder, GraphQlBuilder, Input, Writable } from '@contember/client' +import { GraphQlBuilder, Input, Writable } from '@contember/client' import { EmbeddedActionsParser, Lexer } from 'chevrotain' import { Environment } from '../dao' import type { EntityName, FieldName, Filter, OrderBy, UniqueWhere } from '../treeParameters' @@ -522,10 +522,10 @@ class Parser extends EmbeddedActionsParser { return where }) - private orderBy: () => Input.OrderBy[] = this.RULE< - Input.OrderBy[] + private orderBy: () => Input.OrderBy<`${Input.OrderDirection}`>[] = this.RULE< + Input.OrderBy<`${Input.OrderDirection}`>[] >('orderBy', () => { - const order: Input.OrderBy[] = [] + const order: Input.OrderBy<`${Input.OrderDirection}`>[] = [] this.AT_LEAST_ONE_SEP({ SEP: tokens.Comma, @@ -537,24 +537,24 @@ class Parser extends EmbeddedActionsParser { fieldNames.push(this.SUBRULE(this.fieldIdentifier)) }, }) - let literal = this.OPTION(() => this.SUBRULE1(this.graphQlLiteral)) as - | CrudQueryBuilder.OrderDirection - | undefined + const literal = this.OPTION(() => this.SUBRULE1(this.identifier)) this.ACTION(() => { + let orderDirection: `${Input.OrderDirection}` if (literal) { - if (literal.value !== 'asc' && literal.value !== 'desc') { - throw new QueryLanguageError(`The only valid order directions are \`asc\` and \`desc\`.`) + if (literal !== 'asc' && literal !== 'desc' && literal !== 'ascNullsFirst' && literal !== 'descNullsLast') { + throw new QueryLanguageError(`The only valid order directions are 'asc', 'desc', 'ascNullsFirst' and 'descNullsLast'.`) } + orderDirection = literal } else { - literal = new GraphQlBuilder.GraphQlLiteral('asc') + orderDirection = 'asc' } - let orderBy: Input.FieldOrderBy = literal + let orderBy: Input.FieldOrderBy<`${Input.OrderDirection}`> = orderDirection for (let i = fieldNames.length - 1; i >= 0; i--) { orderBy = { [fieldNames[i]]: orderBy } } - order.push(orderBy as Input.OrderBy) + order.push(orderBy as Input.OrderBy<`${Input.OrderDirection}`>) }) }, }) @@ -646,7 +646,7 @@ class Parser extends EmbeddedActionsParser { ALT: () => this.SUBRULE(this.number), }, { - ALT: () => this.SUBRULE(this.graphQlLiteral), + ALT: () => this.SUBRULE(this.identifier), }, { ALT: () => { @@ -757,12 +757,6 @@ class Parser extends EmbeddedActionsParser { return parseFloat(this.CONSUME(tokens.NumberLiteral).image) }) - private graphQlLiteral: () => GraphQlBuilder.GraphQlLiteral = this.RULE('graphQlLiteral', () => { - const image = this.SUBRULE(this.identifier) - - return new GraphQlBuilder.GraphQlLiteral(image) - }) - private variable = this.RULE('variable', () => { this.CONSUME(tokens.DollarSign) const variableName = this.CONSUME(tokens.Identifier).image @@ -778,7 +772,8 @@ class Parser extends EmbeddedActionsParser { } if (Parser.environment.hasVariable(variableName)) { - return Parser.environment.getVariable(variableName) + const value = Parser.environment.getVariable(variableName) + return value instanceof GraphQlBuilder.GraphQlLiteral ? value.value : value } if (Parser.environment.hasParameter(variableName)) { return Parser.environment.getParameter(variableName) diff --git a/packages/binding/tests/cases/unit/queryLanguage/general.spec.tsx b/packages/binding/tests/cases/unit/queryLanguage/general.spec.tsx index 6bd6c80bfb..e7ade019fd 100644 --- a/packages/binding/tests/cases/unit/queryLanguage/general.spec.tsx +++ b/packages/binding/tests/cases/unit/queryLanguage/general.spec.tsx @@ -46,12 +46,12 @@ describe('query language parser', () => { { field: 'field', filter: undefined, - reducedBy: { ab: 456, literalColumn: new GraphQlBuilder.GraphQlLiteral('literal') }, + reducedBy: { ab: 456, literalColumn: 'literal' }, }, { field: 'x', filter: undefined, - reducedBy: { x: new GraphQlBuilder.GraphQlLiteral('truecolor') }, + reducedBy: { x: 'truecolor' }, }, ], }) diff --git a/packages/binding/tests/cases/unit/queryLanguage/orderBy.spec.tsx b/packages/binding/tests/cases/unit/queryLanguage/orderBy.spec.tsx index a35047df34..0436737e18 100644 --- a/packages/binding/tests/cases/unit/queryLanguage/orderBy.spec.tsx +++ b/packages/binding/tests/cases/unit/queryLanguage/orderBy.spec.tsx @@ -9,40 +9,40 @@ const parse = (input: string) => { describe('orderBy QueryLanguage parser', () => { it('should parse single field names', () => { - expect(parse('fooName')).toEqual([{ fooName: new GraphQlBuilder.GraphQlLiteral('asc') }]) + expect(parse('fooName')).toEqual([{ fooName: 'asc' }]) }) it('should parse nested field names', () => { expect(parse('fooName.barName.bazName')).toEqual([ - { fooName: { barName: { bazName: new GraphQlBuilder.GraphQlLiteral('asc') } } }, + { fooName: { barName: { bazName: 'asc' } } }, ]) }) it('should parse multiple field names', () => { expect(parse('foo.bar, baz, x.y.z')).toEqual([ - { foo: { bar: new GraphQlBuilder.GraphQlLiteral('asc') } }, - { baz: new GraphQlBuilder.GraphQlLiteral('asc') }, - { x: { y: { z: new GraphQlBuilder.GraphQlLiteral('asc') } } }, + { foo: { bar: 'asc' } }, + { baz: 'asc' }, + { x: { y: { z: 'asc' } } }, ]) }) it('should parse order directions', () => { expect(parse('foo asc, bar desc')).toEqual([ - { foo: new GraphQlBuilder.GraphQlLiteral('asc') }, - { bar: new GraphQlBuilder.GraphQlLiteral('desc') }, + { foo: 'asc' }, + { bar: 'desc' }, ]) expect(parse('foo.bar asc, a.b.c desc')).toEqual([ - { foo: { bar: new GraphQlBuilder.GraphQlLiteral('asc') } }, - { a: { b: { c: new GraphQlBuilder.GraphQlLiteral('desc') } } }, + { foo: { bar: 'asc' } }, + { a: { b: { c: 'desc' } } }, ]) }) it('should parse order with odd whitespace use', () => { expect(parse(' \t foo.bar ,baz desc, \t x.y.z\t \t desc ')).toEqual([ - { foo: { bar: new GraphQlBuilder.GraphQlLiteral('asc') } }, - { baz: new GraphQlBuilder.GraphQlLiteral('desc') }, - { x: { y: { z: new GraphQlBuilder.GraphQlLiteral('desc') } } }, + { foo: { bar: 'asc' } }, + { baz: 'desc' }, + { x: { y: { z: 'desc' } } }, ]) }) }) diff --git a/packages/binding/tests/cases/unit/queryLanguage/singleRelativeField.spec.tsx b/packages/binding/tests/cases/unit/queryLanguage/singleRelativeField.spec.tsx index ebb3df7fa7..03ec25b9bb 100644 --- a/packages/binding/tests/cases/unit/queryLanguage/singleRelativeField.spec.tsx +++ b/packages/binding/tests/cases/unit/queryLanguage/singleRelativeField.spec.tsx @@ -80,7 +80,7 @@ describe('single relative fields QueryLanguage parser', () => { filter: undefined, reducedBy: { a: 'b', - bar: new GraphQlLiteral('Literal'), + bar: 'Literal', }, }, ], @@ -111,7 +111,7 @@ describe('single relative fields QueryLanguage parser', () => { be: { indeed: { not: { - shallow: new GraphQlLiteral('baz'), + shallow: 'baz', }, }, }, diff --git a/packages/client/src/content/upload/GenerateUploadUrlMutationBuilder.ts b/packages/client/src/content/upload/GenerateUploadUrlMutationBuilder.ts index 158e2707b1..7d8b405715 100644 --- a/packages/client/src/content/upload/GenerateUploadUrlMutationBuilder.ts +++ b/packages/client/src/content/upload/GenerateUploadUrlMutationBuilder.ts @@ -1,6 +1,7 @@ import { GraphQlLiteral } from '../../graphQlBuilder' -import { GraphQlField, GraphQlPrintResult, GraphQlQueryPrinter, GraphQlSelectionSet, GraphQlSelectionSetItem } from '../../builder' +import { GraphQlField, GraphQlPrintResult, GraphQlQueryPrinter, GraphQlSelectionSetItem } from '../../builder' import { replaceGraphQlLiteral } from '../client' +import { GraphQlBuilder } from '../../index' class GenerateUploadUrlMutationBuilder { private static generateUploadUrlFields = [ @@ -43,7 +44,7 @@ class GenerateUploadUrlMutationBuilder { }, acl: { graphQlType: 'S3Acl', - value: fileParameters.acl?.value, + value: fileParameters.acl instanceof GraphQlBuilder.GraphqlLiteral ? fileParameters.acl?.value : fileParameters.acl, }, }, GenerateUploadUrlMutationBuilder.generateUploadUrlFields)) } @@ -55,7 +56,11 @@ class GenerateUploadUrlMutationBuilder { } namespace GenerateUploadUrlMutationBuilder { - export type Acl = GraphQlLiteral<'PUBLIC_READ' | 'PRIVATE' | 'NONE'>; + export type Acl = + | 'PUBLIC_READ' + | 'PRIVATE' + | 'NONE' + | GraphQlLiteral<'PUBLIC_READ' | 'PRIVATE' | 'NONE'> export type FileParameters = { contentType: string diff --git a/packages/client/src/crudQueryBuilder/types.ts b/packages/client/src/crudQueryBuilder/types.ts index 05a5bc5dff..42e65b3f49 100644 --- a/packages/client/src/crudQueryBuilder/types.ts +++ b/packages/client/src/crudQueryBuilder/types.ts @@ -1,3 +1,4 @@ +import { Input } from '@contember/schema' import { GraphQlLiteral } from '../graphQlBuilder' export type Mutations = 'create' | 'update' | 'delete' @@ -44,7 +45,10 @@ export interface WriteRelationOps { update: 'create' | 'connect' | 'delete' | 'disconnect' | 'update' | 'upsert' } -export type OrderDirection = GraphQlLiteral<'asc'> | GraphQlLiteral<'desc'> +export type OrderDirection = + | GraphQlLiteral<'asc'> + | GraphQlLiteral<'desc'> + | `${Input.OrderDirection}` // TODO Silly enums because TS does not support enum extension 🙄 // https://github.com/Microsoft/TypeScript/issues/17592 diff --git a/packages/client/src/graphQlBuilder/GraphQlLiteral.ts b/packages/client/src/graphQlBuilder/GraphQlLiteral.ts index 8d1f2358cd..0cd335f193 100644 --- a/packages/client/src/graphQlBuilder/GraphQlLiteral.ts +++ b/packages/client/src/graphQlBuilder/GraphQlLiteral.ts @@ -1,5 +1,12 @@ +/** + * @deprecated Directly use the value instead. + */ export class GraphQlLiteral { - constructor(public readonly value: Value) {} + constructor(public readonly value: Value) { + if (import.meta.env.DEV) { + console.warn('GraphQlLiteral is deprecated, use the value directly instead.') + } + } public toString() { return `Literal(${this.value})` diff --git a/packages/client/tests/cases/unit/generateUploadUrlMutationBuilder.spec.ts b/packages/client/tests/cases/unit/generateUploadUrlMutationBuilder.spec.ts index 1648e7a590..94bc29493a 100644 --- a/packages/client/tests/cases/unit/generateUploadUrlMutationBuilder.spec.ts +++ b/packages/client/tests/cases/unit/generateUploadUrlMutationBuilder.spec.ts @@ -39,7 +39,7 @@ describe('generate upload url mutation builder', () => { }, myMp3: { contentType: 'audio/mpeg', - acl: new GraphQlLiteral('PUBLIC_READ'), + acl: 'PUBLIC_READ', expiration: 123456, prefix: 'foo', }, diff --git a/packages/react-binding/src/accessorSorting/SetOrderFieldOnCreate.tsx b/packages/react-binding/src/accessorSorting/SetOrderFieldOnCreate.tsx index e59b7633e9..6ec262dba4 100644 --- a/packages/react-binding/src/accessorSorting/SetOrderFieldOnCreate.tsx +++ b/packages/react-binding/src/accessorSorting/SetOrderFieldOnCreate.tsx @@ -1,12 +1,11 @@ -import type { CrudQueryBuilder, Input } from '@contember/client' -import { GraphQlLiteral } from '@contember/client' -import { QueryLanguage } from '@contember/binding' +import type { Input } from '@contember/client' import type { SugaredOrderBy, SugaredQualifiedEntityList, SugaredRelativeSingleField, SugaredUnconstrainedQualifiedSingleEntity, } from '@contember/binding' +import { QueryLanguage } from '@contember/binding' import { Component, EntityListSubTree, EntitySubTree, Field } from '../coreComponents' import { SugaredField } from '../helperComponents' import { addEntityAtIndex } from './addEntityAtIndex' @@ -32,8 +31,8 @@ export const SetOrderFieldOnCreate = Component( [currentValue]: accumulator, }), { - [desugaredOrderField.field]: new GraphQlLiteral(order), - } as Input.OrderBy, + [desugaredOrderField.field]: order, + } as Input.OrderBy<`${Input.OrderDirection}`>, ), ] diff --git a/packages/react-datagrid/src/cells/EnumCell.tsx b/packages/react-datagrid/src/cells/EnumCell.tsx index 748f62ab78..9eccc345be 100644 --- a/packages/react-datagrid/src/cells/EnumCell.tsx +++ b/packages/react-datagrid/src/cells/EnumCell.tsx @@ -1,6 +1,6 @@ import { ComponentType, FunctionComponent, ReactNode } from 'react' import { Component, QueryLanguage, SugarableRelativeSingleField, wrapFilterInHasOnes } from '@contember/react-binding' -import { GraphQlLiteral, Input } from '@contember/client' +import { Input } from '@contember/client' import { DataGridColumnCommonProps, FilterRendererProps } from '../types' import { DataGridColumn } from '../grid' @@ -42,14 +42,14 @@ export const createEnumCell = [] = [] + const conditions: Input.Condition[] = [] if (nullCondition) { conditions.push({ isNull: true }) } conditions.push({ - in: values.map(it => new GraphQlLiteral(it)), + in: values, }) return wrapFilterInHasOnes(desugared.hasOneRelationPath, { From 93cd5a50d5f1d8860dd2fe113dcd3be50bd7f67f Mon Sep 17 00:00:00 2001 From: David Matejka Date: Thu, 23 Nov 2023 17:27:29 +0100 Subject: [PATCH 12/17] refactor(client): remove unused types --- packages/client/src/crudQueryBuilder/types.ts | 69 ------------------- packages/client/src/index.ts | 23 +------ 2 files changed, 3 insertions(+), 89 deletions(-) diff --git a/packages/client/src/crudQueryBuilder/types.ts b/packages/client/src/crudQueryBuilder/types.ts index 42e65b3f49..2a29de10f0 100644 --- a/packages/client/src/crudQueryBuilder/types.ts +++ b/packages/client/src/crudQueryBuilder/types.ts @@ -1,77 +1,8 @@ import { Input } from '@contember/schema' import { GraphQlLiteral } from '../graphQlBuilder' -export type Mutations = 'create' | 'update' | 'delete' - -export type Queries = 'get' | 'list' | 'paginate' - -export type GetQueryArguments = 'by' - -export type ListQueryArguments = 'filter' | 'orderBy' | 'offset' | 'limit' - -export type PaginateQueryArguments = 'filter' | 'orderBy' | 'skip' | 'first' - -export type CreateMutationArguments = 'data' - -export type UpdateMutationArguments = 'data' | 'by' - -export type DeleteMutationArguments = 'by' - -export type ReductionArguments = 'filter' | 'by' - -export type HasOneArguments = 'filter' - -export type HasManyArguments = 'filter' | 'orderBy' | 'offset' | 'limit' - -export type UpdateMutationFields = 'ok' | 'validation' | 'errors' | 'errorMessage' | 'node' - -export type CreateMutationFields = 'ok' | 'validation' | 'errors' | 'errorMessage' | 'node' - -export type DeleteMutationFields = 'ok' | 'node' | 'errors' | 'errorMessage' - -export type WriteArguments = CreateMutationArguments | UpdateMutationArguments | DeleteMutationArguments - -export type WriteFields = UpdateMutationFields | CreateMutationFields - -export type ReadArguments = - | GetQueryArguments - | ListQueryArguments - | PaginateQueryArguments - | HasOneArguments - | HasManyArguments - -export interface WriteRelationOps { - create: 'create' | 'connect' - update: 'create' | 'connect' | 'delete' | 'disconnect' | 'update' | 'upsert' -} - export type OrderDirection = | GraphQlLiteral<'asc'> | GraphQlLiteral<'desc'> | `${Input.OrderDirection}` -// TODO Silly enums because TS does not support enum extension 🙄 -// https://github.com/Microsoft/TypeScript/issues/17592 -export namespace WriteOperation { - export interface Operation { - op: 'create' | 'update' | 'delete' - } - export abstract class Operation implements Operation {} - - export interface ContentfulOperation { - op: 'create' | 'update' - } - export abstract class ContentfulOperation extends Operation implements ContentfulOperation {} - - export class Update extends ContentfulOperation { - override readonly op = 'update' as const - } - - export class Create extends ContentfulOperation { - override readonly op = 'create' as const - } - - export class Delete extends Operation { - override readonly op = 'delete' as const - } -} diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index ae11f144cb..5b0dc50496 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -1,33 +1,16 @@ import * as CrudQueryBuilderTmp from './crudQueryBuilder' import * as GraphQlBuilderTmp from './graphQlBuilder' + export { GraphQlLiteral } from './graphQlBuilder' export namespace GraphQlBuilder { - export import GraphqlLiteral = GraphQlBuilderTmp.GraphQlLiteral - export import GraphQlLiteral = GraphQlBuilderTmp.GraphQlLiteral + export import GraphqlLiteral = GraphQlBuilderTmp.GraphQlLiteral; + export import GraphQlLiteral = GraphQlBuilderTmp.GraphQlLiteral; } export namespace CrudQueryBuilder { - export type CreateMutationArguments = CrudQueryBuilderTmp.CreateMutationArguments - export type CreateMutationFields = CrudQueryBuilderTmp.CreateMutationFields - export type DeleteMutationArguments = CrudQueryBuilderTmp.DeleteMutationArguments - export type DeleteMutationFields = CrudQueryBuilderTmp.DeleteMutationFields - export type GetQueryArguments = CrudQueryBuilderTmp.GetQueryArguments - export type HasManyArguments = CrudQueryBuilderTmp.HasManyArguments - export type HasOneArguments = CrudQueryBuilderTmp.HasOneArguments - export type ListQueryArguments = CrudQueryBuilderTmp.ListQueryArguments - export type Mutations = CrudQueryBuilderTmp.Mutations export type OrderDirection = CrudQueryBuilderTmp.OrderDirection - export type PaginateQueryArguments = CrudQueryBuilderTmp.PaginateQueryArguments - export type Queries = CrudQueryBuilderTmp.Queries - export type ReadArguments = CrudQueryBuilderTmp.ReadArguments - export type ReductionArguments = CrudQueryBuilderTmp.ReductionArguments - export type UpdateMutationArguments = CrudQueryBuilderTmp.UpdateMutationArguments - export type UpdateMutationFields = CrudQueryBuilderTmp.UpdateMutationFields - export type WriteArguments = CrudQueryBuilderTmp.WriteArguments - export type WriteFields = CrudQueryBuilderTmp.WriteFields - export type WriteRelationOps = CrudQueryBuilderTmp.WriteRelationOps } export * from './builder' From 8d78f55b3c508e10428d84b8d4200eeb530c8bb7 Mon Sep 17 00:00:00 2001 From: David Matejka Date: Fri, 8 Dec 2023 15:16:04 +0100 Subject: [PATCH 13/17] refactor: split "client" package --- build/packageList.js | 5 +- .../api-extractor.json | 0 .../package.json | 6 +- .../src/ContemberClientGenerator.ts | 2 +- .../src/EntityTypeSchemaGenerator.ts | 0 .../src/EnumTypeSchemaGenerator.ts | 0 .../src/NameSchemaGenerator.ts | 2 +- .../src/generate.ts | 0 .../src/index.ts | 0 .../src/tsconfig.json | 2 +- .../tests/generateEnittyTypes.test.ts | 0 .../tests/generateEnumTypes.test.ts | 0 .../tests/schemas.ts | 0 .../tests/tsconfig.json | 0 .../tsconfig.json | 0 .../tsdoc.json | 0 .../vite.config.js | 2 +- packages/client-content/api-extractor.json | 3 + packages/client-content/package.json | 47 ++++++ .../src}/ContentClient.ts | 128 ++++++++++------- .../src}/ContentQueryBuilder.ts | 2 +- .../src}/TypedContentQueryBuilder.ts | 0 .../client => client-content/src}/index.ts | 1 - .../src}/nodes/ContentEntitySelection.ts | 2 +- .../src}/nodes/ContentMutation.ts | 2 +- .../src}/nodes/ContentQuery.ts | 2 +- .../src}/nodes/TypedEntitySelection.ts | 0 .../src}/nodes/index.ts | 0 packages/client-content/src/tsconfig.json | 14 ++ .../src}/types/Input.ts | 0 .../src}/types/Result.ts | 0 .../src}/types/Schema.ts | 0 .../src}/types/index.ts | 0 .../src}/utils/createListArgs.ts | 4 +- .../src}/utils/createTypedArgs.ts | 2 +- .../src}/utils/mutationFragments.ts | 2 +- .../tests/cases/unit/lib.ts | 0 .../tests/cases/unit/mutation.test.ts | 0 .../tests/cases/unit/query.test.ts | 0 packages/client-content/tests/tsconfig.json | 10 ++ packages/client-content/tsconfig.json | 8 ++ packages/client-content/tsdoc.json | 6 + packages/client-content/vite.config.js | 3 + packages/client/package.json | 8 ++ packages/client/src/builder/types/json.ts | 4 - packages/client/src/content/index.ts | 2 +- .../utils => }/replaceGraphQlLiteral.ts | 5 +- .../GenerateUploadUrlMutationBuilder.ts | 4 +- .../client/src/graphQlClient/GraphQlClient.ts | 134 +----------------- packages/client/src/graphQlClient/index.ts | 7 + packages/client/src/index.ts | 3 +- packages/client/src/tsconfig.json | 13 +- packages/graphql-builder/api-extractor.json | 3 + packages/graphql-builder/package.json | 45 ++++++ .../src}/GraphQlQueryPrinter.ts | 6 - .../builder => graphql-builder/src}/index.ts | 1 - .../src}/nodes/GraphQlField.ts | 11 +- .../src}/nodes/GraphQlFragment.ts | 3 - .../src}/nodes/GraphQlFragmentSpread.ts | 3 - .../src}/nodes/GraphQlInlineFragment.ts | 3 - .../src}/nodes/index.ts | 0 packages/graphql-builder/src/tsconfig.json | 6 + .../tests/cases/unit/builder.test.ts | 21 +++ packages/graphql-builder/tests/tsconfig.json | 10 ++ packages/graphql-builder/tsconfig.json | 8 ++ packages/graphql-builder/tsdoc.json | 6 + packages/graphql-builder/vite.config.js | 3 + packages/graphql-client/api-extractor.json | 3 + packages/graphql-client/package.json | 42 ++++++ packages/graphql-client/src/GraphQlClient.ts | 93 ++++++++++++ .../graphql-client/src/GraphQlClientError.ts | 26 ++++ .../src/GraphQlClientRequestOptions.ts | 12 ++ packages/graphql-client/src/index.ts | 3 + packages/graphql-client/src/tsconfig.json | 6 + .../tests/cases/unit/client.test.ts | 8 ++ packages/graphql-client/tests/tsconfig.json | 10 ++ packages/graphql-client/tsconfig.json | 8 ++ packages/graphql-client/tsdoc.json | 6 + packages/graphql-client/vite.config.js | 3 + tsconfig.json | 5 +- yarn.lock | 33 ++++- 81 files changed, 588 insertions(+), 234 deletions(-) rename packages/{client-generator => client-content-generator}/api-extractor.json (100%) rename packages/{client-generator => client-content-generator}/package.json (89%) rename packages/{client-generator => client-content-generator}/src/ContemberClientGenerator.ts (99%) rename packages/{client-generator => client-content-generator}/src/EntityTypeSchemaGenerator.ts (100%) rename packages/{client-generator => client-content-generator}/src/EnumTypeSchemaGenerator.ts (100%) rename packages/{client-generator => client-content-generator}/src/NameSchemaGenerator.ts (98%) rename packages/{client-generator => client-content-generator}/src/generate.ts (100%) rename packages/{client-generator => client-content-generator}/src/index.ts (100%) rename packages/{client-generator => client-content-generator}/src/tsconfig.json (78%) rename packages/{client-generator => client-content-generator}/tests/generateEnittyTypes.test.ts (100%) rename packages/{client-generator => client-content-generator}/tests/generateEnumTypes.test.ts (100%) rename packages/{client-generator => client-content-generator}/tests/schemas.ts (100%) rename packages/{client-generator => client-content-generator}/tests/tsconfig.json (100%) rename packages/{client-generator => client-content-generator}/tsconfig.json (100%) rename packages/{client-generator => client-content-generator}/tsdoc.json (100%) rename packages/{client-generator => client-content-generator}/vite.config.js (84%) create mode 100644 packages/client-content/api-extractor.json create mode 100644 packages/client-content/package.json rename packages/{client/src/content/client => client-content/src}/ContentClient.ts (71%) rename packages/{client/src/content/client => client-content/src}/ContentQueryBuilder.ts (98%) rename packages/{client/src/content/client => client-content/src}/TypedContentQueryBuilder.ts (100%) rename packages/{client/src/content/client => client-content/src}/index.ts (77%) rename packages/{client/src/content/client => client-content/src}/nodes/ContentEntitySelection.ts (99%) rename packages/{client/src/content/client => client-content/src}/nodes/ContentMutation.ts (86%) rename packages/{client/src/content/client => client-content/src}/nodes/ContentQuery.ts (85%) rename packages/{client/src/content/client => client-content/src}/nodes/TypedEntitySelection.ts (100%) rename packages/{client/src/content/client => client-content/src}/nodes/index.ts (100%) create mode 100644 packages/client-content/src/tsconfig.json rename packages/{client/src/content/client => client-content/src}/types/Input.ts (100%) rename packages/{client/src/content/client => client-content/src}/types/Result.ts (100%) rename packages/{client/src/content/client => client-content/src}/types/Schema.ts (100%) rename packages/{client/src/content/client => client-content/src}/types/index.ts (100%) rename packages/{client/src/content/client => client-content/src}/utils/createListArgs.ts (83%) rename packages/{client/src/content/client => client-content/src}/utils/createTypedArgs.ts (82%) rename packages/{client/src/content/client => client-content/src}/utils/mutationFragments.ts (96%) rename packages/{client => client-content}/tests/cases/unit/lib.ts (100%) rename packages/{client => client-content}/tests/cases/unit/mutation.test.ts (100%) rename packages/{client => client-content}/tests/cases/unit/query.test.ts (100%) create mode 100644 packages/client-content/tests/tsconfig.json create mode 100644 packages/client-content/tsconfig.json create mode 100644 packages/client-content/tsdoc.json create mode 100644 packages/client-content/vite.config.js delete mode 100644 packages/client/src/builder/types/json.ts rename packages/client/src/content/{client/utils => }/replaceGraphQlLiteral.ts (86%) create mode 100644 packages/graphql-builder/api-extractor.json create mode 100644 packages/graphql-builder/package.json rename packages/{client/src/builder => graphql-builder/src}/GraphQlQueryPrinter.ts (98%) rename packages/{client/src/builder => graphql-builder/src}/index.ts (68%) rename packages/{client/src/builder => graphql-builder/src}/nodes/GraphQlField.ts (85%) rename packages/{client/src/builder => graphql-builder/src}/nodes/GraphQlFragment.ts (91%) rename packages/{client/src/builder => graphql-builder/src}/nodes/GraphQlFragmentSpread.ts (81%) rename packages/{client/src/builder => graphql-builder/src}/nodes/GraphQlInlineFragment.ts (90%) rename packages/{client/src/builder => graphql-builder/src}/nodes/index.ts (100%) create mode 100644 packages/graphql-builder/src/tsconfig.json create mode 100644 packages/graphql-builder/tests/cases/unit/builder.test.ts create mode 100644 packages/graphql-builder/tests/tsconfig.json create mode 100644 packages/graphql-builder/tsconfig.json create mode 100644 packages/graphql-builder/tsdoc.json create mode 100644 packages/graphql-builder/vite.config.js create mode 100644 packages/graphql-client/api-extractor.json create mode 100644 packages/graphql-client/package.json create mode 100644 packages/graphql-client/src/GraphQlClient.ts create mode 100644 packages/graphql-client/src/GraphQlClientError.ts create mode 100644 packages/graphql-client/src/GraphQlClientRequestOptions.ts create mode 100644 packages/graphql-client/src/index.ts create mode 100644 packages/graphql-client/src/tsconfig.json create mode 100644 packages/graphql-client/tests/cases/unit/client.test.ts create mode 100644 packages/graphql-client/tests/tsconfig.json create mode 100644 packages/graphql-client/tsconfig.json create mode 100644 packages/graphql-client/tsdoc.json create mode 100644 packages/graphql-client/vite.config.js diff --git a/build/packageList.js b/build/packageList.js index e4cd791f09..579b69a125 100644 --- a/build/packageList.js +++ b/build/packageList.js @@ -21,7 +21,10 @@ export const list = { 'binding', 'brand', 'client', - 'client-generator', + 'client-content', + 'client-content-generator', + 'graphql-builder', + 'graphql-client', 'interface-tester', 'layout', 'react-auto', diff --git a/packages/client-generator/api-extractor.json b/packages/client-content-generator/api-extractor.json similarity index 100% rename from packages/client-generator/api-extractor.json rename to packages/client-content-generator/api-extractor.json diff --git a/packages/client-generator/package.json b/packages/client-content-generator/package.json similarity index 89% rename from packages/client-generator/package.json rename to packages/client-content-generator/package.json index ba46713bb9..b839f1fb27 100644 --- a/packages/client-generator/package.json +++ b/packages/client-content-generator/package.json @@ -1,5 +1,5 @@ { - "name": "@contember/client-generator", + "name": "@contember/client-content-generator", "license": "Apache-2.0", "version": "0.0.0", "private": true, @@ -39,7 +39,7 @@ "test": "vitest" }, "dependencies": { - "@contember/client": "workspace:*", + "@contember/client-content": "workspace:*", "@contember/schema": "^1.3.6", "@contember/schema-utils": "^1.3.6" }, @@ -50,6 +50,6 @@ "repository": { "type": "git", "url": "https://github.com/contember/interface.git", - "directory": "packages/client-generator" + "directory": "packages/client-content-generator" } } diff --git a/packages/client-generator/src/ContemberClientGenerator.ts b/packages/client-content-generator/src/ContemberClientGenerator.ts similarity index 99% rename from packages/client-generator/src/ContemberClientGenerator.ts rename to packages/client-content-generator/src/ContemberClientGenerator.ts index b9c699661b..e786ab79e8 100644 --- a/packages/client-generator/src/ContemberClientGenerator.ts +++ b/packages/client-content-generator/src/ContemberClientGenerator.ts @@ -17,7 +17,7 @@ export class ContemberClientGenerator { const enumTypeSchema = this.enumTypeSchemaGenerator.generate(model) const entityTypeSchema = this.entityTypeSchemaGenerator.generate(model) - const namesCode = `import { SchemaNames } from '@contember/client' + const namesCode = `import { SchemaNames } from '@contember/client-content' export const ContemberClientNames: SchemaNames = ` + JSON.stringify(nameSchema, null, 2) const indexCode = ` diff --git a/packages/client-generator/src/EntityTypeSchemaGenerator.ts b/packages/client-content-generator/src/EntityTypeSchemaGenerator.ts similarity index 100% rename from packages/client-generator/src/EntityTypeSchemaGenerator.ts rename to packages/client-content-generator/src/EntityTypeSchemaGenerator.ts diff --git a/packages/client-generator/src/EnumTypeSchemaGenerator.ts b/packages/client-content-generator/src/EnumTypeSchemaGenerator.ts similarity index 100% rename from packages/client-generator/src/EnumTypeSchemaGenerator.ts rename to packages/client-content-generator/src/EnumTypeSchemaGenerator.ts diff --git a/packages/client-generator/src/NameSchemaGenerator.ts b/packages/client-content-generator/src/NameSchemaGenerator.ts similarity index 98% rename from packages/client-generator/src/NameSchemaGenerator.ts rename to packages/client-content-generator/src/NameSchemaGenerator.ts index 5cfa9022a2..1992a22eb1 100644 --- a/packages/client-generator/src/NameSchemaGenerator.ts +++ b/packages/client-content-generator/src/NameSchemaGenerator.ts @@ -1,4 +1,4 @@ -import { SchemaNames, SchemaEntityNames } from '@contember/client' +import { SchemaNames, SchemaEntityNames } from '@contember/client-content' import { Model } from '@contember/schema' import { acceptEveryFieldVisitor } from '@contember/schema-utils' diff --git a/packages/client-generator/src/generate.ts b/packages/client-content-generator/src/generate.ts similarity index 100% rename from packages/client-generator/src/generate.ts rename to packages/client-content-generator/src/generate.ts diff --git a/packages/client-generator/src/index.ts b/packages/client-content-generator/src/index.ts similarity index 100% rename from packages/client-generator/src/index.ts rename to packages/client-content-generator/src/index.ts diff --git a/packages/client-generator/src/tsconfig.json b/packages/client-content-generator/src/tsconfig.json similarity index 78% rename from packages/client-generator/src/tsconfig.json rename to packages/client-content-generator/src/tsconfig.json index d766dd72a3..f95b08d0bc 100644 --- a/packages/client-generator/src/tsconfig.json +++ b/packages/client-content-generator/src/tsconfig.json @@ -5,6 +5,6 @@ "types": ["node"] }, "references": [ - { "path": "../../client/src" } + { "path": "../../client-content/src" } ] } diff --git a/packages/client-generator/tests/generateEnittyTypes.test.ts b/packages/client-content-generator/tests/generateEnittyTypes.test.ts similarity index 100% rename from packages/client-generator/tests/generateEnittyTypes.test.ts rename to packages/client-content-generator/tests/generateEnittyTypes.test.ts diff --git a/packages/client-generator/tests/generateEnumTypes.test.ts b/packages/client-content-generator/tests/generateEnumTypes.test.ts similarity index 100% rename from packages/client-generator/tests/generateEnumTypes.test.ts rename to packages/client-content-generator/tests/generateEnumTypes.test.ts diff --git a/packages/client-generator/tests/schemas.ts b/packages/client-content-generator/tests/schemas.ts similarity index 100% rename from packages/client-generator/tests/schemas.ts rename to packages/client-content-generator/tests/schemas.ts diff --git a/packages/client-generator/tests/tsconfig.json b/packages/client-content-generator/tests/tsconfig.json similarity index 100% rename from packages/client-generator/tests/tsconfig.json rename to packages/client-content-generator/tests/tsconfig.json diff --git a/packages/client-generator/tsconfig.json b/packages/client-content-generator/tsconfig.json similarity index 100% rename from packages/client-generator/tsconfig.json rename to packages/client-content-generator/tsconfig.json diff --git a/packages/client-generator/tsdoc.json b/packages/client-content-generator/tsdoc.json similarity index 100% rename from packages/client-generator/tsdoc.json rename to packages/client-content-generator/tsdoc.json diff --git a/packages/client-generator/vite.config.js b/packages/client-content-generator/vite.config.js similarity index 84% rename from packages/client-generator/vite.config.js rename to packages/client-content-generator/vite.config.js index 1e8e1422a6..2e9a038aa0 100644 --- a/packages/client-generator/vite.config.js +++ b/packages/client-content-generator/vite.config.js @@ -2,7 +2,7 @@ import { createViteConfig } from '../../build/createViteConfig.js' import { defineConfig } from 'vite' export default defineConfig(args => { - const config = createViteConfig('client-generator')(args) + const config = createViteConfig('client-content-generator')(args) return { ...config, diff --git a/packages/client-content/api-extractor.json b/packages/client-content/api-extractor.json new file mode 100644 index 0000000000..66c17dd719 --- /dev/null +++ b/packages/client-content/api-extractor.json @@ -0,0 +1,3 @@ +{ + "extends": "../../build/api-extractor.json" +} diff --git a/packages/client-content/package.json b/packages/client-content/package.json new file mode 100644 index 0000000000..a0f8d68b6d --- /dev/null +++ b/packages/client-content/package.json @@ -0,0 +1,47 @@ +{ + "name": "@contember/client-content", + "license": "Apache-2.0", + "version": "0.0.0", + "main": "./dist/production/index.js", + "exports": { + ".": { + "import": { + "types": "./dist/types/index.d.ts", + "development": "./dist/development/index.js", + "production": "./dist/production/index.js", + "default": "./dist/production/index.js" + }, + "require": { + "types": "./dist/types/index.d.ts", + "development": "./dist/development/index.cjs", + "production": "./dist/production/index.cjs", + "default": "./dist/production/index.cjs" + } + } + }, + "files": [ + "dist/", + "src/" + ], + "typings": "./dist/types/index.d.ts", + "type": "module", + "sideEffects": false, + "scripts": { + "build": "yarn build:js:dev && yarn build:js:prod", + "build:js:dev": "NODE_ENV=development vite build --mode development", + "build:js:prod": "vite build --mode production", + "ae:build": "api-extractor run --local", + "ae:test": "api-extractor run", + "test": "vitest" + }, + "dependencies": { + "@contember/graphql-builder": "workspace:*", + "@contember/graphql-client": "workspace:*", + "@contember/schema": "^1.3.6" + }, + "repository": { + "type": "git", + "url": "https://github.com/contember/interface.git", + "directory": "packages/client-content" + } +} diff --git a/packages/client/src/content/client/ContentClient.ts b/packages/client-content/src/ContentClient.ts similarity index 71% rename from packages/client/src/content/client/ContentClient.ts rename to packages/client-content/src/ContentClient.ts index 734d757bf5..1ab3be13a2 100644 --- a/packages/client/src/content/client/ContentClient.ts +++ b/packages/client-content/src/ContentClient.ts @@ -1,25 +1,18 @@ import { ContentMutation, ContentQuery } from './nodes' import { mutationFragments } from './utils/mutationFragments' import { MutationResult, TransactionResult } from './types' -import { GraphQlClientRequestOptions } from '../../graphQlClient' -import { GraphQlField, GraphQlFragmentSpread, GraphQlQueryPrinter, GraphQlSelectionSetItem } from '../../builder' +import { GraphQlClientRequestOptions } from '@contember/graphql-client' +import { GraphQlField, GraphQlFragmentSpread, GraphQlQueryPrinter, GraphQlSelectionSetItem } from '@contember/graphql-builder' -export type CommonMutationOptions = { +export type MutationWithTransactionOptions = { deferForeignKeyConstraints?: boolean deferUniqueConstraints?: boolean + transaction?: true } -export type MutationWithTransactionOptions = - & CommonMutationOptions - & { - transaction?: true - } - -export type MutationWithoutTransactionOptions = - & CommonMutationOptions - & { - transaction: false - } +export type MutationWithoutTransactionOptions = { + transaction: false +} export type QueryExecutorOptions = { variables?: Record @@ -43,19 +36,26 @@ export class ContentClient { public async query(query: ContentQuery, options?: QueryExecutorOptions): Promise public async query>(queries: {[K in keyof Values]: ContentQuery}, options?: QueryExecutorOptions): Promise public async query(queries: Record> | ContentQuery, options?: QueryExecutorOptions): Promise { + const { query, variables } = this.prepareQuery(queries) + const result = await this.executor(query, { variables, ...options }) + return this.processQueryResult(queries, result) + } + + private prepareQuery(queries: Record> | ContentQuery) { const printer = new GraphQlQueryPrinter() const selectionSet = queries instanceof ContentQuery - ? [new GraphQlField('value', queries.queryFieldName, queries.args, queries.nodeSelection)] - : Object.entries(queries).map(([alias, query]) => new GraphQlField(alias, query.queryFieldName, query.args, query.nodeSelection)) + ? [new GraphQlField('value', queries.queryFieldName, queries.args, queries.nodeSelection)] + : Object.entries(queries).map(([alias, query]) => new GraphQlField(alias, query.queryFieldName, query.args, query.nodeSelection)) - const { query, variables } = printer.printDocument('query', selectionSet, {}) - const result = await this.executor(query, { variables, ...options }) + return printer.printDocument('query', selectionSet, {}) + } + private processQueryResult(queries: Record> | ContentQuery, result: any) { if (queries instanceof ContentQuery) { - return queries.parse((result as any).value) + return queries.parse(result.value) } - return Object.fromEntries(Object.entries(queries).map(([alias, query]) => [alias, query.parse((result as any)[alias])])) + return Object.fromEntries(Object.entries(queries).map(([alias, query]) => [alias, query.parse(result[alias])])) } public async mutate( @@ -103,36 +103,18 @@ export class ContentClient { }>> public async mutate(input: Record | ContentQuery> | ContentMutation | ContentMutation[], options?: & QueryExecutorOptions & (MutationWithTransactionOptions | MutationWithoutTransactionOptions)): Promise { - const printer = new GraphQlQueryPrinter() - const fields: GraphQlField[] = [] - let transaction = options?.transaction ?? true - if (input instanceof ContentMutation) { - fields.push(this.createMutationField('mut', input)) - } else if (Array.isArray(input)) { - let i = 0 - for (const mutation of input) { - fields.push(this.createMutationField('mut_' + i++, mutation)) - } - } else { - for (const [alias, mutation] of Object.entries(input)) { - if (mutation instanceof ContentQuery) { - fields.push(new GraphQlField(alias, 'query', {}, [ - new GraphQlField('value', mutation.queryFieldName, mutation.args, mutation.nodeSelection), - ])) - } else { - fields.push(this.createMutationField(alias, mutation)) - } - } - } - - const selectionSet = transaction ? [new GraphQlField(null, 'transaction', { - ...(options?.deferForeignKeyConstraints ? { deferForeignKeyConstraints: { graphQlType: 'Boolean', value: options?.deferForeignKeyConstraints } } : {}), - ...(options?.deferUniqueConstraints ? { deferUniqueConstraints: { graphQlType: 'Boolean', value: options?.deferUniqueConstraints } } : {}), - }, fields)] : fields - - const { query, variables } = printer.printDocument('mutation', selectionSet, mutationFragments) + const { query, variables } = this.prepareMutation(input, options) const result = await this.executor(query, { variables, ...options }) + return this.processMutationResponse(result, input, options) + } + + private processMutationResponse( + result: any, + input: Record | ContentQuery> | ContentMutation | ContentMutation[], + options?: & QueryExecutorOptions & (MutationWithTransactionOptions | MutationWithoutTransactionOptions), + ) { + const transaction = options?.transaction ?? true const innerResult = transaction ? (result as any).transaction : result let value: any @@ -164,6 +146,56 @@ export class ContentClient { } } + private prepareMutation( + input: Record | ContentQuery> | ContentMutation | ContentMutation[], + options?: & QueryExecutorOptions & (MutationWithTransactionOptions | MutationWithoutTransactionOptions), + ) { + const printer = new GraphQlQueryPrinter() + + const fields: GraphQlField[] = [] + if (input instanceof ContentMutation) { + fields.push(this.createMutationField('mut', input)) + } else if (Array.isArray(input)) { + let i = 0 + for (const mutation of input) { + fields.push(this.createMutationField('mut_' + i++, mutation)) + } + } else { + for (const [alias, mutation] of Object.entries(input)) { + if (mutation instanceof ContentQuery) { + fields.push(new GraphQlField(alias, 'query', {}, [ + new GraphQlField('value', mutation.queryFieldName, mutation.args, mutation.nodeSelection), + ])) + } else { + fields.push(this.createMutationField(alias, mutation)) + } + } + } + + const selectionSet = options?.transaction !== false + ? [this.wrapIntoTransaction(fields, options)] + : fields + + return printer.printDocument('mutation', selectionSet, mutationFragments) + } + + private wrapIntoTransaction(fields: GraphQlField[], options?: MutationWithTransactionOptions) { + return new GraphQlField(null, 'transaction', { + ...(options?.deferForeignKeyConstraints ? { + deferForeignKeyConstraints: { + graphQlType: 'Boolean', + value: options?.deferForeignKeyConstraints, + }, + } : {}), + ...(options?.deferUniqueConstraints ? { + deferUniqueConstraints: { + graphQlType: 'Boolean', + value: options?.deferUniqueConstraints, + }, + } : {}), + }, fields) + } + private createMutationField(alias: string, mutation: ContentMutation): GraphQlField { const items: GraphQlSelectionSetItem[] = [ new GraphQlField(null, 'ok'), diff --git a/packages/client/src/content/client/ContentQueryBuilder.ts b/packages/client-content/src/ContentQueryBuilder.ts similarity index 98% rename from packages/client/src/content/client/ContentQueryBuilder.ts rename to packages/client-content/src/ContentQueryBuilder.ts index 786eb554db..63b9fc9405 100644 --- a/packages/client/src/content/client/ContentQueryBuilder.ts +++ b/packages/client-content/src/ContentQueryBuilder.ts @@ -9,7 +9,7 @@ import { import { createListArgs } from './utils/createListArgs' import { createTypedArgs } from './utils/createTypedArgs' import { Input } from '@contember/schema' -import { GraphQlField, GraphQlSelectionSet } from '../../builder' +import { GraphQlField, GraphQlSelectionSet } from '@contember/graphql-builder' export type EntitySelectionOrCallback = | ContentEntitySelection diff --git a/packages/client/src/content/client/TypedContentQueryBuilder.ts b/packages/client-content/src/TypedContentQueryBuilder.ts similarity index 100% rename from packages/client/src/content/client/TypedContentQueryBuilder.ts rename to packages/client-content/src/TypedContentQueryBuilder.ts diff --git a/packages/client/src/content/client/index.ts b/packages/client-content/src/index.ts similarity index 77% rename from packages/client/src/content/client/index.ts rename to packages/client-content/src/index.ts index a468828c3b..baa9bed567 100644 --- a/packages/client/src/content/client/index.ts +++ b/packages/client-content/src/index.ts @@ -3,4 +3,3 @@ export * from './TypedContentQueryBuilder' export * from './ContentQueryBuilder' export * from './nodes' export * from './types' -export * from './utils/replaceGraphQlLiteral' diff --git a/packages/client/src/content/client/nodes/ContentEntitySelection.ts b/packages/client-content/src/nodes/ContentEntitySelection.ts similarity index 99% rename from packages/client/src/content/client/nodes/ContentEntitySelection.ts rename to packages/client-content/src/nodes/ContentEntitySelection.ts index cfe0bc300b..3e0aabc408 100644 --- a/packages/client/src/content/client/nodes/ContentEntitySelection.ts +++ b/packages/client-content/src/nodes/ContentEntitySelection.ts @@ -1,7 +1,7 @@ import { createListArgs } from '../utils/createListArgs' import { ContentClientInput, SchemaEntityNames, SchemaNames } from '../types' import { Input } from '@contember/schema' -import { GraphQlField, GraphQlFieldTypedArgs, GraphQlSelectionSet } from '../../../builder' +import { GraphQlField, GraphQlFieldTypedArgs, GraphQlSelectionSet } from '@contember/graphql-builder' /** * @internal diff --git a/packages/client/src/content/client/nodes/ContentMutation.ts b/packages/client-content/src/nodes/ContentMutation.ts similarity index 86% rename from packages/client/src/content/client/nodes/ContentMutation.ts rename to packages/client-content/src/nodes/ContentMutation.ts index bb2f7e27aa..ee9620b999 100644 --- a/packages/client/src/content/client/nodes/ContentMutation.ts +++ b/packages/client-content/src/nodes/ContentMutation.ts @@ -1,5 +1,5 @@ import { SchemaEntityNames } from '../types' -import { GraphQlFieldTypedArgs, GraphQlSelectionSet } from '../../../builder' +import { GraphQlFieldTypedArgs, GraphQlSelectionSet } from '@contember/graphql-builder' export class ContentMutation { public readonly type = 'mutation' diff --git a/packages/client/src/content/client/nodes/ContentQuery.ts b/packages/client-content/src/nodes/ContentQuery.ts similarity index 85% rename from packages/client/src/content/client/nodes/ContentQuery.ts rename to packages/client-content/src/nodes/ContentQuery.ts index 883a4cad54..964f1f72df 100644 --- a/packages/client/src/content/client/nodes/ContentQuery.ts +++ b/packages/client-content/src/nodes/ContentQuery.ts @@ -1,5 +1,5 @@ import { SchemaEntityNames } from '../types/Schema' -import { GraphQlFieldTypedArgs, GraphQlSelectionSet } from '../../../builder' +import { GraphQlFieldTypedArgs, GraphQlSelectionSet } from '@contember/graphql-builder' export class ContentQuery { diff --git a/packages/client/src/content/client/nodes/TypedEntitySelection.ts b/packages/client-content/src/nodes/TypedEntitySelection.ts similarity index 100% rename from packages/client/src/content/client/nodes/TypedEntitySelection.ts rename to packages/client-content/src/nodes/TypedEntitySelection.ts diff --git a/packages/client/src/content/client/nodes/index.ts b/packages/client-content/src/nodes/index.ts similarity index 100% rename from packages/client/src/content/client/nodes/index.ts rename to packages/client-content/src/nodes/index.ts diff --git a/packages/client-content/src/tsconfig.json b/packages/client-content/src/tsconfig.json new file mode 100644 index 0000000000..b92922336e --- /dev/null +++ b/packages/client-content/src/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../tsconfig.settings.json", + "compilerOptions": { + "outDir": "../dist/types" + }, + "references": [ + { + "path": "../../graphql-builder/src" + }, + { + "path": "../../graphql-client/src" + } + ] +} diff --git a/packages/client/src/content/client/types/Input.ts b/packages/client-content/src/types/Input.ts similarity index 100% rename from packages/client/src/content/client/types/Input.ts rename to packages/client-content/src/types/Input.ts diff --git a/packages/client/src/content/client/types/Result.ts b/packages/client-content/src/types/Result.ts similarity index 100% rename from packages/client/src/content/client/types/Result.ts rename to packages/client-content/src/types/Result.ts diff --git a/packages/client/src/content/client/types/Schema.ts b/packages/client-content/src/types/Schema.ts similarity index 100% rename from packages/client/src/content/client/types/Schema.ts rename to packages/client-content/src/types/Schema.ts diff --git a/packages/client/src/content/client/types/index.ts b/packages/client-content/src/types/index.ts similarity index 100% rename from packages/client/src/content/client/types/index.ts rename to packages/client-content/src/types/index.ts diff --git a/packages/client/src/content/client/utils/createListArgs.ts b/packages/client-content/src/utils/createListArgs.ts similarity index 83% rename from packages/client/src/content/client/utils/createListArgs.ts rename to packages/client-content/src/utils/createListArgs.ts index cb72da2996..62468bcb17 100644 --- a/packages/client/src/content/client/utils/createListArgs.ts +++ b/packages/client-content/src/utils/createListArgs.ts @@ -1,5 +1,5 @@ -import { SchemaEntityNames } from '../types/Schema' -import { GraphQlFieldTypedArgs } from '../../../builder' +import { SchemaEntityNames } from '../types' +import { GraphQlFieldTypedArgs } from '@contember/graphql-builder' export const createListArgs = (entity: SchemaEntityNames, args: { filter?: any, orderBy?: any, limit?: number, offset?: number }, type: 'list' | 'paginate' = 'list'): GraphQlFieldTypedArgs => { return { diff --git a/packages/client/src/content/client/utils/createTypedArgs.ts b/packages/client-content/src/utils/createTypedArgs.ts similarity index 82% rename from packages/client/src/content/client/utils/createTypedArgs.ts rename to packages/client-content/src/utils/createTypedArgs.ts index 394e169e62..6661843f6e 100644 --- a/packages/client/src/content/client/utils/createTypedArgs.ts +++ b/packages/client-content/src/utils/createTypedArgs.ts @@ -1,4 +1,4 @@ -import { GraphQlFieldTypedArgs } from '../../../builder' +import { GraphQlFieldTypedArgs } from '@contember/graphql-builder' export const createTypedArgs = >( args: TArgs, diff --git a/packages/client/src/content/client/utils/mutationFragments.ts b/packages/client-content/src/utils/mutationFragments.ts similarity index 96% rename from packages/client/src/content/client/utils/mutationFragments.ts rename to packages/client-content/src/utils/mutationFragments.ts index 97765c7c70..effe887b0f 100644 --- a/packages/client/src/content/client/utils/mutationFragments.ts +++ b/packages/client-content/src/utils/mutationFragments.ts @@ -1,4 +1,4 @@ -import { GraphQlField, GraphQlFragment, GraphQlFragmentSpread, GraphQlInlineFragment } from '../../../builder' +import { GraphQlField, GraphQlFragment, GraphQlFragmentSpread, GraphQlInlineFragment } from '@contember/graphql-builder' export const mutationFragments: Record = { MutationError: new GraphQlFragment('MutationError', '_MutationError', [ diff --git a/packages/client/tests/cases/unit/lib.ts b/packages/client-content/tests/cases/unit/lib.ts similarity index 100% rename from packages/client/tests/cases/unit/lib.ts rename to packages/client-content/tests/cases/unit/lib.ts diff --git a/packages/client/tests/cases/unit/mutation.test.ts b/packages/client-content/tests/cases/unit/mutation.test.ts similarity index 100% rename from packages/client/tests/cases/unit/mutation.test.ts rename to packages/client-content/tests/cases/unit/mutation.test.ts diff --git a/packages/client/tests/cases/unit/query.test.ts b/packages/client-content/tests/cases/unit/query.test.ts similarity index 100% rename from packages/client/tests/cases/unit/query.test.ts rename to packages/client-content/tests/cases/unit/query.test.ts diff --git a/packages/client-content/tests/tsconfig.json b/packages/client-content/tests/tsconfig.json new file mode 100644 index 0000000000..833dd61fbe --- /dev/null +++ b/packages/client-content/tests/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.settings.json", + "compilerOptions": { + "noEmit": true, + "emitDeclarationOnly": false + }, + "references": [ + { "path": "../src" } + ] +} diff --git a/packages/client-content/tsconfig.json b/packages/client-content/tsconfig.json new file mode 100644 index 0000000000..e3a586f5ef --- /dev/null +++ b/packages/client-content/tsconfig.json @@ -0,0 +1,8 @@ +{ + "files": [], + "include": [], + "references": [ + { "path": "./src" }, + { "path": "./tests" } + ] +} diff --git a/packages/client-content/tsdoc.json b/packages/client-content/tsdoc.json new file mode 100644 index 0000000000..a46f62a20a --- /dev/null +++ b/packages/client-content/tsdoc.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json", + "extends": [ + "../../tsdoc.json" + ] +} diff --git a/packages/client-content/vite.config.js b/packages/client-content/vite.config.js new file mode 100644 index 0000000000..9f0f501374 --- /dev/null +++ b/packages/client-content/vite.config.js @@ -0,0 +1,3 @@ +import { createViteConfig } from './../../build/createViteConfig.js' + +export default createViteConfig('client') diff --git a/packages/client/package.json b/packages/client/package.json index 1463de1c9d..f0434acf89 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -35,7 +35,15 @@ "test": "vitest" }, "dependencies": { + "@contember/client-content": "workspace:*", + "@contember/graphql-builder": "workspace:*", + "@contember/graphql-client": "workspace:*", "@contember/schema": "^1.3.6", "p-limit": "^4.0.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/contember/interface.git", + "directory": "packages/client" } } diff --git a/packages/client/src/builder/types/json.ts b/packages/client/src/builder/types/json.ts deleted file mode 100644 index d0937c0c06..0000000000 --- a/packages/client/src/builder/types/json.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type JSONPrimitive = string | number | boolean | null | E -export type JSONValue = JSONPrimitive | JSONObject | JSONArray -export type JSONObject = { readonly [K in string]?: JSONValue } -export type JSONArray = readonly JSONValue[] diff --git a/packages/client/src/content/index.ts b/packages/client/src/content/index.ts index 54034eaa4d..346c38237f 100644 --- a/packages/client/src/content/index.ts +++ b/packages/client/src/content/index.ts @@ -1,4 +1,4 @@ export * from './upload' export * from './params' -export * from './client' export * from './formatContentApiRelativeUrl' +export * from './replaceGraphQlLiteral' diff --git a/packages/client/src/content/client/utils/replaceGraphQlLiteral.ts b/packages/client/src/content/replaceGraphQlLiteral.ts similarity index 86% rename from packages/client/src/content/client/utils/replaceGraphQlLiteral.ts rename to packages/client/src/content/replaceGraphQlLiteral.ts index b919a46ffe..a58c550f02 100644 --- a/packages/client/src/content/client/utils/replaceGraphQlLiteral.ts +++ b/packages/client/src/content/replaceGraphQlLiteral.ts @@ -1,9 +1,8 @@ -import { GraphQlLiteral } from '../../../graphQlBuilder' -import { JSONPrimitive } from '../../../builder' +import { GraphQlLiteral } from '../graphQlBuilder' export type ReplaceGraphQlLiteral = T extends GraphQlLiteral ? Value - : T extends JSONPrimitive + : T extends string | number | boolean | null ? T // Keep primitives as is : T extends {} ? { [K in keyof T]: ReplaceGraphQlLiteral } // Recursively apply to objects diff --git a/packages/client/src/content/upload/GenerateUploadUrlMutationBuilder.ts b/packages/client/src/content/upload/GenerateUploadUrlMutationBuilder.ts index 7d8b405715..a78a4cd5fa 100644 --- a/packages/client/src/content/upload/GenerateUploadUrlMutationBuilder.ts +++ b/packages/client/src/content/upload/GenerateUploadUrlMutationBuilder.ts @@ -1,7 +1,7 @@ import { GraphQlLiteral } from '../../graphQlBuilder' -import { GraphQlField, GraphQlPrintResult, GraphQlQueryPrinter, GraphQlSelectionSetItem } from '../../builder' -import { replaceGraphQlLiteral } from '../client' +import { GraphQlField, GraphQlPrintResult, GraphQlQueryPrinter, GraphQlSelectionSetItem } from '@contember/graphql-builder' import { GraphQlBuilder } from '../../index' +import { replaceGraphQlLiteral } from '../replaceGraphQlLiteral' class GenerateUploadUrlMutationBuilder { private static generateUploadUrlFields = [ diff --git a/packages/client/src/graphQlClient/GraphQlClient.ts b/packages/client/src/graphQlClient/GraphQlClient.ts index 7978ad34f6..d8cd6fd339 100644 --- a/packages/client/src/graphQlClient/GraphQlClient.ts +++ b/packages/client/src/graphQlClient/GraphQlClient.ts @@ -1,97 +1,26 @@ -export interface GraphQlClientRequestOptions { - variables?: GraphQlClientVariables - apiToken?: string - signal?: AbortSignal - headers?: Record - onResponse?: (response: Response) => void - onData?: (json: unknown) => void +import { GraphQlClient as BaseGraphQLClient, GraphQlClientRequestOptions as BaseGraphQlClientRequestOptions } from '@contember/graphql-client' +export interface GraphQlClientRequestOptions extends BaseGraphQlClientRequestOptions { /** * @deprecated use apiToken */ apiTokenOverride?: string } -export interface GraphQlClientVariables { - [name: string]: any -} - export type GraphQlClientFailedRequestMetadata = Pick & { responseText: string } -export class GraphQlClient { - constructor(public readonly apiUrl: string, private readonly apiToken?: string) { } - - async execute(query: string, options: GraphQlClientRequestOptions = {}): Promise { - let body: string | null = null - let response: Response | null = null - const createError = (type: GraphqlErrorType, errors?: any[], cause?: unknown) => { - const request = { - url: this.apiUrl, - query, - variables: options.variables ?? {}, - } - - const details = `HTTP response: ${response ? (response.status + ' ' + response.statusText) : ''} -HTTP body: -${body !== null ? body : ''} - -GraphQL query: -${query}` - - return new GraphQlClientError(`GraphQL request failed: ${type}`, type, request, response ?? undefined, errors, details, cause) - } - try { - response = await this.doExecute(query, options) - } catch (e) { - const aborted = typeof e === 'object' && e !== null && (e as { name?: unknown }).name === 'AbortError' - throw createError(aborted ? 'aborted' : 'network error', undefined, e) - } - - options?.onResponse?.(response) - - body = await response.text() - - let data: any - try { - data = JSON.parse(body) - } catch (e) { - throw createError('invalid response body', undefined, e) - } - options?.onData?.(data) - - if (response.status === 401) { - throw createError('unauthorized') - } - if (response.status === 403) { - throw createError('forbidden') - } - if (response.status >= 400 && response.status < 500) { - throw createError('bad request', data.errors) - } - if (response.status >= 500) { - throw createError('server error') - } - if (!(typeof data === 'object') || data === null) { - throw createError('invalid response body') - } - if ('errors' in data) { - throw createError('response errors', data.errors) - } - if (!('data' in data)) { - throw createError('invalid response body') - } - - return data.data - } - +export class GraphQlClient extends BaseGraphQLClient { /** * @deprecated use execute */ async sendRequest(query: string, options: GraphQlClientRequestOptions = {}): Promise { console.debug(query) - const response = await this.doExecute(query, options) + const response = await this.doExecute(query, { + ...options, + apiToken: options.apiTokenOverride ?? options.apiToken, + }) if (response.ok) { // It may still have errors (e.g. unfilled fields) but as far as the request goes, it is ok. @@ -107,53 +36,4 @@ ${query}` return Promise.reject(failedRequest) } - - private async doExecute( - query: string, - { apiToken, apiTokenOverride, signal, variables, headers }: GraphQlClientRequestOptions = {}, - ): Promise { - const resolvedHeaders: Record = { - 'Content-Type': 'application/json', - ...headers, - } - const resolvedToken = apiToken ?? apiTokenOverride ?? this.apiToken - - if (resolvedToken !== undefined) { - resolvedHeaders['Authorization'] = `Bearer ${resolvedToken}` - } - - return await fetch(this.apiUrl, { - method: 'POST', - headers: resolvedHeaders, - signal, - body: JSON.stringify({ query, variables }), - }) - } -} - -export type GraphqlErrorRequest = { url: string, query: string, variables: Record }; - -export type GraphqlErrorType = - | 'aborted' - | 'network error' - | 'invalid response body' - | 'bad request' - | 'unauthorized' - | 'forbidden' - | 'server error' - | 'response errors' - -export class GraphQlClientError extends Error { - constructor( - message: string, - public readonly type: GraphqlErrorType, - public readonly request: GraphqlErrorRequest, - public readonly response?: Response, - public readonly errors?: readonly any[], - public readonly details?: string, - cause?: unknown, - ) { - super(message) - this.cause = cause - } } diff --git a/packages/client/src/graphQlClient/index.ts b/packages/client/src/graphQlClient/index.ts index ee7d2ab590..e687c184d7 100644 --- a/packages/client/src/graphQlClient/index.ts +++ b/packages/client/src/graphQlClient/index.ts @@ -1 +1,8 @@ export * from './GraphQlClient' +export type { + GraphQlClientVariables, + GraphQlErrorType, + GraphQlErrorRequest, +} from '@contember/graphql-client' + +export { GraphQlClientError } from '@contember/graphql-client' diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index 5b0dc50496..3c0406dc25 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -13,10 +13,11 @@ export namespace CrudQueryBuilder { export type OrderDirection = CrudQueryBuilderTmp.OrderDirection } -export * from './builder' export * from './content' export * from './graphQlClient' export * from './system' export * from './tenant' +export * from '@contember/client-content' + export type { Input, Value, Result, Writable } from '@contember/schema' diff --git a/packages/client/src/tsconfig.json b/packages/client/src/tsconfig.json index 8542cac73a..6dd7878e93 100644 --- a/packages/client/src/tsconfig.json +++ b/packages/client/src/tsconfig.json @@ -2,5 +2,16 @@ "extends": "../../../tsconfig.settings.json", "compilerOptions": { "outDir": "../dist/types" - } + }, + "references": [ + { + "path": "../../client-content/src" + }, + { + "path": "../../graphql-builder/src" + }, + { + "path": "../../graphql-client/src" + } + ] } diff --git a/packages/graphql-builder/api-extractor.json b/packages/graphql-builder/api-extractor.json new file mode 100644 index 0000000000..66c17dd719 --- /dev/null +++ b/packages/graphql-builder/api-extractor.json @@ -0,0 +1,3 @@ +{ + "extends": "../../build/api-extractor.json" +} diff --git a/packages/graphql-builder/package.json b/packages/graphql-builder/package.json new file mode 100644 index 0000000000..2e994a99f9 --- /dev/null +++ b/packages/graphql-builder/package.json @@ -0,0 +1,45 @@ +{ + "name": "@contember/graphql-builder", + "license": "Apache-2.0", + "version": "0.0.0", + "main": "./dist/production/index.js", + "exports": { + ".": { + "import": { + "types": "./dist/types/index.d.ts", + "development": "./dist/development/index.js", + "production": "./dist/production/index.js", + "default": "./dist/production/index.js" + }, + "require": { + "types": "./dist/types/index.d.ts", + "development": "./dist/development/index.cjs", + "production": "./dist/production/index.cjs", + "default": "./dist/production/index.cjs" + } + } + }, + "files": [ + "dist/", + "src/" + ], + "typings": "./dist/types/index.d.ts", + "type": "module", + "sideEffects": false, + "scripts": { + "build": "yarn build:js:dev && yarn build:js:prod", + "build:js:dev": "NODE_ENV=development vite build --mode development", + "build:js:prod": "vite build --mode production", + "ae:build": "api-extractor run --local", + "ae:test": "api-extractor run", + "test": "vitest" + }, + "dependencies": { + "@contember/schema": "^1.3.6" + }, + "repository": { + "type": "git", + "url": "https://github.com/contember/interface.git", + "directory": "packages/graphql-builder" + } +} diff --git a/packages/client/src/builder/GraphQlQueryPrinter.ts b/packages/graphql-builder/src/GraphQlQueryPrinter.ts similarity index 98% rename from packages/client/src/builder/GraphQlQueryPrinter.ts rename to packages/graphql-builder/src/GraphQlQueryPrinter.ts index 38c2ff5820..ca69a2c543 100644 --- a/packages/client/src/builder/GraphQlQueryPrinter.ts +++ b/packages/graphql-builder/src/GraphQlQueryPrinter.ts @@ -1,14 +1,8 @@ import { JSONValue } from '@contember/schema' import { GraphQlField, GraphQlFragment, GraphQlFragmentSpread, GraphQlInlineFragment, GraphQlSelectionSet } from './nodes' -/** - * @internal - */ export type GraphQlPrintResult = { query: string; variables: Record } -/** - * @internal - */ export class GraphQlQueryPrinter { private indentString = '\t' diff --git a/packages/client/src/builder/index.ts b/packages/graphql-builder/src/index.ts similarity index 68% rename from packages/client/src/builder/index.ts rename to packages/graphql-builder/src/index.ts index 1faa1dcbe9..a80cfc0b6d 100644 --- a/packages/client/src/builder/index.ts +++ b/packages/graphql-builder/src/index.ts @@ -1,3 +1,2 @@ export * from './nodes' -export * from './types/json' export * from './GraphQlQueryPrinter' diff --git a/packages/client/src/builder/nodes/GraphQlField.ts b/packages/graphql-builder/src/nodes/GraphQlField.ts similarity index 85% rename from packages/client/src/builder/nodes/GraphQlField.ts rename to packages/graphql-builder/src/nodes/GraphQlField.ts index 59b7cad026..bb8978220a 100644 --- a/packages/client/src/builder/nodes/GraphQlField.ts +++ b/packages/graphql-builder/src/nodes/GraphQlField.ts @@ -1,10 +1,7 @@ -import { JSONValue } from '../types/json' import { GraphQlFragmentSpread } from './GraphQlFragmentSpread' import { GraphQlInlineFragment } from './GraphQlInlineFragment' +import { JSONValue } from '@contember/schema' -/** - * @internal - */ export class GraphQlField { constructor( public readonly alias: string | null, @@ -15,17 +12,11 @@ export class GraphQlField { } } -/** - * @internal - */ export type GraphQlFieldTypedArgs = Record -/** - * @internal - */ export type GraphQlSelectionSetItem = GraphQlField | GraphQlFragmentSpread | GraphQlInlineFragment export type GraphQlSelectionSet = GraphQlSelectionSetItem[] diff --git a/packages/client/src/builder/nodes/GraphQlFragment.ts b/packages/graphql-builder/src/nodes/GraphQlFragment.ts similarity index 91% rename from packages/client/src/builder/nodes/GraphQlFragment.ts rename to packages/graphql-builder/src/nodes/GraphQlFragment.ts index 325efaeb41..3cf11fb042 100644 --- a/packages/client/src/builder/nodes/GraphQlFragment.ts +++ b/packages/graphql-builder/src/nodes/GraphQlFragment.ts @@ -1,8 +1,5 @@ import { GraphQlSelectionSet } from './GraphQlField' -/** - * @internal - */ export class GraphQlFragment { constructor( public readonly name: string, diff --git a/packages/client/src/builder/nodes/GraphQlFragmentSpread.ts b/packages/graphql-builder/src/nodes/GraphQlFragmentSpread.ts similarity index 81% rename from packages/client/src/builder/nodes/GraphQlFragmentSpread.ts rename to packages/graphql-builder/src/nodes/GraphQlFragmentSpread.ts index 493a10af61..4281335e52 100644 --- a/packages/client/src/builder/nodes/GraphQlFragmentSpread.ts +++ b/packages/graphql-builder/src/nodes/GraphQlFragmentSpread.ts @@ -1,6 +1,3 @@ -/** - * @internal - */ export class GraphQlFragmentSpread { constructor( public readonly name: string, diff --git a/packages/client/src/builder/nodes/GraphQlInlineFragment.ts b/packages/graphql-builder/src/nodes/GraphQlInlineFragment.ts similarity index 90% rename from packages/client/src/builder/nodes/GraphQlInlineFragment.ts rename to packages/graphql-builder/src/nodes/GraphQlInlineFragment.ts index 73288364f4..b11be881ed 100644 --- a/packages/client/src/builder/nodes/GraphQlInlineFragment.ts +++ b/packages/graphql-builder/src/nodes/GraphQlInlineFragment.ts @@ -1,8 +1,5 @@ import { GraphQlSelectionSet } from './GraphQlField' -/** - * @internal - */ export class GraphQlInlineFragment { constructor( public readonly type: string, diff --git a/packages/client/src/builder/nodes/index.ts b/packages/graphql-builder/src/nodes/index.ts similarity index 100% rename from packages/client/src/builder/nodes/index.ts rename to packages/graphql-builder/src/nodes/index.ts diff --git a/packages/graphql-builder/src/tsconfig.json b/packages/graphql-builder/src/tsconfig.json new file mode 100644 index 0000000000..8542cac73a --- /dev/null +++ b/packages/graphql-builder/src/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../../tsconfig.settings.json", + "compilerOptions": { + "outDir": "../dist/types" + } +} diff --git a/packages/graphql-builder/tests/cases/unit/builder.test.ts b/packages/graphql-builder/tests/cases/unit/builder.test.ts new file mode 100644 index 0000000000..53406e78f8 --- /dev/null +++ b/packages/graphql-builder/tests/cases/unit/builder.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, test } from 'vitest' +import { GraphQlField, GraphQlQueryPrinter } from '../../../src' + +describe('builder', () => { + + test('fetch field', async () => { + const printer = new GraphQlQueryPrinter() + const result = printer.printDocument('query', [ + new GraphQlField(null, 'test', {}), + ], {}) + expect(result).toMatchInlineSnapshot(` + { + "query": "query { + test + } + ", + "variables": {}, + } + `) + }) +}) diff --git a/packages/graphql-builder/tests/tsconfig.json b/packages/graphql-builder/tests/tsconfig.json new file mode 100644 index 0000000000..833dd61fbe --- /dev/null +++ b/packages/graphql-builder/tests/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.settings.json", + "compilerOptions": { + "noEmit": true, + "emitDeclarationOnly": false + }, + "references": [ + { "path": "../src" } + ] +} diff --git a/packages/graphql-builder/tsconfig.json b/packages/graphql-builder/tsconfig.json new file mode 100644 index 0000000000..e3a586f5ef --- /dev/null +++ b/packages/graphql-builder/tsconfig.json @@ -0,0 +1,8 @@ +{ + "files": [], + "include": [], + "references": [ + { "path": "./src" }, + { "path": "./tests" } + ] +} diff --git a/packages/graphql-builder/tsdoc.json b/packages/graphql-builder/tsdoc.json new file mode 100644 index 0000000000..a46f62a20a --- /dev/null +++ b/packages/graphql-builder/tsdoc.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json", + "extends": [ + "../../tsdoc.json" + ] +} diff --git a/packages/graphql-builder/vite.config.js b/packages/graphql-builder/vite.config.js new file mode 100644 index 0000000000..0a8618c72a --- /dev/null +++ b/packages/graphql-builder/vite.config.js @@ -0,0 +1,3 @@ +import { createViteConfig } from './../../build/createViteConfig.js' + +export default createViteConfig('graphql-builder') diff --git a/packages/graphql-client/api-extractor.json b/packages/graphql-client/api-extractor.json new file mode 100644 index 0000000000..66c17dd719 --- /dev/null +++ b/packages/graphql-client/api-extractor.json @@ -0,0 +1,3 @@ +{ + "extends": "../../build/api-extractor.json" +} diff --git a/packages/graphql-client/package.json b/packages/graphql-client/package.json new file mode 100644 index 0000000000..e1c7e5235a --- /dev/null +++ b/packages/graphql-client/package.json @@ -0,0 +1,42 @@ +{ + "name": "@contember/graphql-client", + "license": "Apache-2.0", + "version": "0.0.0", + "main": "./dist/production/index.js", + "exports": { + ".": { + "import": { + "types": "./dist/types/index.d.ts", + "development": "./dist/development/index.js", + "production": "./dist/production/index.js", + "default": "./dist/production/index.js" + }, + "require": { + "types": "./dist/types/index.d.ts", + "development": "./dist/development/index.cjs", + "production": "./dist/production/index.cjs", + "default": "./dist/production/index.cjs" + } + } + }, + "files": [ + "dist/", + "src/" + ], + "typings": "./dist/types/index.d.ts", + "type": "module", + "sideEffects": false, + "scripts": { + "build": "yarn build:js:dev && yarn build:js:prod", + "build:js:dev": "NODE_ENV=development vite build --mode development", + "build:js:prod": "vite build --mode production", + "ae:build": "api-extractor run --local", + "ae:test": "api-extractor run", + "test": "vitest" + }, + "repository": { + "type": "git", + "url": "https://github.com/contember/interface.git", + "directory": "packages/graphql-client" + } +} diff --git a/packages/graphql-client/src/GraphQlClient.ts b/packages/graphql-client/src/GraphQlClient.ts new file mode 100644 index 0000000000..649169f22f --- /dev/null +++ b/packages/graphql-client/src/GraphQlClient.ts @@ -0,0 +1,93 @@ +import { GraphQlClientError, GraphQlErrorType } from './GraphQlClientError' +import { GraphQlClientRequestOptions } from './GraphQlClientRequestOptions' + +export class GraphQlClient { + constructor(public readonly apiUrl: string, private readonly apiToken?: string) { } + + async execute(query: string, options: GraphQlClientRequestOptions = {}): Promise { + let body: string | null = null + let response: Response | null = null + const createError = (type: GraphQlErrorType, errors?: any[], cause?: unknown) => { + const request = { + url: this.apiUrl, + query, + variables: options.variables ?? {}, + } + + const details = `HTTP response: ${response ? (response.status + ' ' + response.statusText) : ''} +HTTP body: +${body !== null ? body : ''} + +GraphQL query: +${query}` + + return new GraphQlClientError(`GraphQL request failed: ${type}`, type, request, response ?? undefined, errors, details, cause) + } + try { + response = await this.doExecute(query, options) + } catch (e) { + const aborted = typeof e === 'object' && e !== null && (e as { name?: unknown }).name === 'AbortError' + throw createError(aborted ? 'aborted' : 'network error', undefined, e) + } + + options?.onResponse?.(response) + + body = await response.text() + + let data: any + try { + data = JSON.parse(body) + } catch (e) { + throw createError('invalid response body', undefined, e) + } + options?.onData?.(data) + + if (response.status === 401) { + throw createError('unauthorized') + } + if (response.status === 403) { + throw createError('forbidden') + } + if (response.status >= 400 && response.status < 500) { + throw createError('bad request', data.errors) + } + if (response.status >= 500) { + throw createError('server error') + } + if (!(typeof data === 'object') || data === null) { + throw createError('invalid response body') + } + if ('errors' in data) { + throw createError('response errors', data.errors) + } + if (!('data' in data)) { + throw createError('invalid response body') + } + + return data.data + } + + + protected async doExecute( + query: string, + { apiToken, signal, variables, headers }: GraphQlClientRequestOptions = {}, + ): Promise { + const resolvedHeaders: Record = { + 'Content-Type': 'application/json', + ...headers, + } + const resolvedToken = apiToken ?? this.apiToken + + if (resolvedToken !== undefined) { + resolvedHeaders['Authorization'] = `Bearer ${resolvedToken}` + } + + return await fetch(this.apiUrl, { + method: 'POST', + headers: resolvedHeaders, + signal, + body: JSON.stringify({ query, variables }), + }) + } +} + diff --git a/packages/graphql-client/src/GraphQlClientError.ts b/packages/graphql-client/src/GraphQlClientError.ts new file mode 100644 index 0000000000..c81ffd461d --- /dev/null +++ b/packages/graphql-client/src/GraphQlClientError.ts @@ -0,0 +1,26 @@ +export type GraphQlErrorRequest = { url: string, query: string, variables: Record }; + +export type GraphQlErrorType = + | 'aborted' + | 'network error' + | 'invalid response body' + | 'bad request' + | 'unauthorized' + | 'forbidden' + | 'server error' + | 'response errors' + +export class GraphQlClientError extends Error { + constructor( + message: string, + public readonly type: GraphQlErrorType, + public readonly request: GraphQlErrorRequest, + public readonly response?: Response, + public readonly errors?: readonly any[], + public readonly details?: string, + cause?: unknown, + ) { + super(message) + this.cause = cause + } +} diff --git a/packages/graphql-client/src/GraphQlClientRequestOptions.ts b/packages/graphql-client/src/GraphQlClientRequestOptions.ts new file mode 100644 index 0000000000..f6c3937a3c --- /dev/null +++ b/packages/graphql-client/src/GraphQlClientRequestOptions.ts @@ -0,0 +1,12 @@ +export interface GraphQlClientRequestOptions { + variables?: GraphQlClientVariables + apiToken?: string + signal?: AbortSignal + headers?: Record + onResponse?: (response: Response) => void + onData?: (json: unknown) => void +} + +export interface GraphQlClientVariables { + [name: string]: any +} diff --git a/packages/graphql-client/src/index.ts b/packages/graphql-client/src/index.ts new file mode 100644 index 0000000000..ea6153f252 --- /dev/null +++ b/packages/graphql-client/src/index.ts @@ -0,0 +1,3 @@ +export * from './GraphQlClient' +export * from './GraphQlClientRequestOptions' +export * from './GraphQlClientError' diff --git a/packages/graphql-client/src/tsconfig.json b/packages/graphql-client/src/tsconfig.json new file mode 100644 index 0000000000..8542cac73a --- /dev/null +++ b/packages/graphql-client/src/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../../tsconfig.settings.json", + "compilerOptions": { + "outDir": "../dist/types" + } +} diff --git a/packages/graphql-client/tests/cases/unit/client.test.ts b/packages/graphql-client/tests/cases/unit/client.test.ts new file mode 100644 index 0000000000..da872d2e44 --- /dev/null +++ b/packages/graphql-client/tests/cases/unit/client.test.ts @@ -0,0 +1,8 @@ +import { describe, test } from 'vitest' + +describe('client', () => { + + test('send query', async () => { + // todo + }) +}) diff --git a/packages/graphql-client/tests/tsconfig.json b/packages/graphql-client/tests/tsconfig.json new file mode 100644 index 0000000000..833dd61fbe --- /dev/null +++ b/packages/graphql-client/tests/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.settings.json", + "compilerOptions": { + "noEmit": true, + "emitDeclarationOnly": false + }, + "references": [ + { "path": "../src" } + ] +} diff --git a/packages/graphql-client/tsconfig.json b/packages/graphql-client/tsconfig.json new file mode 100644 index 0000000000..e3a586f5ef --- /dev/null +++ b/packages/graphql-client/tsconfig.json @@ -0,0 +1,8 @@ +{ + "files": [], + "include": [], + "references": [ + { "path": "./src" }, + { "path": "./tests" } + ] +} diff --git a/packages/graphql-client/tsdoc.json b/packages/graphql-client/tsdoc.json new file mode 100644 index 0000000000..a46f62a20a --- /dev/null +++ b/packages/graphql-client/tsdoc.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json", + "extends": [ + "../../tsdoc.json" + ] +} diff --git a/packages/graphql-client/vite.config.js b/packages/graphql-client/vite.config.js new file mode 100644 index 0000000000..29ee53fe50 --- /dev/null +++ b/packages/graphql-client/vite.config.js @@ -0,0 +1,3 @@ +import { createViteConfig } from './../../build/createViteConfig.js' + +export default createViteConfig('graphql-client') diff --git a/tsconfig.json b/tsconfig.json index de2ba03424..6c10360a1e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,10 @@ { "path": "./packages/binding" }, { "path": "./packages/brand" }, { "path": "./packages/client" }, - { "path": "./packages/client-generator" }, + { "path": "./packages/client-content" }, + { "path": "./packages/client-content-generator" }, + { "path": "./packages/graphql-builder" }, + { "path": "./packages/graphql-client" }, { "path": "./packages/interface-tester" }, { "path": "./packages/layout" }, { "path": "./packages/react-auto" }, diff --git a/yarn.lock b/yarn.lock index d5cccc6d10..11de9191e8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2788,11 +2788,11 @@ __metadata: languageName: unknown linkType: soft -"@contember/client-generator@workspace:packages/client-generator": +"@contember/client-content-generator@workspace:packages/client-content-generator": version: 0.0.0-use.local - resolution: "@contember/client-generator@workspace:packages/client-generator" + resolution: "@contember/client-content-generator@workspace:packages/client-content-generator" dependencies: - "@contember/client": "workspace:*" + "@contember/client-content": "workspace:*" "@contember/schema": ^1.3.6 "@contember/schema-definition": ^1.3.6 "@contember/schema-utils": ^1.3.6 @@ -2802,10 +2802,23 @@ __metadata: languageName: unknown linkType: soft +"@contember/client-content@workspace:*, @contember/client-content@workspace:packages/client-content": + version: 0.0.0-use.local + resolution: "@contember/client-content@workspace:packages/client-content" + dependencies: + "@contember/graphql-builder": "workspace:*" + "@contember/graphql-client": "workspace:*" + "@contember/schema": ^1.3.6 + languageName: unknown + linkType: soft + "@contember/client@workspace:*, @contember/client@workspace:packages/client": version: 0.0.0-use.local resolution: "@contember/client@workspace:packages/client" dependencies: + "@contember/client-content": "workspace:*" + "@contember/graphql-builder": "workspace:*" + "@contember/graphql-client": "workspace:*" "@contember/schema": ^1.3.6 p-limit: ^4.0.0 languageName: unknown @@ -2850,6 +2863,20 @@ __metadata: languageName: node linkType: hard +"@contember/graphql-builder@workspace:*, @contember/graphql-builder@workspace:packages/graphql-builder": + version: 0.0.0-use.local + resolution: "@contember/graphql-builder@workspace:packages/graphql-builder" + dependencies: + "@contember/schema": ^1.3.6 + languageName: unknown + linkType: soft + +"@contember/graphql-client@workspace:*, @contember/graphql-client@workspace:packages/graphql-client": + version: 0.0.0-use.local + resolution: "@contember/graphql-client@workspace:packages/graphql-client" + languageName: unknown + linkType: soft + "@contember/interface-tester@workspace:*, @contember/interface-tester@workspace:packages/interface-tester": version: 0.0.0-use.local resolution: "@contember/interface-tester@workspace:packages/interface-tester" From 39ce4289e87d301556037bb8ac1664d283e0fed5 Mon Sep 17 00:00:00 2001 From: David Matejka Date: Mon, 11 Dec 2023 16:41:51 +0100 Subject: [PATCH 14/17] feat(graphql-client): add onBeforeRequest --- packages/graphql-client/src/GraphQlClient.ts | 3 +++ packages/graphql-client/src/GraphQlClientRequestOptions.ts | 1 + 2 files changed, 4 insertions(+) diff --git a/packages/graphql-client/src/GraphQlClient.ts b/packages/graphql-client/src/GraphQlClient.ts index 649169f22f..d87ea80907 100644 --- a/packages/graphql-client/src/GraphQlClient.ts +++ b/packages/graphql-client/src/GraphQlClient.ts @@ -23,6 +23,9 @@ ${query}` return new GraphQlClientError(`GraphQL request failed: ${type}`, type, request, response ?? undefined, errors, details, cause) } + + options?.onBeforeRequest?.({ query, variables: options.variables ?? {} }) + try { response = await this.doExecute(query, options) } catch (e) { diff --git a/packages/graphql-client/src/GraphQlClientRequestOptions.ts b/packages/graphql-client/src/GraphQlClientRequestOptions.ts index f6c3937a3c..143c2d0df7 100644 --- a/packages/graphql-client/src/GraphQlClientRequestOptions.ts +++ b/packages/graphql-client/src/GraphQlClientRequestOptions.ts @@ -3,6 +3,7 @@ export interface GraphQlClientRequestOptions { apiToken?: string signal?: AbortSignal headers?: Record + onBeforeRequest?: (query: { query: string, variables: GraphQlClientVariables }) => void onResponse?: (response: Response) => void onData?: (json: unknown) => void } From 11f0506fa938f53f56e88c8f714d16d8fa26b4c6 Mon Sep 17 00:00:00 2001 From: David Matejka Date: Mon, 11 Dec 2023 16:42:16 +0100 Subject: [PATCH 15/17] feat(binding): add console.debug on graphql query --- packages/binding/src/core/DataBinding.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/binding/src/core/DataBinding.ts b/packages/binding/src/core/DataBinding.ts index 6119689bdb..3f277a27ca 100644 --- a/packages/binding/src/core/DataBinding.ts +++ b/packages/binding/src/core/DataBinding.ts @@ -144,6 +144,9 @@ export class DataBinding { try { response = await this.contentClient.mutate(mutations, { signal, + onBeforeRequest: ({ query, variables }) => { + console.debug(query, variables) + }, }) } catch (e) { if (e instanceof GraphQlClientError) { @@ -353,6 +356,9 @@ export class DataBinding { try { return await this.contentClient.query(query, { signal, + onBeforeRequest: ({ query, variables }) => { + console.debug(query, variables) + }, }) } catch (e) { if (e instanceof GraphQlClientError) { From 8c66f58a3bcbdabb1452169f428d425004ba8d66 Mon Sep 17 00:00:00 2001 From: David Matejka Date: Mon, 11 Dec 2023 16:42:38 +0100 Subject: [PATCH 16/17] refactor(client-content): move transaction handling to query builder --- packages/binding/src/core/DataBinding.ts | 7 +- packages/client-content/src/ContentClient.ts | 207 +--- .../client-content/src/ContentQueryBuilder.ts | 87 +- .../src/TypedContentQueryBuilder.ts | 26 +- .../src/nodes/ContentEntitySelection.ts | 19 +- .../src/nodes/ContentMutation.ts | 23 - .../{ContentQuery.ts => ContentOperation.ts} | 15 +- packages/client-content/src/nodes/index.ts | 3 +- .../src/utils/ContentOperationSet.ts | 11 + .../src/utils/createMutationOperationSet.ts | 42 + .../src/utils/createQueryOperationSet.ts | 20 + .../tests/cases/unit/mutation.test.ts | 890 +++++++++++++++--- .../tests/cases/unit/query.test.ts | 94 +- 13 files changed, 1049 insertions(+), 395 deletions(-) delete mode 100644 packages/client-content/src/nodes/ContentMutation.ts rename packages/client-content/src/nodes/{ContentQuery.ts => ContentOperation.ts} (51%) create mode 100644 packages/client-content/src/utils/ContentOperationSet.ts create mode 100644 packages/client-content/src/utils/createMutationOperationSet.ts create mode 100644 packages/client-content/src/utils/createQueryOperationSet.ts diff --git a/packages/binding/src/core/DataBinding.ts b/packages/binding/src/core/DataBinding.ts index 3f277a27ca..e1a377cce7 100644 --- a/packages/binding/src/core/DataBinding.ts +++ b/packages/binding/src/core/DataBinding.ts @@ -1,5 +1,6 @@ import { - ContentClient, ContentQueryBuilder, + ContentClient, + ContentQueryBuilder, GraphQlClient, GraphQlClientError, MutationResult, @@ -142,7 +143,9 @@ export class DataBinding { let response: DataBindingTransactionResult try { - response = await this.contentClient.mutate(mutations, { + response = await this.contentClient.mutate(this.queryBuilder.transaction(mutations, { + deferForeignKeyConstraints: true, + }), { signal, onBeforeRequest: ({ query, variables }) => { console.debug(query, variables) diff --git a/packages/client-content/src/ContentClient.ts b/packages/client-content/src/ContentClient.ts index 1ab3be13a2..3d29d64cdb 100644 --- a/packages/client-content/src/ContentClient.ts +++ b/packages/client-content/src/ContentClient.ts @@ -1,28 +1,12 @@ -import { ContentMutation, ContentQuery } from './nodes' import { mutationFragments } from './utils/mutationFragments' -import { MutationResult, TransactionResult } from './types' import { GraphQlClientRequestOptions } from '@contember/graphql-client' -import { GraphQlField, GraphQlFragmentSpread, GraphQlQueryPrinter, GraphQlSelectionSetItem } from '@contember/graphql-builder' - -export type MutationWithTransactionOptions = { - deferForeignKeyConstraints?: boolean - deferUniqueConstraints?: boolean - transaction?: true -} - -export type MutationWithoutTransactionOptions = { - transaction: false -} +import { GraphQlQueryPrinter } from '@contember/graphql-builder' +import { ContentMutation, ContentQuery } from './nodes' +import { createMutationOperationSet } from './utils/createMutationOperationSet' +import { createQueryOperationSet } from './utils/createQueryOperationSet' -export type QueryExecutorOptions = { - variables?: Record - apiToken?: string - signal?: AbortSignal - headers?: Record - onResponse?: (response: Response) => void - onData?: (json: unknown) => void -} +export type QueryExecutorOptions = GraphQlClientRequestOptions export type QueryExecutor = (query: string, options: GraphQlClientRequestOptions) => Promise @@ -36,182 +20,23 @@ export class ContentClient { public async query(query: ContentQuery, options?: QueryExecutorOptions): Promise public async query>(queries: {[K in keyof Values]: ContentQuery}, options?: QueryExecutorOptions): Promise public async query(queries: Record> | ContentQuery, options?: QueryExecutorOptions): Promise { - const { query, variables } = this.prepareQuery(queries) - const result = await this.executor(query, { variables, ...options }) - return this.processQueryResult(queries, result) - } - - private prepareQuery(queries: Record> | ContentQuery) { const printer = new GraphQlQueryPrinter() - - const selectionSet = queries instanceof ContentQuery - ? [new GraphQlField('value', queries.queryFieldName, queries.args, queries.nodeSelection)] - : Object.entries(queries).map(([alias, query]) => new GraphQlField(alias, query.queryFieldName, query.args, query.nodeSelection)) - - return printer.printDocument('query', selectionSet, {}) - } - - private processQueryResult(queries: Record> | ContentQuery, result: any) { - if (queries instanceof ContentQuery) { - return queries.parse(result.value) - } - return Object.fromEntries(Object.entries(queries).map(([alias, query]) => [alias, query.parse(result[alias])])) - } - - public async mutate( - mutation: ContentMutation, - options: MutationWithoutTransactionOptions & QueryExecutorOptions, - ): Promise> - public async mutate( - mutation: ContentMutation, - options?: MutationWithTransactionOptions & QueryExecutorOptions, - ): Promise>> - - public async mutate( - mutations: ContentMutation[], - options: MutationWithoutTransactionOptions & QueryExecutorOptions, - ): Promise[]> - public async mutate( - mutations: ContentMutation[], - options?: MutationWithTransactionOptions & QueryExecutorOptions - ): Promise[]>> - - public async mutate>>( - input: Input, - options: MutationWithoutTransactionOptions & QueryExecutorOptions, - ): Promise<{ - [K in keyof Input]: Input[K] extends ContentMutation ? MutationResult : never - }> - public async mutate>>( - input: Input, - options?: MutationWithTransactionOptions & QueryExecutorOptions - ): Promise ? MutationResult : never - }>> - - public async mutate | ContentQuery>>( - input: Input, - options: MutationWithoutTransactionOptions & QueryExecutorOptions, - ): Promise<{ - [K in keyof Input]: Input[K] extends ContentMutation ? MutationResult : Input[K] extends ContentQuery ? Value : never - }> - public async mutate | ContentQuery>>( - input: Input, - options?: MutationWithTransactionOptions & QueryExecutorOptions - ): Promise ? MutationResult : Input[K] extends ContentQuery ? Value : never - }>> - - public async mutate(input: Record | ContentQuery> | ContentMutation | ContentMutation[], options?: & QueryExecutorOptions & (MutationWithTransactionOptions | MutationWithoutTransactionOptions)): Promise { - const { query, variables } = this.prepareMutation(input, options) + const operationSet = createQueryOperationSet(queries) + const { query, variables } = printer.printDocument('query', operationSet.selection, {}) const result = await this.executor(query, { variables, ...options }) - - return this.processMutationResponse(result, input, options) + return operationSet.parse(result) } - private processMutationResponse( - result: any, - input: Record | ContentQuery> | ContentMutation | ContentMutation[], - options?: & QueryExecutorOptions & (MutationWithTransactionOptions | MutationWithoutTransactionOptions), - ) { - const transaction = options?.transaction ?? true - const innerResult = transaction ? (result as any).transaction : result - - let value: any - if (input instanceof ContentMutation) { - value = innerResult.mut - } else if (Array.isArray(input)) { - value = input.map((_, i) => innerResult['mut_' + i] ?? null) - } else { - value = {} - for (const [alias, mutation] of Object.entries(input)) { - if (mutation instanceof ContentQuery) { - value[alias] = mutation.parse(innerResult[alias].value) - } else { - value[alias] = innerResult[alias] - } - } - } - - if (transaction) { - return { - ok: innerResult.ok, - errorMessage: innerResult.errorMessage, - errors: innerResult.errors, - validation: innerResult.validation, - data: value, - } - } else { - return value - } - } + public async mutate(mutation: ContentMutation, options?: QueryExecutorOptions,): Promise + public async mutate(mutations: ContentMutation[], options?: QueryExecutorOptions,): Promise + public async mutate>(mutations: { [K in keyof Values]: ContentMutation | ContentQuery }, options?: QueryExecutorOptions): Promise + public async mutate(input: Record | ContentQuery> | ContentMutation | ContentMutation[], options?: QueryExecutorOptions): Promise { + const operationSet = createMutationOperationSet(input) - private prepareMutation( - input: Record | ContentQuery> | ContentMutation | ContentMutation[], - options?: & QueryExecutorOptions & (MutationWithTransactionOptions | MutationWithoutTransactionOptions), - ) { const printer = new GraphQlQueryPrinter() + const { query, variables } = printer.printDocument('mutation', operationSet.selection, mutationFragments) + const result = await this.executor(query, { variables, ...options }) - const fields: GraphQlField[] = [] - if (input instanceof ContentMutation) { - fields.push(this.createMutationField('mut', input)) - } else if (Array.isArray(input)) { - let i = 0 - for (const mutation of input) { - fields.push(this.createMutationField('mut_' + i++, mutation)) - } - } else { - for (const [alias, mutation] of Object.entries(input)) { - if (mutation instanceof ContentQuery) { - fields.push(new GraphQlField(alias, 'query', {}, [ - new GraphQlField('value', mutation.queryFieldName, mutation.args, mutation.nodeSelection), - ])) - } else { - fields.push(this.createMutationField(alias, mutation)) - } - } - } - - const selectionSet = options?.transaction !== false - ? [this.wrapIntoTransaction(fields, options)] - : fields - - return printer.printDocument('mutation', selectionSet, mutationFragments) - } - - private wrapIntoTransaction(fields: GraphQlField[], options?: MutationWithTransactionOptions) { - return new GraphQlField(null, 'transaction', { - ...(options?.deferForeignKeyConstraints ? { - deferForeignKeyConstraints: { - graphQlType: 'Boolean', - value: options?.deferForeignKeyConstraints, - }, - } : {}), - ...(options?.deferUniqueConstraints ? { - deferUniqueConstraints: { - graphQlType: 'Boolean', - value: options?.deferUniqueConstraints, - }, - } : {}), - }, fields) - } - - private createMutationField(alias: string, mutation: ContentMutation): GraphQlField { - const items: GraphQlSelectionSetItem[] = [ - new GraphQlField(null, 'ok'), - new GraphQlField(null, 'errorMessage'), - new GraphQlField(null, 'errors', {}, [ - new GraphQlFragmentSpread('MutationError'), - ]), - ] - if (mutation.operation !== 'delete') { - items.push(new GraphQlField(null, 'validation', {}, [ - new GraphQlFragmentSpread('ValidationResult'), - ])) - } - if (mutation.nodeSelection) { - items.push(new GraphQlField(null, 'node', {}, mutation.nodeSelection)) - } - return new GraphQlField(alias, mutation.mutationFieldName, mutation.mutationArgs, items) + return operationSet.parse(result) } } diff --git a/packages/client-content/src/ContentQueryBuilder.ts b/packages/client-content/src/ContentQueryBuilder.ts index 63b9fc9405..199832128e 100644 --- a/packages/client-content/src/ContentQueryBuilder.ts +++ b/packages/client-content/src/ContentQueryBuilder.ts @@ -1,20 +1,26 @@ -import { ContentClientInput, SchemaNames } from './types' +import { ContentClientInput, MutationResult, SchemaNames, TransactionResult } from './types' import { ContentEntitySelection, ContentEntitySelectionCallback, ContentEntitySelectionContext, ContentMutation, - ContentQuery, createEntitySelection, + ContentOperation, + ContentQuery, } from './nodes' import { createListArgs } from './utils/createListArgs' import { createTypedArgs } from './utils/createTypedArgs' import { Input } from '@contember/schema' -import { GraphQlField, GraphQlSelectionSet } from '@contember/graphql-builder' +import { GraphQlField, GraphQlFragmentSpread, GraphQlSelectionSet } from '@contember/graphql-builder' +import { createMutationOperationSet } from './utils/createMutationOperationSet' export type EntitySelectionOrCallback = | ContentEntitySelection | ContentEntitySelectionCallback +export type MutationTransactionOptions = { + deferForeignKeyConstraints?: boolean + deferUniqueConstraints?: boolean +} export class ContentQueryBuilder { constructor(private readonly schema: SchemaNames) { @@ -24,7 +30,7 @@ export class ContentQueryBuilder { public fragment(name: string, fieldsCallback?: ContentEntitySelectionCallback): ContentEntitySelection { const context = this.createContext(name) - const entitySelection = createEntitySelection(context, []) + const entitySelection = new ContentEntitySelection(context, []) if (!fieldsCallback) { return entitySelection } @@ -45,7 +51,7 @@ export class ContentQueryBuilder { ]), ] - return new ContentQuery(context.entity, fieldName, typedArgs, selectionSet, it => { + return new ContentOperation('query', fieldName, typedArgs, selectionSet, it => { return it.pageInfo.totalCount }) } @@ -57,7 +63,7 @@ export class ContentQueryBuilder { const typedArgs = createListArgs(context.entity, args) const selectionSet = this.resolveSelectionSet(fields, context) - return new ContentQuery(context.entity, fieldName, typedArgs, selectionSet) + return new ContentOperation('query', fieldName, typedArgs, selectionSet) } @@ -70,24 +76,24 @@ export class ContentQueryBuilder { }) const selectionSet = this.resolveSelectionSet(fields, context) - return new ContentQuery(context.entity, fieldName, typedArgs, selectionSet) + return new ContentOperation('query', fieldName, typedArgs, selectionSet) } - public create(name: string, args: Input.CreateInput, fields?: EntitySelectionOrCallback): ContentMutation { + public create(name: string, args: Input.CreateInput, fields?: EntitySelectionOrCallback): ContentMutation { const context = this.createContext(name) const fieldName = `create${name}` const typedArgs = createTypedArgs(args, { data: `${context.entity.name}CreateInput!`, }) - const selectionSet = fields ? this.resolveSelectionSet(fields, context) : undefined + const selectionSet = this.createMutationSelection('create', fields ? this.resolveSelectionSet(fields, context) : undefined) - return new ContentMutation(context.entity, 'create', fieldName, typedArgs, selectionSet) + return new ContentOperation('mutation', fieldName, typedArgs, selectionSet) } - public update(name: string, args: Input.UpdateInput, fields?: EntitySelectionOrCallback): ContentMutation { + public update(name: string, args: Input.UpdateInput, fields?: EntitySelectionOrCallback): ContentMutation { const context = this.createContext(name) const fieldName = `update${name}` @@ -96,13 +102,13 @@ export class ContentQueryBuilder { by: `${context.entity.name}UniqueWhere!`, filter: `${context.entity.name}Where`, }) - const selectionSet = fields ? this.resolveSelectionSet(fields, context) : undefined + const selectionSet = this.createMutationSelection('update', fields ? this.resolveSelectionSet(fields, context) : undefined) - return new ContentMutation(context.entity, 'update', fieldName, typedArgs, selectionSet) + return new ContentOperation('mutation', fieldName, typedArgs, selectionSet) } - public upsert(name: string, args: Input.UpsertInput, fields?: EntitySelectionOrCallback): ContentMutation { + public upsert(name: string, args: Input.UpsertInput, fields?: EntitySelectionOrCallback): ContentMutation { const context = this.createContext(name) const fieldName = `upsert${name}` @@ -112,13 +118,14 @@ export class ContentQueryBuilder { by: `${context.entity.name}UniqueWhere!`, filter: `${context.entity.name}Where`, }) - const selectionSet = fields ? this.resolveSelectionSet(fields, context) : undefined - return new ContentMutation(context.entity, 'upsert', fieldName, typedArgs, selectionSet) + const selectionSet = this.createMutationSelection('upsert', fields ? this.resolveSelectionSet(fields, context) : undefined) + + return new ContentOperation('mutation', fieldName, typedArgs, selectionSet) } - public delete(name: string, args: Input.UniqueQueryInput, fields?: EntitySelectionOrCallback): ContentMutation { + public delete(name: string, args: Input.UniqueQueryInput, fields?: EntitySelectionOrCallback): ContentMutation { const context = this.createContext(name) const typedArgs = createTypedArgs(args, { @@ -126,16 +133,56 @@ export class ContentQueryBuilder { filter: `${context.entity.name}Where`, }) const fieldName = `delete${name}` - const selectionSet = fields ? this.resolveSelectionSet(fields, context) : undefined + const selectionSet = this.createMutationSelection('delete', fields ? this.resolveSelectionSet(fields, context) : undefined) + + return new ContentOperation('mutation', fieldName, typedArgs, selectionSet) + } + + public transaction( + input: Record | ContentQuery> | ContentMutation | ContentMutation[], + options: MutationTransactionOptions = {}, + ): ContentMutation> { + const combined = createMutationOperationSet(input) + + const transactionArgs = createTypedArgs({ options }, { + options: 'MutationTransactionOptions', + }) + return new ContentOperation, 'mutation'>('mutation', 'transaction', transactionArgs, combined.selection, ({ ok, errorMessage, errors, validation, ...data }) => { + return { + ok, + errorMessage, + errors, + validation, + data: combined.parse(data), + } + }) + } - return new ContentMutation(context.entity, 'delete', fieldName, typedArgs, selectionSet) + private createMutationSelection(operation: 'create' | 'update' | 'delete' | 'upsert', selection?: GraphQlSelectionSet): GraphQlSelectionSet { + const items: GraphQlSelectionSet = [ + new GraphQlField(null, 'ok'), + new GraphQlField(null, 'errorMessage'), + new GraphQlField(null, 'errors', {}, [ + new GraphQlFragmentSpread('MutationError'), + ]), + ] + if (operation !== 'delete') { + items.push(new GraphQlField(null, 'validation', {}, [ + new GraphQlFragmentSpread('ValidationResult'), + ])) + } + if (selection) { + items.push(new GraphQlField(null, 'node', {}, selection)) + } + return items } + private resolveSelectionSet( fields: EntitySelectionOrCallback, context: ContentEntitySelectionContext, ) { - return (typeof fields === 'function' ? fields(createEntitySelection(context, [])) : fields).selectionSet + return (typeof fields === 'function' ? fields(new ContentEntitySelection(context, [])) : fields).selectionSet } private createContext( diff --git a/packages/client-content/src/TypedContentQueryBuilder.ts b/packages/client-content/src/TypedContentQueryBuilder.ts index 7becd0f2a4..292c142de4 100644 --- a/packages/client-content/src/TypedContentQueryBuilder.ts +++ b/packages/client-content/src/TypedContentQueryBuilder.ts @@ -1,5 +1,6 @@ -import { ContentClientInput, SchemaTypeLike } from './types' +import { ContentClientInput, MutationResult, SchemaTypeLike, TransactionResult } from './types' import { ContentMutation, ContentQuery, TypedEntitySelection, TypedEntitySelectionCallback } from './nodes' +import { MutationTransactionOptions } from './ContentQueryBuilder' export type TypedContentEntitySelectionOrCallback = @@ -28,7 +29,6 @@ export interface TypedContentQueryBuilder { fields: TypedContentEntitySelectionOrCallback, ): ContentQuery - get( name: EntityName, args: ContentClientInput.UniqueQueryInput, @@ -39,24 +39,38 @@ export interface TypedContentQueryBuilder { name: EntityName, args: ContentClientInput.CreateInput, fields?: TypedContentEntitySelectionOrCallback, - ): ContentMutation + ): ContentMutation> update( name: EntityName, args: ContentClientInput.UpdateInput, fields?: TypedContentEntitySelectionOrCallback, - ): ContentMutation + ): ContentMutation> upsert( name: EntityName, args: ContentClientInput.UpsertInput, fields?: TypedContentEntitySelectionOrCallback, - ): ContentMutation + ): ContentMutation> delete( name: EntityName, args: ContentClientInput.UniqueQueryInput, fields?: TypedContentEntitySelectionOrCallback, - ): ContentMutation + ): ContentMutation> + + transaction( + mutation: ContentMutation, + options?: MutationTransactionOptions, + ): ContentMutation> + transaction( + mutations: ContentMutation[], + options?: MutationTransactionOptions, + ): ContentMutation> + + transaction>( + mutations: { [K in keyof Values]: ContentMutation | ContentQuery }, + options?: MutationTransactionOptions, + ): ContentMutation> } diff --git a/packages/client-content/src/nodes/ContentEntitySelection.ts b/packages/client-content/src/nodes/ContentEntitySelection.ts index 3e0aabc408..75e8b95688 100644 --- a/packages/client-content/src/nodes/ContentEntitySelection.ts +++ b/packages/client-content/src/nodes/ContentEntitySelection.ts @@ -25,14 +25,7 @@ export type EntitySelectionAnyArgs = | EntitySelectionManyByArgs | EntitySelectionOneArgs -export const createEntitySelection = ( - context: ContentEntitySelectionContext, - selectionSet: GraphQlSelectionSet, -): ContentEntitySelection => { - return new ContentEntitySelection(context, selectionSet) -} - -type ContentEntitySelectionOrCallback = ContentEntitySelectionCallback | ContentEntitySelection +export type ContentEntitySelectionOrCallback = ContentEntitySelectionCallback | ContentEntitySelection export class ContentEntitySelection { @@ -97,7 +90,7 @@ export class ContentEntitySelection { ...this.selectionSet, ...columns.map(col => new GraphQlField(null, col)), ] - return createEntitySelection(this.context, nodes) + return new ContentEntitySelection(this.context, nodes) } private _column( @@ -141,7 +134,7 @@ export class ContentEntitySelection { schema: this.context.schema, } - const entitySelection = typeof fields === 'function' ? fields(createEntitySelection(newContext, [])) : fields + const entitySelection = typeof fields === 'function' ? fields(new ContentEntitySelection(newContext, [])) : fields const newObjectField = new GraphQlField( alias, name, @@ -188,7 +181,7 @@ export class ContentEntitySelection { }, } - const entitySelection = typeof fields === 'function' ? fields(createEntitySelection(newContext, []) as any) : fields + const entitySelection = typeof fields === 'function' ? fields(new ContentEntitySelection(newContext, []) as any) : fields const newObjectField = new GraphQlField( alias, name, @@ -226,7 +219,7 @@ export class ContentEntitySelection { }, } const entitySelection = typeof fields === 'function' - ? fields(createEntitySelection(newContext, []) as any) + ? fields(new ContentEntitySelection(newContext, []) as any) : fields const newObjectField = new GraphQlField( alias, @@ -239,7 +232,7 @@ export class ContentEntitySelection { private withField(field: GraphQlField) { - return createEntitySelection(this.context, [ + return new ContentEntitySelection(this.context, [ ...this.selectionSet, field, ]) diff --git a/packages/client-content/src/nodes/ContentMutation.ts b/packages/client-content/src/nodes/ContentMutation.ts deleted file mode 100644 index ee9620b999..0000000000 --- a/packages/client-content/src/nodes/ContentMutation.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { SchemaEntityNames } from '../types' -import { GraphQlFieldTypedArgs, GraphQlSelectionSet } from '@contember/graphql-builder' - -export class ContentMutation { - public readonly type = 'mutation' - - /** - * @internal - */ - constructor( - /** @internal */ - public readonly entity: SchemaEntityNames, - /** @internal */ - public readonly operation: 'create' | 'update' | 'delete' | 'upsert', - /** @internal */ - public readonly mutationFieldName: string, - /** @internal */ - public readonly mutationArgs: GraphQlFieldTypedArgs = {}, - /** @internal */ - public readonly nodeSelection?: GraphQlSelectionSet, - ) { - } -} diff --git a/packages/client-content/src/nodes/ContentQuery.ts b/packages/client-content/src/nodes/ContentOperation.ts similarity index 51% rename from packages/client-content/src/nodes/ContentQuery.ts rename to packages/client-content/src/nodes/ContentOperation.ts index 964f1f72df..2387d0f1d6 100644 --- a/packages/client-content/src/nodes/ContentQuery.ts +++ b/packages/client-content/src/nodes/ContentOperation.ts @@ -1,24 +1,23 @@ -import { SchemaEntityNames } from '../types/Schema' import { GraphQlFieldTypedArgs, GraphQlSelectionSet } from '@contember/graphql-builder' - -export class ContentQuery { - public readonly type = 'query' - +export class ContentOperation { /** * @internal */ constructor( /** @internal */ - public readonly entity: SchemaEntityNames, + public readonly type: TType, /** @internal */ - public readonly queryFieldName: string, + public readonly fieldName: string, /** @internal */ public readonly args: GraphQlFieldTypedArgs = {}, /** @internal */ - public readonly nodeSelection?: GraphQlSelectionSet, + public readonly selection?: GraphQlSelectionSet, /** @internal */ public readonly parse: (value: any) => TValue = it => it as TValue, ) { } } + +export type ContentQuery = ContentOperation +export type ContentMutation = ContentOperation diff --git a/packages/client-content/src/nodes/index.ts b/packages/client-content/src/nodes/index.ts index d13d85b9ba..17673b91c5 100644 --- a/packages/client-content/src/nodes/index.ts +++ b/packages/client-content/src/nodes/index.ts @@ -1,4 +1,3 @@ export * from './ContentEntitySelection' -export * from './ContentMutation' -export * from './ContentQuery' +export * from './ContentOperation' export * from './TypedEntitySelection' diff --git a/packages/client-content/src/utils/ContentOperationSet.ts b/packages/client-content/src/utils/ContentOperationSet.ts new file mode 100644 index 0000000000..9e03246300 --- /dev/null +++ b/packages/client-content/src/utils/ContentOperationSet.ts @@ -0,0 +1,11 @@ +import { GraphQlSelectionSet } from '@contember/graphql-builder' + +export class ContentOperationSet { + constructor( + /** @internal */ + public readonly selection: GraphQlSelectionSet, + /** @internal */ + public readonly parse: (value: any) => TValue = it => it as TValue, + ) { + } +} diff --git a/packages/client-content/src/utils/createMutationOperationSet.ts b/packages/client-content/src/utils/createMutationOperationSet.ts new file mode 100644 index 0000000000..ee0e190ac8 --- /dev/null +++ b/packages/client-content/src/utils/createMutationOperationSet.ts @@ -0,0 +1,42 @@ +import { ContentMutation, ContentOperation, ContentQuery } from '../nodes' +import { GraphQlField } from '@contember/graphql-builder' +import { ContentOperationSet } from './ContentOperationSet' + +export const createMutationOperationSet = ( + input: + | Record | ContentQuery> + | ContentMutation + | ContentMutation[], +): ContentOperationSet => { + + if (input instanceof ContentOperation) { + return new ContentOperationSet( + [new GraphQlField('mut', input.fieldName, input.args, input.selection)], + it => input.parse(it.mut), + ) + } + + if (Array.isArray(input)) { + return new ContentOperationSet( + input.map((mut, i) => new GraphQlField('mut_' + i++, mut.fieldName, mut.args, mut.selection)), + data => input.map((mut, i) => mut.parse(data['mut_' + i] ?? null)), + ) + } + + return new ContentOperationSet( + Object.entries(input).map(([alias, mutation]) => { + if (mutation.type === 'query') { + return new GraphQlField(alias, 'query', {}, [ + new GraphQlField('value', mutation.fieldName, mutation.args, mutation.selection), + ]) + } + return new GraphQlField(alias, mutation.fieldName, mutation.args, mutation.selection) + }), + res => Object.fromEntries(Object.entries(input).map(([alias, mutation]) => { + if (mutation.type === 'query') { + return [alias, mutation.parse(res[alias]?.value ?? null)] + } + return [alias, mutation.parse(res[alias] ?? null)] + })), + ) +} diff --git a/packages/client-content/src/utils/createQueryOperationSet.ts b/packages/client-content/src/utils/createQueryOperationSet.ts new file mode 100644 index 0000000000..f9571a0e17 --- /dev/null +++ b/packages/client-content/src/utils/createQueryOperationSet.ts @@ -0,0 +1,20 @@ +import { ContentOperation, ContentQuery } from '../nodes' +import { ContentOperationSet } from './ContentOperationSet' +import { GraphQlField } from '@contember/graphql-builder' + +export const createQueryOperationSet = ( + input: Record> | ContentQuery, +): ContentOperationSet => { + + if (input instanceof ContentOperation) { + return new ContentOperationSet( + [new GraphQlField('value', input.fieldName, input.args, input.selection)], + it => input.parse(it.value), + ) + } + + return new ContentOperationSet( + Object.entries(input).map(([alias, query]) => new GraphQlField(alias, query.fieldName, query.args, query.selection)), + res => Object.fromEntries(Object.entries(input).map(([alias, query]) => [alias, query.parse(res[alias] ?? null)])), + ) +} diff --git a/packages/client-content/tests/cases/unit/mutation.test.ts b/packages/client-content/tests/cases/unit/mutation.test.ts index 416b380fb7..848257810e 100644 --- a/packages/client-content/tests/cases/unit/mutation.test.ts +++ b/packages/client-content/tests/cases/unit/mutation.test.ts @@ -1,29 +1,30 @@ import { describe, expect, test } from 'vitest' import { createClient, qb } from './lib' -describe('mutations', () => { - test('create', async () => { +describe('mutations in trx', () => { + test('create trx', async () => { const [client, calls] = createClient({ - transaction: { + mut: { ok: true, mut: { ok: true, }, }, }) - const result = await client.mutate(qb.create('Author', { + const createAuthor = qb.transaction(qb.create('Author', { data: { name: 'John', email: 'xx@localhost', }, })) + const result = await client.mutate(createAuthor) expect(result.data.ok).toBe(true) expect(calls).toHaveLength(1) expect(calls[0].query).toMatchInlineSnapshot(` - "mutation($data_AuthorCreateInput_0: AuthorCreateInput!) { - transaction { - mut: createAuthor(data: $data_AuthorCreateInput_0) { + "mutation($options_MutationTransactionOptions_0: MutationTransactionOptions, $data_AuthorCreateInput_1: AuthorCreateInput!) { + mut: transaction(options: $options_MutationTransactionOptions_0) { + mut: createAuthor(data: $data_AuthorCreateInput_1) { ok errorMessage errors { @@ -69,37 +70,38 @@ describe('mutations', () => { `) expect(calls[0].variables).toMatchInlineSnapshot(` { - "data_AuthorCreateInput_0": { + "data_AuthorCreateInput_1": { "email": "xx@localhost", "name": "John", }, + "options_MutationTransactionOptions_0": {}, } `) }) test('mutation with a node', async () => { const [client, calls] = createClient({ - transaction: { + mut: { ok: true, mut: { ok: true, - node: { id: 1 }, + node: { id: 123 }, }, }, }) - const result = await client.mutate(qb.create('Author', { + const result = await client.mutate(qb.transaction(qb.create('Author', { data: { name: 'John', email: 'xx@localhost', }, - }, it => it.$('id'))) - expect(result.data.ok).toBe(true) - expect(result.data.node?.id).toBe(1) + }, it => it.$('id')))) + expect(result.ok).toBe(true) + expect(result.data?.node?.id).toBe(123) expect(calls).toHaveLength(1) expect(calls[0].query).toMatchInlineSnapshot(` - "mutation($data_AuthorCreateInput_0: AuthorCreateInput!) { - transaction { - mut: createAuthor(data: $data_AuthorCreateInput_0) { + "mutation($options_MutationTransactionOptions_0: MutationTransactionOptions, $data_AuthorCreateInput_1: AuthorCreateInput!) { + mut: transaction(options: $options_MutationTransactionOptions_0) { + mut: createAuthor(data: $data_AuthorCreateInput_1) { ok errorMessage errors { @@ -148,35 +150,36 @@ describe('mutations', () => { `) expect(calls[0].variables).toMatchInlineSnapshot(` { - "data_AuthorCreateInput_0": { + "data_AuthorCreateInput_1": { "email": "xx@localhost", "name": "John", }, + "options_MutationTransactionOptions_0": {}, } `) }) test('update', async () => { const [client, calls] = createClient({ - transaction: { + mut: { ok: true, mut: { ok: true, }, }, }) - await client.mutate(qb.update('Author', { + await client.mutate(qb.transaction(qb.update('Author', { by: { id: 1 }, data: { name: 'John', email: 'xx@localhost', }, - })) + }))) expect(calls).toHaveLength(1) expect(calls[0].query).toMatchInlineSnapshot(` - "mutation($by_AuthorUniqueWhere_0: AuthorUniqueWhere!, $data_AuthorUpdateInput_1: AuthorUpdateInput!) { - transaction { - mut: updateAuthor(by: $by_AuthorUniqueWhere_0, data: $data_AuthorUpdateInput_1) { + "mutation($options_MutationTransactionOptions_0: MutationTransactionOptions, $by_AuthorUniqueWhere_1: AuthorUniqueWhere!, $data_AuthorUpdateInput_2: AuthorUpdateInput!) { + mut: transaction(options: $options_MutationTransactionOptions_0) { + mut: updateAuthor(by: $by_AuthorUniqueWhere_1, data: $data_AuthorUpdateInput_2) { ok errorMessage errors { @@ -222,34 +225,35 @@ describe('mutations', () => { `) expect(calls[0].variables).toMatchInlineSnapshot(` { - "by_AuthorUniqueWhere_0": { + "by_AuthorUniqueWhere_1": { "id": 1, }, - "data_AuthorUpdateInput_1": { + "data_AuthorUpdateInput_2": { "email": "xx@localhost", "name": "John", }, + "options_MutationTransactionOptions_0": {}, } `) }) test('delete', async () => { const [client, calls] = createClient({ - transaction: { + mut: { ok: true, mut: { ok: true, }, }, }) - await client.mutate(qb.delete('Author', { + await client.mutate(qb.transaction(qb.delete('Author', { by: { id: 1 }, - })) + }))) expect(calls).toHaveLength(1) expect(calls[0].query).toMatchInlineSnapshot(` - "mutation($by_AuthorUniqueWhere_0: AuthorUniqueWhere!) { - transaction { - mut: deleteAuthor(by: $by_AuthorUniqueWhere_0) { + "mutation($options_MutationTransactionOptions_0: MutationTransactionOptions, $by_AuthorUniqueWhere_1: AuthorUniqueWhere!) { + mut: transaction(options: $options_MutationTransactionOptions_0) { + mut: deleteAuthor(by: $by_AuthorUniqueWhere_1) { ok errorMessage errors { @@ -275,23 +279,24 @@ describe('mutations', () => { `) expect(calls[0].variables).toMatchInlineSnapshot(` { - "by_AuthorUniqueWhere_0": { + "by_AuthorUniqueWhere_1": { "id": 1, }, + "options_MutationTransactionOptions_0": {}, } `) }) test('upsert', async () => { const [client, calls] = createClient({ - transaction: { + mut: { ok: true, mut: { ok: true, }, }, }) - await client.mutate(qb.upsert('Author', { + await client.mutate(qb.transaction(qb.upsert('Author', { by: { id: 1 }, create: { name: 'John', @@ -301,75 +306,76 @@ describe('mutations', () => { name: 'John', email: 'xx@localhost', }, - })) + }))) expect(calls).toHaveLength(1) expect(calls[0].query).toMatchInlineSnapshot(` - "mutation($by_AuthorUniqueWhere_0: AuthorUniqueWhere!, $create_AuthorCreateInput_1: AuthorCreateInput!, $update_AuthorUpdateInput_2: AuthorUpdateInput!) { - transaction { - mut: upsertAuthor(by: $by_AuthorUniqueWhere_0, create: $create_AuthorCreateInput_1, update: $update_AuthorUpdateInput_2) { - ok - errorMessage - errors { - ... MutationError + "mutation($options_MutationTransactionOptions_0: MutationTransactionOptions, $by_AuthorUniqueWhere_1: AuthorUniqueWhere!, $create_AuthorCreateInput_2: AuthorCreateInput!, $update_AuthorUpdateInput_3: AuthorUpdateInput!) { + mut: transaction(options: $options_MutationTransactionOptions_0) { + mut: upsertAuthor(by: $by_AuthorUniqueWhere_1, create: $create_AuthorCreateInput_2, update: $update_AuthorUpdateInput_3) { + ok + errorMessage + errors { + ... MutationError + } + validation { + ... ValidationResult + } + } } - validation { - ... ValidationResult - } - } - } - } - fragment MutationError on _MutationError { - paths { - ... on _FieldPathFragment { - field - } - ... on _IndexPathFragment { - index - alias } - } - message - type - } - fragment ValidationResult on _ValidationResult { - valid - errors { - path { - ... on _FieldPathFragment { - field - } - ... on _IndexPathFragment { - index - alias + fragment MutationError on _MutationError { + paths { + ... on _FieldPathFragment { + field + } + ... on _IndexPathFragment { + index + alias + } } + message + type } - message { - text + fragment ValidationResult on _ValidationResult { + valid + errors { + path { + ... on _FieldPathFragment { + field + } + ... on _IndexPathFragment { + index + alias + } + } + message { + text + } + } } - } - } - " - `) + " + `) expect(calls[0].variables).toMatchInlineSnapshot(` - { - "by_AuthorUniqueWhere_0": { - "id": 1, - }, - "create_AuthorCreateInput_1": { - "email": "xx@localhost", - "name": "John", - }, - "update_AuthorUpdateInput_2": { - "email": "xx@localhost", - "name": "John", - }, - } - `) + { + "by_AuthorUniqueWhere_1": { + "id": 1, + }, + "create_AuthorCreateInput_2": { + "email": "xx@localhost", + "name": "John", + }, + "options_MutationTransactionOptions_0": {}, + "update_AuthorUpdateInput_3": { + "email": "xx@localhost", + "name": "John", + }, + } + `) }) test('multiple mutations as array', async () => { const [client, calls] = createClient({ - transaction: { + mut: { ok: true, mut_0: { ok: true, @@ -379,7 +385,7 @@ describe('mutations', () => { }, }, }) - const result = await client.mutate([ + const result = await client.mutate(qb.transaction([ qb.create('Author', { data: { name: 'John', @@ -392,16 +398,16 @@ describe('mutations', () => { email: 'xx@localhost', }, }), - ]) + ])) expect(result.ok).toBe(true) expect(result.data).toHaveLength(2) expect(result.data[0].ok).toBe(true) expect(result.data[1].ok).toBe(true) expect(calls).toHaveLength(1) expect(calls[0].query).toMatchInlineSnapshot(` - "mutation($data_AuthorCreateInput_0: AuthorCreateInput!, $data_AuthorCreateInput_1: AuthorCreateInput!) { - transaction { - mut_0: createAuthor(data: $data_AuthorCreateInput_0) { + "mutation($options_MutationTransactionOptions_0: MutationTransactionOptions, $data_AuthorCreateInput_1: AuthorCreateInput!, $data_AuthorCreateInput_2: AuthorCreateInput!) { + mut: transaction(options: $options_MutationTransactionOptions_0) { + mut_0: createAuthor(data: $data_AuthorCreateInput_1) { ok errorMessage errors { @@ -411,7 +417,7 @@ describe('mutations', () => { ... ValidationResult } } - mut_1: createAuthor(data: $data_AuthorCreateInput_1) { + mut_1: createAuthor(data: $data_AuthorCreateInput_2) { ok errorMessage errors { @@ -457,21 +463,22 @@ describe('mutations', () => { `) expect(calls[0].variables).toMatchInlineSnapshot(` { - "data_AuthorCreateInput_0": { + "data_AuthorCreateInput_1": { "email": "xx@localhost", "name": "John", }, - "data_AuthorCreateInput_1": { + "data_AuthorCreateInput_2": { "email": "xx@localhost", "name": "John", }, + "options_MutationTransactionOptions_0": {}, } `) }) - test('multiple named mutations', async () => { + test('multiple named mutations trx', async () => { const [client, calls] = createClient({ - transaction: { + mut: { ok: true, createAuthor: { ok: true, @@ -481,7 +488,7 @@ describe('mutations', () => { }, }, }) - const result = await client.mutate({ + const result = await client.mutate(qb.transaction({ createAuthor: qb.create('Author', { data: { name: 'John', @@ -494,14 +501,14 @@ describe('mutations', () => { content: 'World', }, }), - }) + })) expect(result.data.createAuthor.ok).toBe(true) expect(result.data.createPost.ok).toBe(true) expect(calls).toHaveLength(1) expect(calls[0].query).toMatchInlineSnapshot(` - "mutation($data_AuthorCreateInput_0: AuthorCreateInput!, $data_PostCreateInput_1: PostCreateInput!) { - transaction { - createAuthor(data: $data_AuthorCreateInput_0) { + "mutation($options_MutationTransactionOptions_0: MutationTransactionOptions, $data_AuthorCreateInput_1: AuthorCreateInput!, $data_PostCreateInput_2: PostCreateInput!) { + mut: transaction(options: $options_MutationTransactionOptions_0) { + createAuthor(data: $data_AuthorCreateInput_1) { ok errorMessage errors { @@ -511,7 +518,7 @@ describe('mutations', () => { ... ValidationResult } } - createPost(data: $data_PostCreateInput_1) { + createPost(data: $data_PostCreateInput_2) { ok errorMessage errors { @@ -556,22 +563,23 @@ describe('mutations', () => { " `) expect(calls[0].variables).toMatchInlineSnapshot(` - { - "data_AuthorCreateInput_0": { - "email": "xx@localhost", - "name": "John", - }, - "data_PostCreateInput_1": { - "content": "World", - "title": "Hello", - }, - } - `) + { + "data_AuthorCreateInput_1": { + "email": "xx@localhost", + "name": "John", + }, + "data_PostCreateInput_2": { + "content": "World", + "title": "Hello", + }, + "options_MutationTransactionOptions_0": {}, + } + `) }) - test('mutation and query combo', async () => { + test('mutation and query combo trx', async () => { const [client, calls] = createClient({ - transaction: { + mut: { ok: true, createPost: { ok: true, @@ -586,7 +594,7 @@ describe('mutations', () => { }, }) - const result = await client.mutate({ + const trx = qb.transaction({ createPost: qb.create('Post', { data: { title: 'Hello', @@ -597,14 +605,15 @@ describe('mutations', () => { by: { id: 1 }, }, it => it.$$()), }) + const result = await client.mutate(trx) expect(result.data.createPost.ok).toBe(true) expect(result.data.post?.title).toBe('Foo bar') expect(calls).toHaveLength(1) expect(calls[0].query).toMatchInlineSnapshot(` - "mutation($data_PostCreateInput_0: PostCreateInput!, $by_PostUniqueWhere_1: PostUniqueWhere!) { - transaction { - createPost(data: $data_PostCreateInput_0) { + "mutation($options_MutationTransactionOptions_0: MutationTransactionOptions, $data_PostCreateInput_1: PostCreateInput!, $by_PostUniqueWhere_2: PostUniqueWhere!) { + mut: transaction(options: $options_MutationTransactionOptions_0) { + createPost(data: $data_PostCreateInput_1) { ok errorMessage errors { @@ -615,7 +624,7 @@ describe('mutations', () => { } } post: query { - value: getPost(by: $by_PostUniqueWhere_1) { + value: getPost(by: $by_PostUniqueWhere_2) { title content } @@ -654,6 +663,635 @@ describe('mutations', () => { } " `) + expect(calls[0].variables).toMatchInlineSnapshot(` + { + "by_PostUniqueWhere_2": { + "id": 1, + }, + "data_PostCreateInput_1": { + "content": "World", + "title": "Hello", + }, + "options_MutationTransactionOptions_0": {}, + } + `) + }) + +}) +describe('mutations without trx', () => { + test('create', async () => { + const [client, calls] = createClient({ + mut: { + ok: true, + }, + }) + const createAuthor = qb.create('Author', { + data: { + name: 'John', + email: 'xx@localhost', + }, + }) + const result = await client.mutate(createAuthor) + + + expect(result.ok).toBe(true) + expect(calls).toHaveLength(1) + expect(calls[0].query).toMatchInlineSnapshot(` + "mutation($data_AuthorCreateInput_0: AuthorCreateInput!) { + mut: createAuthor(data: $data_AuthorCreateInput_0) { + ok + errorMessage + errors { + ... MutationError + } + validation { + ... ValidationResult + } + } + } + fragment MutationError on _MutationError { + paths { + ... on _FieldPathFragment { + field + } + ... on _IndexPathFragment { + index + alias + } + } + message + type + } + fragment ValidationResult on _ValidationResult { + valid + errors { + path { + ... on _FieldPathFragment { + field + } + ... on _IndexPathFragment { + index + alias + } + } + message { + text + } + } + } + " + `) + expect(calls[0].variables).toMatchInlineSnapshot(` + { + "data_AuthorCreateInput_0": { + "email": "xx@localhost", + "name": "John", + }, + } + `) + }) + + test('mutation with a node', async () => { + const [client, calls] = createClient({ + mut: { + ok: true, + node: { id: 123 }, + }, + }) + const result = await client.mutate(qb.create('Author', { + data: { + name: 'John', + email: 'xx@localhost', + }, + }, it => it.$('id'))) + expect(result.ok).toBe(true) + expect(result.node?.id).toBe(123) + expect(calls).toHaveLength(1) + expect(calls[0].query).toMatchInlineSnapshot(` + "mutation($data_AuthorCreateInput_0: AuthorCreateInput!) { + mut: createAuthor(data: $data_AuthorCreateInput_0) { + ok + errorMessage + errors { + ... MutationError + } + validation { + ... ValidationResult + } + node { + id + } + } + } + fragment MutationError on _MutationError { + paths { + ... on _FieldPathFragment { + field + } + ... on _IndexPathFragment { + index + alias + } + } + message + type + } + fragment ValidationResult on _ValidationResult { + valid + errors { + path { + ... on _FieldPathFragment { + field + } + ... on _IndexPathFragment { + index + alias + } + } + message { + text + } + } + } + " + `) + expect(calls[0].variables).toMatchInlineSnapshot(` + { + "data_AuthorCreateInput_0": { + "email": "xx@localhost", + "name": "John", + }, + } + `) + }) + + test('update', async () => { + const [client, calls] = createClient({ + mut: { + ok: true, + }, + }) + await client.mutate(qb.update('Author', { + by: { id: 1 }, + data: { + name: 'John', + email: 'xx@localhost', + }, + })) + expect(calls).toHaveLength(1) + expect(calls[0].query).toMatchInlineSnapshot(` + "mutation($by_AuthorUniqueWhere_0: AuthorUniqueWhere!, $data_AuthorUpdateInput_1: AuthorUpdateInput!) { + mut: updateAuthor(by: $by_AuthorUniqueWhere_0, data: $data_AuthorUpdateInput_1) { + ok + errorMessage + errors { + ... MutationError + } + validation { + ... ValidationResult + } + } + } + fragment MutationError on _MutationError { + paths { + ... on _FieldPathFragment { + field + } + ... on _IndexPathFragment { + index + alias + } + } + message + type + } + fragment ValidationResult on _ValidationResult { + valid + errors { + path { + ... on _FieldPathFragment { + field + } + ... on _IndexPathFragment { + index + alias + } + } + message { + text + } + } + } + " + `) + expect(calls[0].variables).toMatchInlineSnapshot(` + { + "by_AuthorUniqueWhere_0": { + "id": 1, + }, + "data_AuthorUpdateInput_1": { + "email": "xx@localhost", + "name": "John", + }, + } + `) + }) + + test('delete', async () => { + const [client, calls] = createClient({ + mut: { + ok: true, + }, + }) + await client.mutate(qb.delete('Author', { + by: { id: 1 }, + })) + expect(calls).toHaveLength(1) + expect(calls[0].query).toMatchInlineSnapshot(` + "mutation($by_AuthorUniqueWhere_0: AuthorUniqueWhere!) { + mut: deleteAuthor(by: $by_AuthorUniqueWhere_0) { + ok + errorMessage + errors { + ... MutationError + } + } + } + fragment MutationError on _MutationError { + paths { + ... on _FieldPathFragment { + field + } + ... on _IndexPathFragment { + index + alias + } + } + message + type + } + " + `) + expect(calls[0].variables).toMatchInlineSnapshot(` + { + "by_AuthorUniqueWhere_0": { + "id": 1, + }, + } + `) + }) + + test('upsert', async () => { + const [client, calls] = createClient({ + mut: { + ok: true, + }, + }) + await client.mutate(qb.upsert('Author', { + by: { id: 1 }, + create: { + name: 'John', + email: 'xx@localhost', + }, + update: { + name: 'John', + email: 'xx@localhost', + }, + })) + expect(calls).toHaveLength(1) + expect(calls[0].query).toMatchInlineSnapshot(` + "mutation($by_AuthorUniqueWhere_0: AuthorUniqueWhere!, $create_AuthorCreateInput_1: AuthorCreateInput!, $update_AuthorUpdateInput_2: AuthorUpdateInput!) { + mut: upsertAuthor(by: $by_AuthorUniqueWhere_0, create: $create_AuthorCreateInput_1, update: $update_AuthorUpdateInput_2) { + ok + errorMessage + errors { + ... MutationError + } + validation { + ... ValidationResult + } + } + } + fragment MutationError on _MutationError { + paths { + ... on _FieldPathFragment { + field + } + ... on _IndexPathFragment { + index + alias + } + } + message + type + } + fragment ValidationResult on _ValidationResult { + valid + errors { + path { + ... on _FieldPathFragment { + field + } + ... on _IndexPathFragment { + index + alias + } + } + message { + text + } + } + } + " + `) + expect(calls[0].variables).toMatchInlineSnapshot(` + { + "by_AuthorUniqueWhere_0": { + "id": 1, + }, + "create_AuthorCreateInput_1": { + "email": "xx@localhost", + "name": "John", + }, + "update_AuthorUpdateInput_2": { + "email": "xx@localhost", + "name": "John", + }, + } + `) + }) + + test('multiple mutations as array', async () => { + const [client, calls] = createClient({ + mut_0: { + ok: true, + }, + mut_1: { + ok: true, + }, + }) + const result = await client.mutate([ + qb.create('Author', { + data: { + name: 'John', + email: 'xx@localhost', + }, + }), + qb.create('Author', { + data: { + name: 'John', + email: 'xx@localhost', + }, + }), + ]) + expect(result).toHaveLength(2) + expect(result[0].ok).toBe(true) + expect(result[1].ok).toBe(true) + expect(calls).toHaveLength(1) + expect(calls[0].query).toMatchInlineSnapshot(` + "mutation($data_AuthorCreateInput_0: AuthorCreateInput!, $data_AuthorCreateInput_1: AuthorCreateInput!) { + mut_0: createAuthor(data: $data_AuthorCreateInput_0) { + ok + errorMessage + errors { + ... MutationError + } + validation { + ... ValidationResult + } + } + mut_1: createAuthor(data: $data_AuthorCreateInput_1) { + ok + errorMessage + errors { + ... MutationError + } + validation { + ... ValidationResult + } + } + } + fragment MutationError on _MutationError { + paths { + ... on _FieldPathFragment { + field + } + ... on _IndexPathFragment { + index + alias + } + } + message + type + } + fragment ValidationResult on _ValidationResult { + valid + errors { + path { + ... on _FieldPathFragment { + field + } + ... on _IndexPathFragment { + index + alias + } + } + message { + text + } + } + } + " + `) + expect(calls[0].variables).toMatchInlineSnapshot(` + { + "data_AuthorCreateInput_0": { + "email": "xx@localhost", + "name": "John", + }, + "data_AuthorCreateInput_1": { + "email": "xx@localhost", + "name": "John", + }, + } + `) + }) + + test('multiple named mutations', async () => { + const [client, calls] = createClient({ + createAuthor: { + ok: true, + }, + createPost: { + ok: true, + }, + }) + const result = await client.mutate({ + createAuthor: qb.create('Author', { + data: { + name: 'John', + email: 'xx@localhost', + }, + }), + createPost: qb.create('Post', { + data: { + title: 'Hello', + content: 'World', + }, + }), + }) + expect(result.createAuthor.ok).toBe(true) + expect(result.createPost.ok).toBe(true) + expect(calls).toHaveLength(1) + expect(calls[0].query).toMatchInlineSnapshot(` + "mutation($data_AuthorCreateInput_0: AuthorCreateInput!, $data_PostCreateInput_1: PostCreateInput!) { + createAuthor(data: $data_AuthorCreateInput_0) { + ok + errorMessage + errors { + ... MutationError + } + validation { + ... ValidationResult + } + } + createPost(data: $data_PostCreateInput_1) { + ok + errorMessage + errors { + ... MutationError + } + validation { + ... ValidationResult + } + } + } + fragment MutationError on _MutationError { + paths { + ... on _FieldPathFragment { + field + } + ... on _IndexPathFragment { + index + alias + } + } + message + type + } + fragment ValidationResult on _ValidationResult { + valid + errors { + path { + ... on _FieldPathFragment { + field + } + ... on _IndexPathFragment { + index + alias + } + } + message { + text + } + } + } + " + `) + expect(calls[0].variables).toMatchInlineSnapshot(` + { + "data_AuthorCreateInput_0": { + "email": "xx@localhost", + "name": "John", + }, + "data_PostCreateInput_1": { + "content": "World", + "title": "Hello", + }, + } + `) + }) + + test('mutation and query combo', async () => { + const [client, calls] = createClient({ + createPost: { + ok: true, + }, + post: { + value: { + id: 1, + title: 'Foo bar', + content: 'Hello world', + }, + }, + }) + + const result = await client.mutate({ + createPost: qb.create('Post', { + data: { + title: 'Hello', + content: 'World', + }, + }), + post: qb.get('Post', { + by: { id: 1 }, + }, it => it.$$()), + }) + expect(result.createPost.ok).toBe(true) + expect(result.post?.title).toBe('Foo bar') + + expect(calls).toHaveLength(1) + expect(calls[0].query).toMatchInlineSnapshot(` + "mutation($data_PostCreateInput_0: PostCreateInput!, $by_PostUniqueWhere_1: PostUniqueWhere!) { + createPost(data: $data_PostCreateInput_0) { + ok + errorMessage + errors { + ... MutationError + } + validation { + ... ValidationResult + } + } + post: query { + value: getPost(by: $by_PostUniqueWhere_1) { + title + content + } + } + } + fragment MutationError on _MutationError { + paths { + ... on _FieldPathFragment { + field + } + ... on _IndexPathFragment { + index + alias + } + } + message + type + } + fragment ValidationResult on _ValidationResult { + valid + errors { + path { + ... on _FieldPathFragment { + field + } + ... on _IndexPathFragment { + index + alias + } + } + message { + text + } + } + } + " + `) expect(calls[0].variables).toMatchInlineSnapshot(` { "by_PostUniqueWhere_1": { diff --git a/packages/client-content/tests/cases/unit/query.test.ts b/packages/client-content/tests/cases/unit/query.test.ts index b49a8943c4..33ff6a2a51 100644 --- a/packages/client-content/tests/cases/unit/query.test.ts +++ b/packages/client-content/tests/cases/unit/query.test.ts @@ -6,10 +6,33 @@ import OrderDirection = Input.OrderDirection; describe('queries', () => { test('list', async () => { - const [client, calls] = createClient() + const [client, calls] = createClient({ + authors: [ + { + name: 'John', + email: 'foo@localhost', + }, + { + name: 'John', + email: 'bar@localhost', + }, + ], + }) const result = await client.query({ authors: qb.list('Author', {}, it => it.$$()), }) + expect(result).toEqual({ + authors: [ + { + name: 'John', + email: 'foo@localhost', + }, + { + name: 'John', + email: 'bar@localhost', + }, + ], + }) expect(calls).toHaveLength(1) expect(calls[0].query).toMatchInlineSnapshot(` "query { @@ -24,11 +47,54 @@ describe('queries', () => { }) test('multiple queries', async () => { - const [client, calls] = createClient() + const [client, calls] = createClient({ + authors: [ + { + name: 'John', + email: 'foo@localhost', + }, + { + name: 'John', + email: 'bar@localhost', + }, + ], + posts: [ + { + title: 'Post 1', + content: 'Content 1', + }, + { + title: 'Post 2', + content: 'Content 2', + }, + ], + }) const result = await client.query({ authors: qb.list('Author', {}, it => it.$$()), posts: qb.list('Post', {}, it => it.$$()), }) + expect(result).toEqual({ + authors: [ + { + name: 'John', + email: 'foo@localhost', + }, + { + name: 'John', + email: 'bar@localhost', + }, + ], + posts: [ + { + title: 'Post 1', + content: 'Content 1', + }, + { + title: 'Post 2', + content: 'Content 2', + }, + ], + }) expect(calls).toHaveLength(1) expect(calls[0].query).toMatchInlineSnapshot(` "query { @@ -48,8 +114,28 @@ describe('queries', () => { test('single query', async () => { - const [client, calls] = createClient() - await client.query(qb.list('Post', {}, it => it.$$())) + const [client, calls] = createClient({ value: [ + { + title: 'Post 1', + content: 'Content 1', + }, + { + title: 'Post 2', + content: 'Content 2', + }, + ] }) + + const result = await client.query(qb.list('Post', {}, it => it.$$())) + expect(result).toEqual([ + { + title: 'Post 1', + content: 'Content 1', + }, + { + title: 'Post 2', + content: 'Content 2', + }, + ]) expect(calls).toHaveLength(1) expect(calls[0].query).toMatchInlineSnapshot(` "query { From 2243277ce2b559d2cfff44f3282bc6c5a3780bb4 Mon Sep 17 00:00:00 2001 From: David Matejka Date: Wed, 6 Dec 2023 15:55:04 +0100 Subject: [PATCH 17/17] chore: ae up --- build/api/binding.api.md | 144 +----- build/api/client-content-generator.api.md | 37 ++ build/api/client-content.api.md | 523 ++++++++++++++++++++++ build/api/client-generator.api.md | 37 ++ build/api/client.api.md | 124 ++--- build/api/graphql-builder.api.md | 75 ++++ build/api/graphql-client.api.md | 72 +++ build/api/react-binding.api.md | 6 +- 8 files changed, 800 insertions(+), 218 deletions(-) create mode 100644 build/api/client-content-generator.api.md create mode 100644 build/api/client-content.api.md create mode 100644 build/api/client-generator.api.md create mode 100644 build/api/graphql-builder.api.md create mode 100644 build/api/graphql-client.api.md diff --git a/build/api/binding.api.md b/build/api/binding.api.md index d11a82430c..f5655b1c4d 100644 --- a/build/api/binding.api.md +++ b/build/api/binding.api.md @@ -4,17 +4,20 @@ ```ts +import { ContentQueryBuilder } from '@contember/client'; import type { CrudQueryBuilder } from '@contember/client'; import { EmbeddedActionsParser } from 'chevrotain'; import { v4 as generateUuid } from 'uuid'; import { GraphQlBuilder } from '@contember/client'; -import type { GraphQlClient } from '@contember/client'; -import type { GraphQlClientFailedRequestMetadata } from '@contember/client'; +import { GraphQlClient } from '@contember/client'; +import { GraphQlClientError } from '@contember/client'; import type { GraphQlClientRequestOptions } from '@contember/client'; import { GraphQlLiteral } from '@contember/client'; import { Input } from '@contember/client'; +import { MutationResult } from '@contember/client'; import type { Result } from '@contember/client'; import { TokenType } from 'chevrotain'; +import { TransactionResult } from '@contember/client'; import type { TreeFilter } from '@contember/client'; // @public (undocumented) @@ -112,9 +115,12 @@ export class Config { setValue(name: Name, value: BindingConfig[Name]): this; } +// @public (undocumented) +export const createQueryBuilder: (schema: Schema) => ContentQueryBuilder; + // @public (undocumented) export class DataBinding { - constructor(contentApiClient: GraphQlClient, systemApiClient: GraphQlClient, tenantApiClient: GraphQlClient, treeStore: TreeStore, environment: Environment, createMarkerTree: (node: Node, environment: Environment) => MarkerTreeRoot, batchedUpdates: (callback: () => any) => void, onUpdate: (newData: TreeRootAccessor, binding: DataBinding) => void, onError: (error: RequestError, binding: DataBinding) => void, onPersistSuccess: (result: SuccessfulPersistResult, binding: DataBinding) => void, options: { + constructor(contentApiClient: GraphQlClient, systemApiClient: GraphQlClient, tenantApiClient: GraphQlClient, treeStore: TreeStore, environment: Environment, createMarkerTree: (node: Node, environment: Environment) => MarkerTreeRoot, batchedUpdates: (callback: () => any) => void, onUpdate: (newData: TreeRootAccessor, binding: DataBinding) => void, onError: (error: GraphQlClientError, binding: DataBinding) => void, onPersistSuccess: (result: SuccessfulPersistResult, binding: DataBinding) => void, options: { skipStateUpdateAfterPersist: boolean; }); // (undocumented) @@ -124,6 +130,9 @@ export class DataBinding { // @public (undocumented) export const DataBindingExtendAborted: unique symbol; +// @public (undocumented) +export type DataBindingTransactionResult = TransactionResult>>; + // @public (undocumented) export class DirtinessTracker { // (undocumented) @@ -706,10 +715,7 @@ export namespace ErrorAccessor { } // @public (undocumented) -export type ErrorPathNodeType = FieldPathErrorFragment | IndexPathErrorFragment; - -// @public (undocumented) -export type ErrorPersistResult = InvalidInputPersistResult | RequestError | InvalidResponseResult; +export type ErrorPersistResult = InvalidInputPersistResult | InvalidResponseResult; // Warning: (ae-forgotten-export) The symbol "GenericEventsMap" needs to be exported by the entry point index.d.ts // @@ -788,16 +794,6 @@ export class EventManager { triggerOnPersistSuccess(options: PersistSuccessOptions): Promise; } -// @public (undocumented) -export interface ExecutionError { - // (undocumented) - message: string | null; - // (undocumented) - path: MutationErrorPath; - // (undocumented) - type: Result.ExecutionErrorType; -} - // @public (undocumented) export type ExpectedEntityCount = 'upToOne' | 'possiblyMany'; @@ -931,14 +927,6 @@ export class FieldMarker { // @public (undocumented) export type FieldName = string; -// @public (undocumented) -export interface FieldPathErrorFragment { - // (undocumented) - __typename: '_FieldPathFragment'; - // (undocumented) - field: string; -} - // @public (undocumented) export type FieldValue = JsonValue; @@ -956,19 +944,6 @@ export type GetEntityListSubTree = (parametersOrAlias: Alias | SugaredQualifiedE // @public (undocumented) export type GetEntitySubTree = (parametersOrAlias: Alias | SugaredQualifiedSingleEntity | SugaredUnconstrainedQualifiedSingleEntity, treeId?: TreeRootId, environment?: Environment) => EntityAccessor; -// @public (undocumented) -export interface GqlError { - // (undocumented) - errors: { - message: string; - path?: string[]; - }[]; - // (undocumented) - query: string; - // (undocumented) - type: 'gqlError'; -} - // @public (undocumented) export interface HasManyRelation extends Relation, EntityListParameters, EntityListEventListeners { } @@ -1009,16 +984,6 @@ export class HasOneRelationMarker { readonly placeholderName: string; } -// @public (undocumented) -export interface IndexPathErrorFragment { - // (undocumented) - __typename: '_IndexPathFragment'; - // (undocumented) - alias: string | null; - // (undocumented) - index: number; -} - // @public (undocumented) export interface InvalidInputPersistResult { // (undocumented) @@ -1154,64 +1119,6 @@ export class MarkerTreeRoot { // @public (undocumented) export type MeaningfulMarker = FieldMarker | HasOneRelationMarker | HasManyRelationMarker | EntityListSubTreeMarker | EntitySubTreeMarker; -// @public (undocumented) -export const metadataToRequestError: (metadata: GraphQlClientFailedRequestMetadata) => RequestError; - -// @public (undocumented) -export interface MutationDataResponse { - // (undocumented) - [alias: string]: MutationResponse; -} - -// @public (undocumented) -export type MutationErrorPath = ErrorPathNodeType[]; - -// @public (undocumented) -export interface MutationRequestResponse { - // (undocumented) - data: MutationTransactionResponse | null; - // (undocumented) - errors?: { - message: string; - path?: string[]; - }[]; -} - -// @public (undocumented) -export interface MutationResponse { - // (undocumented) - errorMessage: string | null; - // (undocumented) - errors: ExecutionError[]; - // (undocumented) - node: ReceivedEntityData; - // (undocumented) - ok: boolean; - // (undocumented) - validation: { - valid: boolean; - errors: ValidationError[]; - } | undefined; -} - -// @public (undocumented) -export interface MutationTransactionResponse { - // (undocumented) - transaction: (MutationDataResponse & { - __typename: 'MutationTransaction'; - ok: boolean; - errorMessage: string | null; - }); -} - -// @public (undocumented) -export interface NetworkErrorRequestError { - // (undocumented) - metadata: GraphQlClientFailedRequestMetadata; - // (undocumented) - type: 'networkError'; -} - // @public (undocumented) export const NIL_UUID = "00000000-0000-0000-0000-000000000000"; @@ -1538,9 +1445,6 @@ export type RelativeSingleField = AnyField & LeafField & { // @public (undocumented) export type RemovalType = 'disconnect' | 'delete'; -// @public (undocumented) -export type RequestError = UnauthorizedRequestError | NetworkErrorRequestError | GqlError | UnknownErrorRequestError; - // @public (undocumented) export type RuntimeId = ServerId | ClientGeneratedUuid | UnpersistedEntityDummyId; @@ -2055,12 +1959,6 @@ export class TreeStore { // @public (undocumented) export const TYPENAME_KEY_NAME = "__typename"; -// @public (undocumented) -export interface UnauthorizedRequestError { - // (undocumented) - type: 'unauthorized'; -} - // @public (undocumented) export interface UnconstrainedQualifiedEntityList extends QualifiedEntityParameters, EntityCreationParameters, EntityListEventListeners, EntityListPreferences { // (undocumented) @@ -2089,12 +1987,6 @@ export type UniqueEntityId = string & { // @public (undocumented) export type UniqueWhere = Input.UniqueWhere; -// @public (undocumented) -export interface UnknownErrorRequestError { - // (undocumented) - type: 'unknownError'; -} - // @public (undocumented) export class UnpersistedEntityDummyId implements RuntimeIdSpec { constructor(); @@ -2229,16 +2121,6 @@ export interface UnsugarableUnconstrainedQualifiedSingleEntity extends Unsugarab isUnpersisted?: boolean; } -// @public (undocumented) -export interface ValidationError { - // (undocumented) - message: { - text: string; - }; - // (undocumented) - path: MutationErrorPath; -} - // @public (undocumented) export class VariableFieldValue { constructor(variableName: string); diff --git a/build/api/client-content-generator.api.md b/build/api/client-content-generator.api.md new file mode 100644 index 0000000000..b28564f613 --- /dev/null +++ b/build/api/client-content-generator.api.md @@ -0,0 +1,37 @@ +## API Report File for "@contember/client-content-generator" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { Model } from '@contember/schema'; +import { SchemaNames } from '@contember/client-content'; + +// @public (undocumented) +export class ContemberClientGenerator { + constructor(nameSchemaGenerator?: NameSchemaGenerator, enumTypeSchemaGenerator?: EnumTypeSchemaGenerator, entityTypeSchemaGenerator?: EntityTypeSchemaGenerator); + // (undocumented) + generate(model: Model.Schema): Record; +} + +// @public (undocumented) +export class EntityTypeSchemaGenerator { + // (undocumented) + generate(model: Model.Schema): string; +} + +// @public (undocumented) +export class EnumTypeSchemaGenerator { + // (undocumented) + generate(model: Model.Schema): string; +} + +// @public (undocumented) +export class NameSchemaGenerator { + // (undocumented) + generate(model: Model.Schema): SchemaNames; +} + +// (No @packageDocumentation comment for this package) + +``` diff --git a/build/api/client-content.api.md b/build/api/client-content.api.md new file mode 100644 index 0000000000..f81165f75c --- /dev/null +++ b/build/api/client-content.api.md @@ -0,0 +1,523 @@ +## API Report File for "@contember/client-content" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { GraphQlClientRequestOptions } from '@contember/graphql-client'; +import { GraphQlFieldTypedArgs } from '@contember/graphql-builder'; +import { GraphQlSelectionSet } from '@contember/graphql-builder'; +import { Input } from '@contember/schema'; +import { JSONObject } from '@contember/schema'; +import { Result } from '@contember/schema'; + +// @public (undocumented) +export class ContentClient { + constructor(executor: QueryExecutor); + // (undocumented) + mutate(mutation: ContentMutation, options?: QueryExecutorOptions): Promise; + // (undocumented) + mutate(mutations: ContentMutation[], options?: QueryExecutorOptions): Promise; + // (undocumented) + mutate>(mutations: { + [K in keyof Values]: ContentMutation | ContentQuery; + }, options?: QueryExecutorOptions): Promise; + // (undocumented) + query(query: ContentQuery, options?: QueryExecutorOptions): Promise; + // (undocumented) + query>(queries: { + [K in keyof Values]: ContentQuery; + }, options?: QueryExecutorOptions): Promise; +} + +// @public (undocumented) +export namespace ContentClientInput { + // (undocumented) + export type AnyListQueryInput = Omit & { + readonly orderBy?: AnyOrderBy; + }; + // (undocumented) + export type AnyOrderBy = Input.OrderBy<`${Input.OrderDirection}`>[]; + // (undocumented) + export type ConnectOrCreateInput = { + readonly connect: UniqueWhere; + readonly create: CreateDataInput; + }; + // (undocumented) + export type ConnectOrCreateRelationInput = { + readonly connectOrCreate: ConnectOrCreateInput; + }; + // (undocumented) + export type ConnectRelationInput = { + readonly connect: UniqueWhere; + }; + // (undocumented) + export type CreateDataInput = { + readonly [key in keyof TEntity['columns']]?: TEntity['columns'][key]['tsType']; + } & { + readonly [key in keyof TEntity['hasMany']]?: CreateManyRelationInput; + } & { + readonly [key in keyof TEntity['hasOne']]?: CreateOneRelationInput; + }; + // (undocumented) + export type CreateInput = { + readonly data: CreateDataInput; + }; + // (undocumented) + export type CreateManyRelationInput = readonly CreateOneRelationInput[]; + // (undocumented) + export type CreateOneRelationInput = ConnectRelationInput | CreateRelationInput | ConnectOrCreateRelationInput; + // (undocumented) + export type CreateRelationInput = { + readonly create: CreateDataInput; + }; + // (undocumented) + export type DeleteInput = { + readonly by: UniqueWhere; + readonly filter?: Where; + }; + // (undocumented) + export type DeleteRelationInput = { + readonly delete: true; + }; + // (undocumented) + export type DeleteSpecifiedRelationInput = { + readonly delete: UniqueWhere; + }; + // (undocumented) + export type DisconnectRelationInput = { + readonly disconnect: true; + }; + // (undocumented) + export type DisconnectSpecifiedRelationInput = { + readonly disconnect: UniqueWhere; + }; + // (undocumented) + export type FieldOrderBy = { + readonly [key in keyof TEntity['columns']]?: `${Input.OrderDirection}` | null; + } & { + readonly [key in keyof TEntity['hasMany']]?: FieldOrderBy | null; + } & { + readonly [key in keyof TEntity['hasOne']]?: FieldOrderBy | null; + }; + // (undocumented) + export type HasManyByRelationInput = { + readonly by: TUnique; + readonly filter?: Where; + }; + // (undocumented) + export type HasManyRelationInput = ListQueryInput; + // (undocumented) + export type HasManyRelationPaginateInput = PaginationQueryInput; + // (undocumented) + export type HasOneRelationInput = { + readonly filter?: Where; + }; + // (undocumented) + export type ListQueryInput = { + readonly filter?: Where; + readonly orderBy?: readonly OrderBy[]; + readonly offset?: number; + readonly limit?: number; + }; + // (undocumented) + export type OrderBy = { + readonly _random?: boolean; + readonly _randomSeeded?: number; + } & FieldOrderBy; + // (undocumented) + export type PaginationQueryInput = { + readonly filter?: Where; + readonly orderBy?: readonly OrderBy[]; + readonly skip?: number; + readonly first?: number; + }; + // (undocumented) + export type UniqueQueryInput = { + readonly by: UniqueWhere; + readonly filter?: Where; + }; + // (undocumented) + export type UniqueWhere = TEntity['unique']; + // (undocumented) + export type UpdateDataInput = { + readonly [key in keyof TEntity['columns']]?: TEntity['columns'][key]['tsType']; + } & { + readonly [key in keyof TEntity['hasMany']]?: UpdateManyRelationInput; + } & { + readonly [key in keyof TEntity['hasOne']]?: UpdateOneRelationInput; + }; + // (undocumented) + export type UpdateInput = { + readonly by: UniqueWhere; + readonly filter?: Where; + readonly data: UpdateDataInput; + }; + // (undocumented) + export type UpdateManyRelationInput = Array>; + // (undocumented) + export type UpdateManyRelationInputItem = CreateRelationInput | ConnectRelationInput | ConnectOrCreateRelationInput | DeleteSpecifiedRelationInput | DisconnectSpecifiedRelationInput | UpdateSpecifiedRelationInput | UpsertSpecifiedRelationInput; + // (undocumented) + export type UpdateOneRelationInput = CreateRelationInput | ConnectRelationInput | ConnectOrCreateRelationInput | DeleteRelationInput | DisconnectRelationInput | UpdateRelationInput | UpsertRelationInput; + // (undocumented) + export type UpdateRelationInput = { + readonly update: UpdateDataInput; + }; + // (undocumented) + export type UpdateSpecifiedRelationInput = { + readonly update: { + readonly by: UniqueWhere; + readonly data: UpdateDataInput; + }; + }; + // (undocumented) + export type UpsertInput = { + readonly by: UniqueWhere; + readonly filter?: Where; + readonly update: UpdateDataInput; + readonly create: CreateDataInput; + }; + // (undocumented) + export type UpsertRelationInput = { + readonly upsert: { + readonly update: UpdateDataInput; + readonly create: CreateDataInput; + }; + }; + // (undocumented) + export type UpsertSpecifiedRelationInput = { + readonly upsert: { + readonly by: UniqueWhere; + readonly update: UpdateDataInput; + readonly create: CreateDataInput; + }; + }; + // (undocumented) + export type Where = { + readonly and?: (readonly (Where)[]) | null; + readonly or?: (readonly (Where)[]) | null; + readonly not?: Where | null; + } & { + readonly [key in keyof TEntity['columns']]?: Input.Condition | null; + } & { + readonly [key in keyof TEntity['hasMany']]?: Where | null; + } & { + readonly [key in keyof TEntity['hasOne']]?: Where | null; + }; +} + +// @public (undocumented) +export class ContentEntitySelection { + // (undocumented) + $$(): ContentEntitySelection; + // (undocumented) + $(field: string, args?: EntitySelectionColumnArgs): ContentEntitySelection; + // (undocumented) + $(field: string, args: EntitySelectionManyArgs, selection: ContentEntitySelectionOrCallback): ContentEntitySelection; + // (undocumented) + $(field: string, args: EntitySelectionManyByArgs, selection: ContentEntitySelectionOrCallback): ContentEntitySelection; + // (undocumented) + $(field: string, args: EntitySelectionOneArgs, selection: ContentEntitySelectionOrCallback): ContentEntitySelection; + // (undocumented) + $(field: string, selection: ContentEntitySelectionOrCallback): ContentEntitySelection; + // @internal + constructor( + context: ContentEntitySelectionContext, + selectionSet: GraphQlSelectionSet); + // @internal (undocumented) + readonly context: ContentEntitySelectionContext; + // @internal (undocumented) + readonly selectionSet: GraphQlSelectionSet; +} + +// @public (undocumented) +export type ContentEntitySelectionCallback = (select: ContentEntitySelection) => ContentEntitySelection; + +// @internal (undocumented) +export type ContentEntitySelectionContext = { + entity: SchemaEntityNames; + schema: SchemaNames; +}; + +// @public (undocumented) +export type ContentEntitySelectionOrCallback = ContentEntitySelectionCallback | ContentEntitySelection; + +// @public (undocumented) +export type ContentMutation = ContentOperation; + +// @public (undocumented) +export class ContentOperation { + // @internal + constructor( + type: TType, + fieldName: string, + args?: GraphQlFieldTypedArgs, + selection?: GraphQlSelectionSet | undefined, + parse?: (value: any) => TValue); + // @internal (undocumented) + readonly args: GraphQlFieldTypedArgs; + // @internal (undocumented) + readonly fieldName: string; + // @internal (undocumented) + readonly parse: (value: any) => TValue; + // @internal (undocumented) + readonly selection?: GraphQlSelectionSet | undefined; + // @internal (undocumented) + readonly type: TType; +} + +// @public (undocumented) +export type ContentQuery = ContentOperation; + +// @public (undocumented) +export class ContentQueryBuilder { + constructor(schema: SchemaNames); + // (undocumented) + count(name: string, args: Pick): ContentQuery; + // (undocumented) + create(name: string, args: Input.CreateInput, fields?: EntitySelectionOrCallback): ContentMutation; + // (undocumented) + delete(name: string, args: Input.UniqueQueryInput, fields?: EntitySelectionOrCallback): ContentMutation; + // (undocumented) + fragment(name: string, fieldsCallback?: ContentEntitySelectionCallback): ContentEntitySelection; + // (undocumented) + get(name: string, args: Input.UniqueQueryInput, fields: EntitySelectionOrCallback): ContentQuery | null>; + // (undocumented) + list(name: string, args: ContentClientInput.AnyListQueryInput, fields: EntitySelectionOrCallback): ContentQuery; + // (undocumented) + transaction(input: Record | ContentQuery> | ContentMutation | ContentMutation[], options?: MutationTransactionOptions): ContentMutation>; + // (undocumented) + update(name: string, args: Input.UpdateInput, fields?: EntitySelectionOrCallback): ContentMutation; + // (undocumented) + upsert(name: string, args: Input.UpsertInput, fields?: EntitySelectionOrCallback): ContentMutation; +} + +// @public (undocumented) +export type EntitySelectionAnyArgs = EntitySelectionColumnArgs | EntitySelectionManyArgs | EntitySelectionManyByArgs | EntitySelectionOneArgs; + +// @public (undocumented) +export type EntitySelectionColumnArgs = EntitySelectionCommonInput; + +// @public (undocumented) +export type EntitySelectionCommonInput = { + as?: Alias; +}; + +// @public (undocumented) +export type EntitySelectionManyArgs = ContentClientInput.AnyListQueryInput & EntitySelectionCommonInput; + +// @public (undocumented) +export type EntitySelectionManyByArgs = { + by: Input.UniqueWhere; + filter?: Input.Where; +} & EntitySelectionCommonInput; + +// @public (undocumented) +export type EntitySelectionOneArgs = { + filter?: Input.Where; +} & EntitySelectionCommonInput; + +// @public (undocumented) +export type EntitySelectionOrCallback = ContentEntitySelection | ContentEntitySelectionCallback; + +// @public (undocumented) +export type EntityTypeLike = { + name: string; + unique: JSONObject; + columns: { + [columnName: string]: any; + }; + hasOne: { + [relationName: string]: EntityTypeLike; + }; + hasMany: { + [relationName: string]: EntityTypeLike; + }; + hasManyBy: { + [relationName: string]: { + entity: EntityTypeLike; + by: JSONObject; + }; + }; +}; + +// @public (undocumented) +export type FieldPath = { + readonly field: string; +}; + +// @public (undocumented) +export type IndexPath = { + readonly index: number; + readonly alias: string | null; +}; + +// @public (undocumented) +export type MutationError = { + readonly paths: Path[]; + readonly message: string; + readonly type: Result.ExecutionErrorType; +}; + +// @public (undocumented) +export type MutationResult = { + readonly ok: boolean; + readonly errorMessage: string | null; + readonly errors: MutationError[]; + readonly node: Value | null; + readonly validation?: ValidationResult; +}; + +// @public (undocumented) +export type MutationTransactionOptions = { + deferForeignKeyConstraints?: boolean; + deferUniqueConstraints?: boolean; +}; + +// @public (undocumented) +export type Path = Array; + +// @public (undocumented) +export type QueryExecutor = (query: string, options: GraphQlClientRequestOptions) => Promise; + +// @public (undocumented) +export type QueryExecutorOptions = GraphQlClientRequestOptions; + +// @public (undocumented) +export type SchemaEntityNames = { + readonly name: Name; + readonly scalars: string[]; + readonly fields: { + readonly [fieldName: string]: { + readonly type: 'column'; + } | { + readonly type: 'many' | 'one'; + readonly entity: string; + }; + }; +}; + +// @public (undocumented) +export type SchemaNames = { + entities: { + [entityName: string]: SchemaEntityNames; + }; +}; + +// @public (undocumented) +export type SchemaTypeLike = { + entities: { + [entityName: string]: EntityTypeLike; + }; +}; + +// @public (undocumented) +export type TransactionResult = { + readonly ok: boolean; + readonly errorMessage: string | null; + readonly errors: MutationError[]; + readonly validation: ValidationResult; + readonly data: V; +}; + +// @public (undocumented) +export type TypedContentEntitySelectionOrCallback = TypedEntitySelection | TypedEntitySelectionCallback; + +// @public (undocumented) +export interface TypedContentQueryBuilder { + // (undocumented) + count(name: EntityName, args: Pick, 'filter'>): ContentQuery; + // (undocumented) + create(name: EntityName, args: ContentClientInput.CreateInput, fields?: TypedContentEntitySelectionOrCallback): ContentMutation>; + // (undocumented) + delete(name: EntityName, args: ContentClientInput.UniqueQueryInput, fields?: TypedContentEntitySelectionOrCallback): ContentMutation>; + // (undocumented) + fragment(name: EntityName): TypedEntitySelection; + // (undocumented) + fragment(name: EntityName, fieldsCallback: TypedEntitySelectionCallback): TypedEntitySelection; + // (undocumented) + get(name: EntityName, args: ContentClientInput.UniqueQueryInput, fields: TypedContentEntitySelectionOrCallback): ContentQuery; + // (undocumented) + list(name: EntityName, args: ContentClientInput.ListQueryInput, fields: TypedContentEntitySelectionOrCallback): ContentQuery; + // (undocumented) + transaction(mutation: ContentMutation, options?: MutationTransactionOptions): ContentMutation>; + // (undocumented) + transaction(mutations: ContentMutation[], options?: MutationTransactionOptions): ContentMutation>; + // (undocumented) + transaction>(mutations: { + [K in keyof Values]: ContentMutation | ContentQuery; + }, options?: MutationTransactionOptions): ContentMutation>; + // (undocumented) + update(name: EntityName, args: ContentClientInput.UpdateInput, fields?: TypedContentEntitySelectionOrCallback): ContentMutation>; + // (undocumented) + upsert(name: EntityName, args: ContentClientInput.UpsertInput, fields?: TypedContentEntitySelectionOrCallback): ContentMutation>; +} + +// @public (undocumented) +export interface TypedEntitySelection { + // (undocumented) + $$(): TypedEntitySelection; + // (undocumented) + $(name: TKey, args?: { + as?: TAlias; + }): TypedEntitySelection; + // (undocumented) + $(name: TNestedKey, args: ContentClientInput.HasManyRelationInput & { + as?: TAlias; + }, fields: TypedEntitySelectionCallback | TypedEntitySelection): TypedEntitySelection; + // (undocumented) + $(name: TNestedKey, fields: TypedEntitySelectionCallback | TypedEntitySelection): TypedEntitySelection; + // (undocumented) + $(name: TNestedKey, args: ContentClientInput.HasManyByRelationInput & { + as?: TAlias; + }, fields: TypedEntitySelectionCallback | TypedEntitySelection): TypedEntitySelection; + // (undocumented) + $(name: TNestedKey, fields: TypedEntitySelectionCallback | TypedEntitySelection): TypedEntitySelection; + // (undocumented) + $(name: TNestedKey, args: ContentClientInput.HasOneRelationInput & { + as?: TAlias; + }, fields: TypedEntitySelectionCallback | TypedEntitySelection): TypedEntitySelection; + // (undocumented) + $(name: TNestedKey, fields: TypedEntitySelectionCallback | TypedEntitySelection): TypedEntitySelection; +} + +// @public (undocumented) +export type TypedEntitySelectionCallback = (select: TypedEntitySelection) => TypedEntitySelection; + +// @public (undocumented) +export type ValidationError = { + readonly path: Path; + readonly message: { + text: string; + }; +}; + +// @public (undocumented) +export type ValidationResult = { + readonly valid: boolean; + readonly errors: ValidationError[]; +}; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/build/api/client-generator.api.md b/build/api/client-generator.api.md new file mode 100644 index 0000000000..f9fc3082c8 --- /dev/null +++ b/build/api/client-generator.api.md @@ -0,0 +1,37 @@ +## API Report File for "@contember/client-generator" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { Model } from '@contember/schema'; +import { SchemaNames } from '@contember/client'; + +// @public (undocumented) +export class ContemberClientGenerator { + constructor(nameSchemaGenerator?: NameSchemaGenerator, enumTypeSchemaGenerator?: EnumTypeSchemaGenerator, entityTypeSchemaGenerator?: EntityTypeSchemaGenerator); + // (undocumented) + generate(model: Model.Schema): Record; +} + +// @public (undocumented) +export class EntityTypeSchemaGenerator { + // (undocumented) + generate(model: Model.Schema): string; +} + +// @public (undocumented) +export class EnumTypeSchemaGenerator { + // (undocumented) + generate(model: Model.Schema): string; +} + +// @public (undocumented) +export class NameSchemaGenerator { + // (undocumented) + generate(model: Model.Schema): SchemaNames; +} + +// (No @packageDocumentation comment for this package) + +``` diff --git a/build/api/client.api.md b/build/api/client.api.md index a8e3b9897f..fdfa17dc65 100644 --- a/build/api/client.api.md +++ b/build/api/client.api.md @@ -4,6 +4,13 @@ ```ts +import { GraphQlClient as GraphQlClient_2 } from '@contember/graphql-client'; +import { GraphQlClientError } from '@contember/graphql-client'; +import { GraphQlClientRequestOptions as GraphQlClientRequestOptions_2 } from '@contember/graphql-client'; +import { GraphQlClientVariables } from '@contember/graphql-client'; +import { GraphQlErrorRequest } from '@contember/graphql-client'; +import { GraphQlErrorType } from '@contember/graphql-client'; +import { GraphQlPrintResult } from '@contember/graphql-builder'; import { Input } from '@contember/schema'; import { Result } from '@contember/schema'; import { Value } from '@contember/schema'; @@ -11,53 +18,10 @@ import { Writable } from '@contember/schema'; // @public (undocumented) export namespace CrudQueryBuilder { - import CrudQueryBuilder = CrudQueryBuilderTmp.CrudQueryBuilder; - import ReadBuilder = CrudQueryBuilderTmp.ReadBuilder; - import WriteBuilder = CrudQueryBuilderTmp.WriteBuilder; - import WriteDataBuilder = CrudQueryBuilderTmp.WriteDataBuilder; - import WriteManyRelationBuilder = CrudQueryBuilderTmp.WriteManyRelationBuilder; - import WriteOneRelationBuilder = CrudQueryBuilderTmp.WriteOneRelationBuilder; - import WriteOperation = CrudQueryBuilderTmp.WriteOperation; // Warning: (ae-forgotten-export) The symbol "CrudQueryBuilderTmp" needs to be exported by the entry point index.d.ts // // (undocumented) - export type CreateMutationArguments = CrudQueryBuilderTmp.CreateMutationArguments; - // (undocumented) - export type CreateMutationFields = CrudQueryBuilderTmp.CreateMutationFields; - // (undocumented) - export type DeleteMutationArguments = CrudQueryBuilderTmp.DeleteMutationArguments; - // (undocumented) - export type DeleteMutationFields = CrudQueryBuilderTmp.DeleteMutationFields; - // (undocumented) - export type GetQueryArguments = CrudQueryBuilderTmp.GetQueryArguments; - // (undocumented) - export type HasManyArguments = CrudQueryBuilderTmp.HasManyArguments; - // (undocumented) - export type HasOneArguments = CrudQueryBuilderTmp.HasOneArguments; - // (undocumented) - export type ListQueryArguments = CrudQueryBuilderTmp.ListQueryArguments; - // (undocumented) - export type Mutations = CrudQueryBuilderTmp.Mutations; - // (undocumented) export type OrderDirection = CrudQueryBuilderTmp.OrderDirection; - // (undocumented) - export type PaginateQueryArguments = CrudQueryBuilderTmp.PaginateQueryArguments; - // (undocumented) - export type Queries = CrudQueryBuilderTmp.Queries; - // (undocumented) - export type ReadArguments = CrudQueryBuilderTmp.ReadArguments; - // (undocumented) - export type ReductionArguments = CrudQueryBuilderTmp.ReductionArguments; - // (undocumented) - export type UpdateMutationArguments = CrudQueryBuilderTmp.UpdateMutationArguments; - // (undocumented) - export type UpdateMutationFields = CrudQueryBuilderTmp.UpdateMutationFields; - // (undocumented) - export type WriteArguments = CrudQueryBuilderTmp.WriteArguments; - // (undocumented) - export type WriteFields = CrudQueryBuilderTmp.WriteFields; - // (undocumented) - export type WriteRelationOps = CrudQueryBuilderTmp.WriteRelationOps; } // @public (undocumented) @@ -109,33 +73,25 @@ export const formatSystemApiRelativeUrl: (projectSlug: string) => string; // @public (undocumented) export class GenerateUploadUrlMutationBuilder { - // (undocumented) - static buildQuery(parameters: GenerateUploadUrlMutationBuilder.MutationParameters): string; + // @internal (undocumented) + static buildQuery(parameters: GenerateUploadUrlMutationBuilder.MutationParameters): GraphQlPrintResult; } // @public (undocumented) export namespace GenerateUploadUrlMutationBuilder { // (undocumented) - export type Acl = GraphQlLiteral<'PUBLIC_READ' | 'PRIVATE' | 'NONE'>; + export type Acl = 'PUBLIC_READ' | 'PRIVATE' | 'NONE' | GraphQlLiteral<'PUBLIC_READ' | 'PRIVATE' | 'NONE'>; // (undocumented) - export interface FileParameters { - // (undocumented) - acl?: Acl; - // (undocumented) + export type FileParameters = { contentType: string; - // (undocumented) expiration?: number; - // (undocumented) - extension?: string; - // (undocumented) - fileName?: string; - // (undocumented) - prefix?: string; - // (undocumented) size?: number; - // (undocumented) + prefix?: string; + extension?: string; suffix?: string; - } + fileName?: string; + acl?: Acl; + }; // (undocumented) export interface MutationParameters { // (undocumented) @@ -169,45 +125,34 @@ export const getTenantErrorMessage: (errorCode: string) => string; export namespace GraphQlBuilder { import GraphqlLiteral = GraphQlBuilderTmp.GraphQlLiteral; import GraphQlLiteral = GraphQlBuilderTmp.GraphQlLiteral; - import ObjectBuilder = GraphQlBuilderTmp.ObjectBuilder; - import QueryCompiler = GraphQlBuilderTmp.QueryCompiler; - import QueryBuilder = GraphQlBuilderTmp.QueryBuilder; - import RootObjectBuilder = GraphQlBuilderTmp.RootObjectBuilder; } // @public (undocumented) -export class GraphQlClient { - constructor(apiUrl: string, apiToken?: string | undefined); - // (undocumented) - readonly apiUrl: string; - // (undocumented) - sendRequest(query: string, { apiTokenOverride, signal, variables, headers }?: GraphQlClientRequestOptions): Promise; +export class GraphQlClient extends GraphQlClient_2 { + // @deprecated (undocumented) + sendRequest(query: string, options?: GraphQlClientRequestOptions): Promise; } +export { GraphQlClientError } + // @public (undocumented) export type GraphQlClientFailedRequestMetadata = Pick & { responseText: string; }; // @public (undocumented) -export interface GraphQlClientRequestOptions { - // (undocumented) +export interface GraphQlClientRequestOptions extends GraphQlClientRequestOptions_2 { + // @deprecated (undocumented) apiTokenOverride?: string; - // (undocumented) - headers?: Record; - // (undocumented) - signal?: AbortSignal; - // (undocumented) - variables?: GraphQlClientVariables; } -// @public (undocumented) -export interface GraphQlClientVariables { - // (undocumented) - [name: string]: any; -} +export { GraphQlClientVariables } -// @public (undocumented) +export { GraphQlErrorRequest } + +export { GraphQlErrorType } + +// @public @deprecated (undocumented) export class GraphQlLiteral { constructor(value: Value); // (undocumented) @@ -262,6 +207,14 @@ export interface RelationFilter { relations: RelationFilter[]; } +// @public (undocumented) +export type ReplaceGraphQlLiteral = T extends GraphQlLiteral ? Value : T extends string | number | boolean | null ? T : T extends {} ? { + [K in keyof T]: ReplaceGraphQlLiteral; +} : T extends any[] ? ReplaceGraphQlLiteral[] : T; + +// @public (undocumented) +export const replaceGraphQlLiteral: (input: T) => ReplaceGraphQlLiteral; + export { Result } // @public (undocumented) @@ -357,6 +310,9 @@ export const whereToFilter: (by: Input.UniqueWhere) => Input.Whe export { Writable } + +export * from "@contember/client-content"; + // (No @packageDocumentation comment for this package) ``` diff --git a/build/api/graphql-builder.api.md b/build/api/graphql-builder.api.md new file mode 100644 index 0000000000..2293e8bf47 --- /dev/null +++ b/build/api/graphql-builder.api.md @@ -0,0 +1,75 @@ +## API Report File for "@contember/graphql-builder" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { JSONValue } from '@contember/schema'; + +// @public (undocumented) +export class GraphQlField { + constructor(alias: string | null, name: string, args?: GraphQlFieldTypedArgs, selectionSet?: GraphQlSelectionSet | undefined); + // (undocumented) + readonly alias: string | null; + // (undocumented) + readonly args: GraphQlFieldTypedArgs; + // (undocumented) + readonly name: string; + // (undocumented) + readonly selectionSet?: GraphQlSelectionSet | undefined; +} + +// @public (undocumented) +export type GraphQlFieldTypedArgs = Record; + +// @public (undocumented) +export class GraphQlFragment { + constructor(name: string, type: string, selectionSet: GraphQlSelectionSet); + // (undocumented) + readonly name: string; + // (undocumented) + readonly selectionSet: GraphQlSelectionSet; + // (undocumented) + readonly type: string; +} + +// @public (undocumented) +export class GraphQlFragmentSpread { + constructor(name: string); + // (undocumented) + readonly name: string; +} + +// @public (undocumented) +export class GraphQlInlineFragment { + constructor(type: string, selectionSet: GraphQlSelectionSet); + // (undocumented) + readonly selectionSet: GraphQlSelectionSet; + // (undocumented) + readonly type: string; +} + +// @public (undocumented) +export type GraphQlPrintResult = { + query: string; + variables: Record; +}; + +// @public (undocumented) +export class GraphQlQueryPrinter { + // (undocumented) + printDocument(operation: 'query' | 'mutation', select: GraphQlSelectionSet, fragments: Record): GraphQlPrintResult; +} + +// @public (undocumented) +export type GraphQlSelectionSet = GraphQlSelectionSetItem[]; + +// @public (undocumented) +export type GraphQlSelectionSetItem = GraphQlField | GraphQlFragmentSpread | GraphQlInlineFragment; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/build/api/graphql-client.api.md b/build/api/graphql-client.api.md new file mode 100644 index 0000000000..6cd1d9c1a9 --- /dev/null +++ b/build/api/graphql-client.api.md @@ -0,0 +1,72 @@ +## API Report File for "@contember/graphql-client" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +// @public (undocumented) +export class GraphQlClient { + constructor(apiUrl: string, apiToken?: string | undefined); + // (undocumented) + readonly apiUrl: string; + // (undocumented) + protected doExecute(query: string, { apiToken, signal, variables, headers }?: GraphQlClientRequestOptions): Promise; + // (undocumented) + execute(query: string, options?: GraphQlClientRequestOptions): Promise; +} + +// @public (undocumented) +export class GraphQlClientError extends Error { + constructor(message: string, type: GraphQlErrorType, request: GraphQlErrorRequest, response?: Response | undefined, errors?: readonly any[] | undefined, details?: string | undefined, cause?: unknown); + // (undocumented) + readonly details?: string | undefined; + // (undocumented) + readonly errors?: readonly any[] | undefined; + // (undocumented) + readonly request: GraphQlErrorRequest; + // (undocumented) + readonly response?: Response | undefined; + // (undocumented) + readonly type: GraphQlErrorType; +} + +// @public (undocumented) +export interface GraphQlClientRequestOptions { + // (undocumented) + apiToken?: string; + // (undocumented) + headers?: Record; + // (undocumented) + onBeforeRequest?: (query: { + query: string; + variables: GraphQlClientVariables; + }) => void; + // (undocumented) + onData?: (json: unknown) => void; + // (undocumented) + onResponse?: (response: Response) => void; + // (undocumented) + signal?: AbortSignal; + // (undocumented) + variables?: GraphQlClientVariables; +} + +// @public (undocumented) +export interface GraphQlClientVariables { + // (undocumented) + [name: string]: any; +} + +// @public (undocumented) +export type GraphQlErrorRequest = { + url: string; + query: string; + variables: Record; +}; + +// @public (undocumented) +export type GraphQlErrorType = 'aborted' | 'network error' | 'invalid response body' | 'bad request' | 'unauthorized' | 'forbidden' | 'server error' | 'response errors'; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/build/api/react-binding.api.md b/build/api/react-binding.api.md index 7b198c23f0..a5ec56a757 100644 --- a/build/api/react-binding.api.md +++ b/build/api/react-binding.api.md @@ -21,6 +21,7 @@ import type { FieldName } from '@contember/binding'; import { FieldValue } from '@contember/binding'; import { Filter } from '@contember/binding'; import type { GetEntityByKey } from '@contember/binding'; +import { GraphQlClientError } from '@contember/react-client'; import type { HasManyRelationMarker } from '@contember/binding'; import type { HasOneRelationMarker } from '@contember/binding'; import { MarkerTreeRoot } from '@contember/binding'; @@ -32,7 +33,6 @@ import { ReactNode } from 'react'; import type { RelativeEntityList } from '@contember/binding'; import type { RelativeSingleEntity } from '@contember/binding'; import { RelativeSingleField } from '@contember/binding'; -import type { RequestError } from '@contember/binding'; import type { SugaredParentEntityParameters } from '@contember/binding'; import type { SugaredQualifiedEntityList } from '@contember/binding'; import type { SugaredQualifiedSingleEntity } from '@contember/binding'; @@ -72,7 +72,7 @@ export type AccessorTreeStateAction = { binding: DataBinding; } | { type: 'failWithError'; - error: RequestError; + error: GraphQlClientError; binding: DataBinding; } | { type: 'reset'; @@ -316,7 +316,7 @@ export interface ErrorAccessorTreeState { // (undocumented) environment: Environment; // (undocumented) - error: RequestError; + error: GraphQlClientError; // (undocumented) name: 'error'; }