├── bun.lockb ├── CHANGELOG.md ├── phpstan.neon ├── renovate.json ├── vitest.config.ts ├── craft ├── ecs.php ├── src ├── web │ ├── assets │ │ └── craft │ │ │ └── CraftAsset.php │ ├── interfaces.ts │ └── craft.ts ├── Plugin.php ├── transformers │ └── CallableTransformer.php ├── collectors │ └── CraftCollector.php ├── controllers │ └── CraftController.php ├── helpers │ └── PruneHelper.php └── services │ └── QueryHandler.php ├── package.json ├── bootstrap.php ├── vite.config.js ├── .gitignore ├── LICENSE.md ├── composer.json ├── tests └── general.test.ts ├── bin └── typescript-transform ├── README.md └── types └── craft-generated.d.ts /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chasegiunta/craft-js/HEAD/bun.lockb -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Release Notes for Craft JS 2 | 3 | ## 1.0.0 4 | - Initial release 5 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - vendor/craftcms/phpstan/phpstan.neon 3 | 4 | parameters: 5 | level: 4 6 | paths: 7 | - src 8 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | include: ["tests/**/*.test.ts"], 6 | }, 7 | }); 8 | -------------------------------------------------------------------------------- /craft: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | run(); 14 | exit($exitCode); 15 | -------------------------------------------------------------------------------- /ecs.php: -------------------------------------------------------------------------------- 1 | paths([ 10 | __DIR__ . '/src', 11 | __FILE__, 12 | ]); 13 | 14 | $ecsConfig->sets([ 15 | SetList::CRAFT_CMS_4, 16 | ]); 17 | }; 18 | -------------------------------------------------------------------------------- /src/web/assets/craft/CraftAsset.php: -------------------------------------------------------------------------------- 1 | safeLoad(); 19 | } 20 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { resolve } from "path"; 2 | import { defineConfig } from "vite"; 3 | 4 | export default defineConfig({ 5 | build: { 6 | lib: { 7 | // Could also be a dictionary or array of multiple entry points 8 | entry: resolve(__dirname, "src/web/craft.ts"), 9 | name: "CraftJS", 10 | // the proper extensions will be added 11 | fileName: "craft", 12 | }, 13 | outDir: resolve(__dirname, "dist/"), 14 | // rollupOptions: { 15 | // // make sure to externalize deps that shouldn't be bundled 16 | // // into your library 17 | // external: ['vue'], 18 | // output: { 19 | // // Provide global variables to use in the UMD build 20 | // // for externalized deps 21 | // globals: { 22 | // vue: 'Vue', 23 | // }, 24 | // }, 25 | // }, 26 | }, 27 | }); 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # System 2 | .DS_Store 3 | 4 | # Composer 5 | /vendor 6 | 7 | # NPM 8 | /node_modules 9 | 10 | # Env files 11 | /.env 12 | /.env.local 13 | /.env.*.local 14 | 15 | # Log files 16 | npm-debug.log* 17 | yarn-debug.log* 18 | yarn-error.log* 19 | 20 | # Editor directories and files 21 | .idea 22 | .vscode 23 | *.suo 24 | *.ntvs* 25 | *.njsproj 26 | *.sln 27 | *.sw* 28 | 29 | # Craft 30 | /web/uploads 31 | /web/imager 32 | 33 | # Webpack build 34 | /web/dist 35 | 36 | # Cap 37 | /db 38 | /.cache 39 | 40 | # Logs 41 | logs 42 | *.log 43 | npm-debug.log* 44 | yarn-debug.log* 45 | yarn-error.log* 46 | pnpm-debug.log* 47 | lerna-debug.log* 48 | 49 | node_modules 50 | dist 51 | dist-ssr 52 | *.local 53 | 54 | # Editor directories and files 55 | .vscode/* 56 | !.vscode/extensions.json 57 | .idea 58 | .DS_Store 59 | *.suo 60 | *.ntvs* 61 | *.njsproj 62 | *.sln 63 | *.sw? 64 | /composer.lock 65 | /storage 66 | .aider* 67 | /ray.php 68 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Pixel & Tonic, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Plugin.php: -------------------------------------------------------------------------------- 1 | 14 | * @copyright Chase Giunta 15 | * @license MIT 16 | * @property-read QueryHandler $queryHandler 17 | */ 18 | class Plugin extends BasePlugin 19 | { 20 | public string $schemaVersion = '1.0.0'; 21 | 22 | public static function config(): array 23 | { 24 | return [ 25 | 'components' => ['queryHandler' => QueryHandler::class], 26 | ]; 27 | } 28 | 29 | public function init() 30 | { 31 | parent::init(); 32 | 33 | // Defer most setup tasks until Craft is fully initialized 34 | Craft::$app->onInit(function () { 35 | $this->attachEventHandlers(); 36 | // ... 37 | }); 38 | } 39 | 40 | private function attachEventHandlers(): void 41 | { 42 | // Register event handlers here ... 43 | // (see https://craftcms.com/docs/4.x/extend/events.html to get started) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chasegiunta/craft-js", 3 | "description": "A javascript client for Craft CMS", 4 | "type": "craft-plugin", 5 | "license": "mit", 6 | "support": { 7 | "email": "me@chasegiunta.com", 8 | "issues": "https://github.com/chasegiunta/craft-js/issues?state=open", 9 | "source": "https://github.com/chasegiunta/craft-js", 10 | "docs": "https://github.com/chasegiunta/craft-js", 11 | "rss": "https://github.com/chasegiunta/craft-js/releases.atom" 12 | }, 13 | "require": { 14 | "php": ">=8.1", 15 | "craftcms/cms": "^4.0.0 | ^5.0.0", 16 | "symfony/expression-language": "^6.4" 17 | }, 18 | "require-dev": { 19 | "craftcms/ecs": "dev-main", 20 | "craftcms/generator": "^1.6", 21 | "craftcms/phpstan": "dev-main", 22 | "spatie/enum": "^3.13", 23 | "spatie/ray": "^1.40", 24 | "spatie/typescript-transformer": "^2.2", 25 | "vlucas/phpdotenv": "^5.6" 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "chasegiunta\\craftjs\\": "src/" 30 | } 31 | }, 32 | "extra": { 33 | "handle": "craft-js", 34 | "name": "Craft JS", 35 | "developer": "Chase Giunta", 36 | "documentationUrl": "https://github.com/chasegiunta/craft-js" 37 | }, 38 | "scripts": { 39 | "check-cs": "ecs check --ansi", 40 | "fix-cs": "ecs check --ansi --fix", 41 | "phpstan": "phpstan --memory-limit=1G" 42 | }, 43 | "config": { 44 | "sort-packages": true, 45 | "platform": { 46 | "php": "8.1" 47 | }, 48 | "allow-plugins": { 49 | "yiisoft/yii2-composer": true, 50 | "craftcms/plugin-installer": true 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/transformers/CallableTransformer.php: -------------------------------------------------------------------------------- 1 | checkForTypeOverride($type); 25 | $return = $this->checkForTypeOverride($type) ?? $transpiler->execute($type); 26 | echo $originalType . ' => ' . $return . PHP_EOL; 27 | return $return; 28 | } 29 | 30 | protected function checkForTypeOverride(Type $type) 31 | { 32 | $overideTypeDetection = false; 33 | $type = explode('|', $type); 34 | foreach ($type as $key => $value) { 35 | if ($value == 'callable') { 36 | $overideTypeDetection = true; 37 | unset($type[$key]); 38 | } 39 | // if (strpos($value, 'craft\\') === 1) { 40 | // $overideTypeDetection = true; 41 | // unset($type[$key]); 42 | // } 43 | } 44 | if ($overideTypeDetection == true) { 45 | return implode('|', $type); 46 | } else { 47 | return null; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/web/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface EntryQuery extends ElementQuery { 2 | [x: string]: any; 3 | section(section: string): EntryQuery; 4 | sectionId(value: number | "not" | number[]): EntryQuery; 5 | type(value: string | EntryType): EntryQuery; 6 | typeId(value: number | "not" | number[]): EntryQuery; 7 | authorId(value: number | "not" | number[]): EntryQuery; 8 | authorGroup(value: string | UserGroup | UserGroup[] | null): EntryQuery; 9 | authorGroupId(value: number | "not" | number[]): EntryQuery; 10 | postDate(value: string | string[]): EntryQuery; 11 | before(value: string | Date): EntryQuery; 12 | after(value: string | Date): EntryQuery; 13 | expiryDate(value: string | string[]): EntryQuery; 14 | status(value: string | string[]): EntryQuery; 15 | } 16 | 17 | interface Section { 18 | id: number; 19 | structureId?: number; 20 | type: string; 21 | } 22 | 23 | interface EntryType { 24 | id: number; 25 | } 26 | 27 | interface UserGroup { 28 | id: number; 29 | } 30 | 31 | export interface ElementQuery { 32 | [x: string]: any; 33 | limit(limit: number | null): ElementQuery; 34 | orderBy(order: string): ElementQuery; 35 | paginate(page: number): ElementQuery; 36 | prune(items: string[]): ElementQuery; 37 | 38 | one(): Promise; 39 | all(): Promise; 40 | exists(): Promise; 41 | ids(): Promise; 42 | count(): Promise; 43 | } 44 | 45 | export interface Craft { 46 | [x: string]: any; 47 | batch(queries: Craft[]): Craft; 48 | entries(): EntryQuery; 49 | // assets(): AssetQuery; 50 | // users(): UserQuery; 51 | // Add other initial methods if there are any 52 | } 53 | -------------------------------------------------------------------------------- /src/collectors/CraftCollector.php: -------------------------------------------------------------------------------- 1 | getName(), $this->craftClasses)) { 40 | return null; 41 | } 42 | 43 | $reflector = ClassTypeReflector::create($class); 44 | 45 | $transformedType = $reflector->getType() 46 | ? $this->resolveAlreadyTransformedType($reflector) 47 | : $this->resolveTypeViaTransformer($reflector); 48 | 49 | return $transformedType; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/general.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "vitest"; 2 | import CraftJs, { _Craft } from "./../src/web/assets/craft/src/craft"; 3 | 4 | test("CraftQuery", () => { 5 | const craft = CraftJs("http://next.test"); 6 | expect(craft.apiUrl).toEqual("http://next.test"); 7 | expect(craft.endpoint).toEqual("/actions/craft-js/craft"); 8 | }); 9 | 10 | test("entries", () => { 11 | const craft = CraftJs("http://next.test"); 12 | const entriesQuery = craft.entries(); 13 | expect(entriesQuery.filters.entries).toEqual(true); 14 | }); 15 | 16 | test("section", () => { 17 | const craft = CraftJs("http://next.test"); 18 | const teamsQuery = craft.entries().section("teams"); 19 | expect(teamsQuery.filters.section).toEqual("teams"); 20 | }); 21 | 22 | test("Undefined Methods", () => { 23 | const craft = CraftJs("http://next.test"); 24 | const teamsQuery = craft.entries().section("teams").division("d1"); 25 | expect(teamsQuery.filters.division).toEqual("d1"); 26 | }); 27 | 28 | test("Teams Listing Query", async () => { 29 | const craft = CraftJs("http://next.test"); 30 | let teamsQuery = craft 31 | .entries() 32 | .section("teams") 33 | .division("d1") 34 | .limit(20) 35 | .prune(["title", "url", "address (state)", "conference"]); 36 | expect(teamsQuery.filters.section).toEqual("teams"); 37 | expect(teamsQuery.filters.division).toEqual("d1"); 38 | expect(teamsQuery.filters.limit).toEqual(20); 39 | expect(teamsQuery.filters.prune).toEqual( 40 | JSON.stringify(["title", "url", "address (state)", "conference"]) 41 | ); 42 | 43 | // set division to null 44 | teamsQuery = teamsQuery.division(null); 45 | expect(teamsQuery.filters.division).toEqual(null); 46 | 47 | await teamsQuery.fetch().then((response) => { 48 | expect(response.data).toHaveLength(20); 49 | expect(response.data).toHaveProperty("0.title"); 50 | expect(response.data).not.toHaveProperty("0.id"); 51 | }); 52 | }); 53 | 54 | test("batch", async () => { 55 | const craft = CraftJs("http://next.test"); 56 | 57 | const query1 = craft 58 | .entries() 59 | .section("teams") 60 | .division("d1") 61 | .limit(20) 62 | .prune(["title"]); 63 | const query2 = craft.entries().section("news").limit(5).prune(["title"]); 64 | 65 | await craft 66 | .batch([query1, query2]) 67 | .fetch() 68 | .then((responses: any[]) => { 69 | const teams = responses[0]; 70 | const news = responses[1]; 71 | 72 | expect(teams.data).toHaveLength(20); 73 | expect(news.data).toHaveLength(5); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /src/controllers/CraftController.php: -------------------------------------------------------------------------------- 1 | requireAcceptsJson(); 35 | 36 | $request = Craft::$app->getRequest(); 37 | $response = Craft::$app->getResponse(); 38 | $response->format = Response::FORMAT_JSON; 39 | 40 | // Get params from request 41 | $params = $request->getQueryParams(); 42 | 43 | $queryHandler = new QueryHandler(); 44 | $response->data = $queryHandler->handleSingleQuery($params); 45 | return $response; 46 | } 47 | 48 | /** 49 | * Handles multiple queries by iterating over the queries, calling the `handleSingleQuery` method of the `QueryHandler` class for each query, and returns an array of results as a JSON response. 50 | * 51 | * @return Response 52 | */ 53 | public function actionBatched(): Response 54 | { 55 | $this->requireAcceptsJson(); 56 | 57 | $request = Craft::$app->getRequest(); 58 | $response = Craft::$app->getResponse(); 59 | $response->format = Response::FORMAT_JSON; 60 | 61 | // Since it's POST, we will get params from the request body 62 | $queries = $request->getBodyParams(); 63 | 64 | if (!is_array($queries)) { 65 | throw new BadRequestHttpException('Invalid request body'); 66 | } 67 | 68 | $results = []; 69 | $queryHandler = new QueryHandler(); 70 | foreach ($queries as $singleQueryParams) { 71 | $results[] = $queryHandler->handleSingleQuery($singleQueryParams); 72 | } 73 | 74 | $response->data = $results; 75 | return $response; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /bin/typescript-transform: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | setName('TypeScript Transform') 35 | ->addArgument('output_file', InputArgument::REQUIRED, 'Writes the generated TypeScript to this file') 36 | ->addArgument('paths', InputArgument::REQUIRED | InputArgument::IS_ARRAY, 'Paths with classes to transform') 37 | ->addOption('enums', 'e', InputOption::VALUE_NONE, 'Use native TypeScript enums') 38 | ->addOption('format', 'f', InputOption::VALUE_NONE, 'Format the output') 39 | ->setCode(function ( 40 | InputInterface $input, 41 | OutputInterface $output 42 | ) use ($transformers): int { 43 | $io = new SymfonyStyle($input, $output); 44 | 45 | $config = TypeScriptTransformerConfig::create() 46 | ->collectors([CraftCollector::class]) 47 | ->autoDiscoverTypes(...$input->getArgument('paths')) 48 | ->transformers($transformers) 49 | ->transformToNativeEnums($input->getOption('enums')) 50 | ->formatter($input->getOption('format') ? PrettierFormatter::class : null) 51 | ->outputFile($input->getArgument('output_file')) 52 | ->writer(ModuleWriter::class); 53 | 54 | $transformer = new TypeScriptTransformer($config); 55 | 56 | try { 57 | $results = $transformer->transform(); 58 | } catch (Exception $exception) { 59 | $io->error($exception->getMessage()); 60 | // $io->info("\nStack trace:\n" . $exception->getTraceAsString()); 61 | $trace = array_map(function ($frame) { 62 | $args = is_array($frame['args']) ? json_encode($frame['args']) : ''; 63 | return [ 64 | 'file' => $frame['file'] ?? '', 65 | 'line' => $frame['line'] ?? '', 66 | 'function' => $frame['function'] ?? '', 67 | 'args' => strlen($args) > 60 ? '...' . substr($args, -60) : $args, 68 | ]; 69 | }, $exception->getTrace()); 70 | 71 | $io->table(['File', 'Line', 'Function', 'Args'], $trace); 72 | 73 | return 1; 74 | } 75 | 76 | $io->table( 77 | ['PHP class', 'TypeScript entity'], 78 | array_map( 79 | fn (string $class, TransformedType $type) => [$class, $type->getTypeScriptName()], 80 | array_keys((array) $results->getIterator()), 81 | array_values((array) $results->getIterator()) 82 | ) 83 | ); 84 | 85 | $io->info("Transformed {$results->count()} PHP types to TypeScript"); 86 | 87 | return 0; 88 | }) 89 | ->run(); 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Craft JS 2 | 3 | 🧪 Experimental concept for a Craft CMS javascript / typescript client that: 4 | 5 | - Attempts to mimic Twig's familiar chainable API 6 | - Aims to offer autocomplete through Typescript 7 | - Makes headless projects more approachable for those who either don't know GraphQL, or don't care for it 8 | 9 | > [!NOTE] 10 | > If you find this concept interesting, please improve on it where you can and submit a PR. This is very much a learning project for me. I'm not a PHP developer by profession, nor do I work with Craft CMS in my day job, though I use it on a volunteer project. Any suggestions for code cleanup, improvements, future ideas, etc are welcome! 11 | 12 | > [!CAUTION] 13 | > Not suitable for production (yet) 14 | 15 | #### Basic Usage 16 | 17 | ```js 18 | import Craft from "{vendorPath}/chasegiunta/craft-js/dist/craft"; 19 | 20 | const craft = Craft("https://your-craft-website.test"); 21 | 22 | // Currently, only read queries are supported 23 | const fetchPosts = async () => { 24 | const posts = craft 25 | .entries() 26 | .section("blog") 27 | .orderBy("title ASC") 28 | .limit(10) 29 | .all(); 30 | await craft.then((response: any) => { 31 | console.log(response.data); 32 | }); 33 | }; 34 | ``` 35 | 36 | ##### Element Pagination 37 | 38 | ```js 39 | const posts = craft 40 | .entries() 41 | .section("blog") 42 | .label("myCustomFieldLabel") // supports custom fields 43 | .paginate(pageNum) // paginate() will override any execution methods (.all(), .one(), etc.) 44 | .limit(10) 45 | .fetch(); // method used to fetch response (this will be changed in the future). 46 | ``` 47 | 48 | ##### Batch Queries 49 | 50 | ```js 51 | const query1 = craft 52 | .entries() 53 | .section("teams") 54 | .division("d1") 55 | .limit(10) 56 | .prune(["title"]); 57 | const query2 = craft 58 | .entries() 59 | .section("news") 60 | .limit(20) 61 | .prune(["title", "url"]); 62 | 63 | await craft 64 | .batch([query1, query2]) 65 | .fetch() 66 | .then((responses: any[]) => { 67 | const teams = responses[0]; 68 | const news = responses[1]; 69 | // ... handle data ... 70 | 71 | console.log(teams.data, news.data); 72 | }); 73 | ``` 74 | 75 | #### Prune Responses 76 | 77 | ```js 78 | const posts = craft 79 | .entries() 80 | .section("blog") 81 | // Basic usage: simply pass an array of fields 82 | .prune(["title", "author", "body", "url", "featuredImage"]) 83 | .all(); 84 | ``` 85 | 86 | ```js 87 | const posts = craft 88 | .entries() 89 | .section("blog") 90 | // Advanced object syntax 91 | .prune({ 92 | title: true, 93 | id: true, 94 | uri: true, 95 | // Related fields simple array syntax 96 | author: ["username", "email"], 97 | // Related fields object syntax 98 | mainImage: { 99 | url: true, 100 | uploader: { 101 | // Nested related fields 102 | email: true, 103 | username: true, 104 | }, 105 | }, 106 | // Matrix fields 107 | contentBlocks: { 108 | // Denote query traits with $ prefix 109 | // https://www.yiiframework.com/doc/api/2.0/yii-db-querytrait 110 | $limit: 10, 111 | // Designate distinct prune fields per type with _ prefix 112 | _body: { 113 | body: true, 114 | intro: true, 115 | }, 116 | _fullWidthImage: { 117 | image: ["url", "alt"], 118 | }, 119 | }, 120 | }) 121 | .all(); 122 | ``` 123 | 124 | --- 125 | 126 | ## Future Idea & Todos: 127 | 128 |
129 | Craft Element CRUD Functionality 130 | 131 | - [ ] Create Elements 132 | - [ ] Entries 133 | - [ ] Users 134 | - [ ] Assets 135 | - [ ] Categories 136 | - [ ] Tags 137 | - [ ] Globals 138 | - [ ] Matrix Blocks 139 | - [ ] Addresses 140 | - [ ] Read Elements 141 | - [x] Entries 142 | - [x] Users 143 | - [x] Assets 144 | - [x] Categories (untested) 145 | - [x] Tags (untested) 146 | - [x] Globals 147 | - [x] Matrix Blocks 148 | - [x] Addresses 149 | - [ ] Update Elements 150 | - [ ] Entries 151 | - [ ] Users 152 | - [ ] Assets 153 | - [ ] Categories 154 | - [ ] Tags 155 | - [ ] Globals 156 | - [ ] Matrix Blocks 157 | - [ ] Addresses 158 | - [ ] Delete Elements 159 | - [ ] Entries 160 | - [ ] Users 161 | - [ ] Assets 162 | - [ ] Categories 163 | - [ ] Tags 164 | - [ ] Globals 165 | - [ ] Matrix Blocks 166 | - [ ] Addresses 167 | 168 |
169 | 170 | - [x] Flesh out `prune()` API 171 | - [x] Nested related elements 172 | - [x] Conditional prune by type (`{% if block.type === 'heading' %}`) 173 | - [ ] Call functions on fields before response 174 | - [ ] Image transforms 175 | - [ ] Generate type interfaces 176 | - [ ] Craft element classes 177 | - [ ] Extended Craft behavior classes (eg `craft.superTable`) 178 | - [ ] Implement user / guest permissions system 179 | - [ ] User authentication 180 | - [ ] Commerce CRUD elements 181 | - [ ] Routes fetching 182 | - [ ] Proper Tests 183 | - [ ] Frontend 184 | - [ ] Backend 185 | 186 | #### Vue app usage demo (outdated prune API) 187 | 188 | https://github.com/chasegiunta/craft-js/assets/1377169/e2e8d096-94d0-45e2-8a80-6c6a26263440 189 | 190 | ## Requirements 191 | 192 | This plugin requires Craft CMS 5.0.0 or later, and PHP 8.0.2 or later. 193 | 194 | ## Installation 195 | 196 | You can install this plugin from the Plugin Store or with Composer. 197 | 198 | #### From the Plugin Store 199 | 200 | Go to the Plugin Store in your project’s Control Panel and search for “Craft JS”. Then press “Install”. 201 | 202 | #### With Composer 203 | 204 | ##### Add the following to your composer.json file: 205 | 206 | ```json 207 | { 208 | "repositories": [ 209 | { 210 | "type": "vcs", 211 | "url": "https://github.com/chasegiunta/craft-js.git" 212 | } 213 | ], 214 | "require": { 215 | "chasegiunta/craft-js": "dev-main" 216 | } 217 | } 218 | ``` 219 | 220 | ```bash 221 | # install 222 | composer install 223 | 224 | # tell Craft to install the plugin 225 | ./craft plugin/install craft-js 226 | ``` 227 | -------------------------------------------------------------------------------- /src/web/craft.ts: -------------------------------------------------------------------------------- 1 | import type { EntryQuery, Craft as _Craft } from "./interfaces"; 2 | /** 3 | * Factory function to start the query chain. 4 | * @param apiUrl - The base URL of the API. 5 | * @returns A proxy object that wraps an instance of the `Craft` class. 6 | */ 7 | function init(apiUrl: string): _Craft { 8 | return craftProxy(apiUrl); 9 | } 10 | /** 11 | * Creates a proxy object for the Craft class. 12 | * The proxy object allows dynamic method invocation and property access, 13 | * and it automatically creates new instances of the Craft class with updated filters based on the method or property accessed. 14 | * @param apiUrl The base URL of the API. 15 | * @returns A proxy object that allows dynamic method invocation and property access on the Craft class. 16 | */ 17 | function craftProxy(apiUrl: string) { 18 | const baseQuery = new Craft(apiUrl) as _Craft & Craft; 19 | return new Proxy(baseQuery, { 20 | get(target, prop) { 21 | if (typeof prop === "string" && prop in target) { 22 | return target[prop]; 23 | } else { 24 | return function (...args) { 25 | const newQuery = target.clone(); 26 | newQuery.filters[prop.toString()] = args.length > 0 ? args[0] : true; 27 | return newQuery; 28 | }; 29 | } 30 | }, 31 | }); 32 | } 33 | 34 | /** 35 | * The `Craft` class is a query builder that allows users to construct and execute API queries. 36 | * It provides methods for filtering and fetching data from the API. 37 | */ 38 | class Craft { 39 | /** 40 | * The base URL of the API. 41 | */ 42 | protected apiUrl: string; 43 | 44 | /** 45 | * The specific endpoint that the query will hit. 46 | */ 47 | protected endpoint: string = `/actions/craft-js/craft`; 48 | 49 | /** 50 | * The specific endpoint that the batch request will hit. 51 | */ 52 | protected batchEndpoint: string = `/actions/craft-js/craft/batched`; 53 | 54 | /** 55 | * An array of `Craft` queries to be executed as a batch. 56 | */ 57 | private queries: Craft[] = []; 58 | 59 | // Filters that will be applied to this query. 60 | public filters: Record = {}; 61 | 62 | /** 63 | * Indicates whether the query is part of a batch. 64 | */ 65 | protected isBatchMode = false; 66 | 67 | /** 68 | * Initializes the `Craft` instance with the API URL. 69 | * @param apiUrl The base URL of the API. 70 | */ 71 | constructor(apiUrl: string) { 72 | this.apiUrl = apiUrl; 73 | } 74 | 75 | /** 76 | * Index signature to allow additional methods. 77 | */ 78 | [method: string]: any; 79 | 80 | /** 81 | * Sets the `prune` filter to remove specific fields from the query. 82 | * @param items The fields to be pruned. 83 | * @returns The `Craft` instance. 84 | */ 85 | prune(items: string[]): Craft { 86 | this.filters.prune = JSON.stringify(items); 87 | return this; 88 | } 89 | 90 | /** 91 | * Creates a new instance of the `Craft` class with the same filters as the original query. 92 | * @returns The cloned `Craft` instance. 93 | */ 94 | clone(): Craft { 95 | const newQuery = craftProxy(this.apiUrl); 96 | newQuery.filters = { ...this.filters }; 97 | return newQuery; 98 | } 99 | 100 | one() { 101 | this.filters.executeMethod = "one"; 102 | return this.fetch(); 103 | } 104 | collect() { 105 | this.filters.executeMethod = "collect"; 106 | return this.fetch(); 107 | } 108 | exists() { 109 | this.filters.executeMethod = "exists"; 110 | return this.fetch(); 111 | } 112 | count() { 113 | this.filters.executeMethod = "count"; 114 | return this.fetch(); 115 | } 116 | ids() { 117 | this.filters.executeMethod = "ids"; 118 | return this.fetch(); 119 | } 120 | column() { 121 | this.filters.executeMethod = "column"; 122 | return this.fetch(); 123 | } 124 | all() { 125 | this.filters.executeMethod = "all"; 126 | return this.fetch(); 127 | } 128 | 129 | /** 130 | * Creates a new instance of the `CraftBatch` class to execute multiple queries as a batch. 131 | * @param queries The queries to be executed as a batch. 132 | * @returns The `CraftBatch` instance. 133 | */ 134 | batch(queries: Craft[]): Craft { 135 | this.isBatchMode = true; 136 | queries.forEach((query) => this.addQuery(query)); 137 | return this; 138 | } 139 | 140 | /** 141 | * Adds a `Craft` query to the batch. 142 | * @param query The `Craft` query to add. 143 | * @returns The `CraftBatch` instance. 144 | */ 145 | addQuery(query: Craft): Craft { 146 | this.queries.push(query); 147 | return this; 148 | } 149 | 150 | /** 151 | * Fetches data from the API. 152 | * @returns A promise that resolves to the fetched data or array of fetched data. 153 | */ 154 | async fetch(): Promise { 155 | let url: URL | null = null; 156 | const headers = new Headers(); 157 | headers.append("Accept", "application/json"); 158 | 159 | const options: RequestInit = { 160 | method: this.isBatchMode ? "POST" : "GET", 161 | headers: headers, 162 | }; 163 | 164 | if (this.isBatchMode) { 165 | // Batch mode 166 | headers.append("Content-Type", "application/json"); 167 | 168 | // Aggregate filters from each query into an array 169 | const batchedFilters = this.queries.map((query) => query.filters); 170 | options.body = JSON.stringify(batchedFilters); 171 | url = new URL(`${this.apiUrl}${this.batchEndpoint}`); 172 | } else { 173 | // Single query mode 174 | const filters = {}; 175 | for (const [key, value] of Object.entries(this.filters)) { 176 | filters[key] = value != null ? value.toString() : null; 177 | } 178 | 179 | const queryString = new URLSearchParams(filters).toString(); 180 | url = new URL(`${this.apiUrl}${this.endpoint}?${queryString}`); 181 | } 182 | 183 | try { 184 | const response = await fetch(url, options); 185 | if (!response.ok) { 186 | throw new Error(`Network response was not ok: ${response.statusText}`); 187 | } 188 | const data = await response.json(); 189 | 190 | if (!this.isBatchMode) { 191 | this.filters = {}; 192 | } 193 | 194 | return data; 195 | } catch (error) { 196 | console.error(`Error fetching data: ${error}`); 197 | throw error; 198 | } 199 | } 200 | } 201 | 202 | export default init; 203 | // export type { EntryQuery, _Craft }; 204 | -------------------------------------------------------------------------------- /src/helpers/PruneHelper.php: -------------------------------------------------------------------------------- 1 | 0) { 14 | $data = [$data]; 15 | } 16 | 17 | $pruneDefinition = $this->normalizePruneDefinition($pruneDefinition); 18 | $prunedData = []; 19 | 20 | foreach ($data as $index => $object) { 21 | // Step into each element (or object) and prune it according to the $pruneDefinition 22 | $prunedData[$index] = $this->pruneObject($object, $pruneDefinition); 23 | } 24 | 25 | return $prunedData; 26 | } 27 | 28 | private function normalizePruneDefinition($pruneDefinition) { 29 | // If $pruneDefinition is a string, convert it to an array 30 | if (is_string($pruneDefinition)) { 31 | $pruneDefinition = json_decode($pruneDefinition, true); 32 | } 33 | 34 | // If $pruneDefinition is a non-associative array, 35 | // Convert it to an associative array with { item: true } 36 | if (is_array($pruneDefinition) && !$this->isAssociativeArray($pruneDefinition)) { 37 | $pruneDefinition = array_fill_keys($pruneDefinition, true); 38 | } else if (!is_array($pruneDefinition) || !count($pruneDefinition) > 0) { 39 | $pruneDefinition = [$pruneDefinition]; 40 | } 41 | 42 | // Loop over each item in $pruneDefinition and recursively normalize each item 43 | foreach ($pruneDefinition as $key => $value) { 44 | if (is_bool($value) || is_int($value) || is_string($value) || is_null($value) || is_float($value)) { 45 | continue; 46 | } 47 | if (is_array($value) || is_object($value)) { 48 | $pruneDefinition[$key] = $this->normalizePruneDefinition($value); 49 | } else { 50 | throw new \Exception('Prune definition values must be an array, object, integer, string, or null.'); 51 | } 52 | } 53 | 54 | return $pruneDefinition; 55 | } 56 | 57 | public function pruneObject($object, $pruneDefinition) { 58 | if (!is_object($object)) { 59 | return ['error' => '$object is not an object']; 60 | } 61 | 62 | // Extract specials from pruneDefinition 63 | list($pruneDefinition, $specials) = $this->extractSpecials($pruneDefinition); 64 | 65 | // For ElementQuery, handle all elements returned by the query 66 | if ($object instanceof ElementQuery) { 67 | return $this->processElementQuery($object, $pruneDefinition, $specials); 68 | } 69 | 70 | // For other objects, handle them directly 71 | return $this->processPruneDefinition($object, $pruneDefinition); 72 | } 73 | 74 | private function extractSpecials($pruneDefinition) { 75 | // If $pruneDefinition is not an array, return it as-is 76 | if (!is_array($pruneDefinition)) return [$pruneDefinition, []]; 77 | 78 | $specials = []; 79 | foreach ($pruneDefinition as $key => $value) { 80 | if (strpos($key, '$') === 0) { // Special keys start with '$' 81 | $specials[substr($key, 1)] = $value; 82 | unset($pruneDefinition[$key]); 83 | } 84 | } 85 | return [$pruneDefinition, $specials]; 86 | } 87 | 88 | private function processElementQuery($elementQuery, $pruneDefinition, $specials = []) { 89 | $result = []; 90 | foreach ($elementQuery->all() as $element) { 91 | $result[] = $this->processPruneDefinition($element, $pruneDefinition); 92 | } 93 | return $result; 94 | } 95 | 96 | private function processPruneDefinition($object, $pruneDefinition) { 97 | $result = []; 98 | 99 | foreach ($pruneDefinition as $field => $details) { 100 | // Extract specials from pruneDefinition 101 | list($details, $specials) = $this->extractSpecials($details); 102 | $result[$field] = $this->getProperty($object, $field, $details, $specials); 103 | } 104 | return $result; 105 | } 106 | 107 | private function getProperty($object, $definitionHandle, $definitionValue, $specials = []) { 108 | if ($definitionValue == false) return; 109 | if (!is_object($object)) return ['error' => 'Not an object']; 110 | 111 | $fieldValue = $this->getFieldValue($object, $definitionHandle, $specials); 112 | 113 | if (is_scalar($fieldValue) || is_null($fieldValue)) { 114 | return $fieldValue; 115 | } 116 | 117 | if (is_array($fieldValue)) { 118 | // If all the array items of $fieldValue are instance of Element, prune the object 119 | $isArrayOfElements = array_reduce($fieldValue, function($carry, $item) { 120 | return $carry && $item instanceof Element; 121 | }, true); 122 | 123 | if ($isArrayOfElements) { 124 | foreach ($fieldValue as $key => $element) { 125 | if ($this->isAssociativeArray($definitionValue) && $this->allArrayKeysAreUnderscored($definitionValue)) { 126 | // Assume associative array is keyed by entry (matrix block) types 127 | foreach ($definitionValue as $underscoredElementType => $typePruneDefinition) { 128 | if ($element->type->handle === ltrim($underscoredElementType, '_')) { 129 | $fieldValue[$key] = $this->pruneObject($element, $definitionValue[$underscoredElementType]); 130 | } 131 | } 132 | } else { 133 | $fieldValue[$key] = $this->pruneObject($element, $definitionValue); 134 | } 135 | } 136 | return $fieldValue; 137 | } 138 | 139 | return $fieldValue; 140 | } 141 | 142 | if ($fieldValue instanceof Element) { 143 | return $this->pruneObject($fieldValue, $definitionValue); 144 | } 145 | 146 | if ($fieldValue instanceof ElementQuery) { 147 | $relatedElementObjectPruneDefinition = array(); 148 | 149 | $definitionValueType = gettype($definitionValue); 150 | if (in_array($definitionValueType, ['array'])) { 151 | foreach ($definitionValue as $key => $nestedPropertyKey) { 152 | $relatedElementObjectPruneDefinition[$nestedPropertyKey] = true; 153 | } 154 | } else { 155 | $relatedElementObjectPruneDefinition[$definitionValue] = true; 156 | } 157 | 158 | return $this->pruneObject($fieldValue, $relatedElementObjectPruneDefinition); 159 | } 160 | 161 | if (is_object($fieldValue)) { 162 | if ($fieldValue instanceof HtmlFieldData) { 163 | return $fieldValue; 164 | } 165 | return $this->pruneObject($fieldValue, $definitionValue); 166 | } 167 | 168 | return $fieldValue; 169 | } 170 | 171 | function getFieldValue($object, $definitionHandle, $specials = []) { 172 | 173 | $fieldValue = null; 174 | 175 | if (is_object($object) && isset($object->$definitionHandle)) { 176 | $fieldValue = $object->$definitionHandle; 177 | } else { 178 | $fieldValue = $object[$definitionHandle]; 179 | } 180 | 181 | if (($fieldValue instanceof Element) && $object->canGetProperty($definitionHandle)) { 182 | $fieldValue = $object->$definitionHandle; 183 | } else if ($fieldValue instanceof ElementQuery) { 184 | $methodCall = $object->$definitionHandle; 185 | $methodCall = $this->applySpecials($methodCall, $specials); 186 | $fieldValue = $methodCall->all(); 187 | } else if (isset($object, $definitionHandle)) { 188 | $fieldValue = $object->$definitionHandle; 189 | } 190 | return $fieldValue; 191 | } 192 | 193 | function isAssociativeArray($arr) { 194 | if (is_array($arr) === false) return false; 195 | if ([] == $arr) return true; 196 | if (array_keys($arr) !== range(0, count($arr) - 1)) return true; 197 | foreach ($arr as $value) { 198 | if (is_array($value) && $this->isAssociativeArray($value)) return true; 199 | } 200 | return false; 201 | } 202 | 203 | private function allArrayKeysAreUnderscored($arr) { 204 | $keys = array_keys($arr); 205 | foreach ($keys as $key) { 206 | if (strpos($key, '_')!== 0) { 207 | return false; 208 | } 209 | } 210 | return true; 211 | } 212 | 213 | private function applySpecials($methodCall, $specials) { 214 | foreach ($specials as $specialHandle => $specialValue) { 215 | $methodCall = $methodCall->$specialHandle($specialValue); 216 | } 217 | return $methodCall; 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/services/QueryHandler.php: -------------------------------------------------------------------------------- 1 | params = $params; 64 | $this->cache = $this->params['cache'] ?? false; 65 | 66 | $cachedResponse = $this->checkForCache(); 67 | 68 | if ($cachedResponse !== null) { 69 | return $cachedResponse; 70 | } 71 | 72 | if (isset($this->params['paginate'])) { 73 | $this->paginate = $this->params['paginate']; 74 | } 75 | 76 | if (isset($this->params['limit'])) { 77 | // If limit is set to 'null', set it to -1 78 | $this->limit = strtolower($this->params['limit']) == 'null' ? -1 : $this->params['limit']; 79 | } 80 | 81 | $this->craftElementClass = $this->checkForCraftElementClass(); 82 | $this->customClass = $this->checkForCustomClass(); 83 | 84 | if (!$this->craftElementClass && !$this->customClass) { 85 | return [ 86 | 'success' => false, 87 | 'message' => 'Missing elementType or custom class', 88 | 'request' => $this->params, 89 | ]; 90 | } 91 | 92 | /** @var ElementQueryInterface|null $queryBuilder */ 93 | $queryBuilder = null; 94 | 95 | if ($this->craftElementClass !== null) { 96 | $queryBuilder = $this->craftElementClass::find(); 97 | } else if ($this->customClass !== null) { 98 | $queryBuilder = new $this->customClass; 99 | } 100 | 101 | $queryBuilder = $this->processQueryBuilderParams($queryBuilder); 102 | 103 | if (isset($this->params['select'])) { 104 | $select = explode(',', $this->params['select']); 105 | $select[] = 'sectionId'; 106 | $this->params['select'] = implode(',', $select); 107 | 108 | $queryBuilder->select($this->params['select']); 109 | } 110 | 111 | if (isset($this->params['with'])) { 112 | $with = explode(',', $this->params['with']); 113 | $queryBuilder->with($with); 114 | } 115 | 116 | if ($this->craftElementClass) { 117 | // Apply limit (Defaults to 100, same as Craft) 118 | $queryBuilder->limit($this->limit); 119 | if ($this->paginate) { 120 | [$data, $paginationInfo] = $this->handleElementQueryPagination($queryBuilder, $this->paginate); 121 | } else { 122 | $executeMethod = $params['executeMethod'] ?? 'all'; 123 | $data = $this->executeQuery($queryBuilder, $executeMethod); 124 | } 125 | 126 | $this->handleCache($data); 127 | } else { 128 | $data = $queryBuilder; 129 | } 130 | 131 | // Prune $data to only return the columns we selected 132 | if (isset($params['prune'])) { 133 | $pruneHelper = new PruneHelper; 134 | $data = $pruneHelper->pruneData($data, $params['prune']); 135 | } else { 136 | if (is_array($data) && count($data) > 0) { 137 | // If $data is an array of objects, convert each object to an array 138 | foreach ($data as $key => $element) { 139 | $data[$key] = $element->toArray(); 140 | } 141 | } 142 | } 143 | 144 | $responseData = [ 145 | 'success' => true, 146 | 'message' => 'CraftController actionIndex()', 147 | 'request' => $this->params, 148 | 'data' => $data, 149 | 'cached' => false, 150 | ]; 151 | 152 | if ($this->paginate) { 153 | $responseData['pagination'] = $paginationInfo; 154 | } 155 | 156 | return $responseData; 157 | } 158 | 159 | /** 160 | * Check if cache parameters are set in $params and retrieve data from cache if available. 161 | * 162 | * @return array|null The cache key and cached data if available, otherwise null. 163 | */ 164 | private function checkForCache() 165 | { 166 | if (!$this->cache) { 167 | return null; 168 | } 169 | 170 | $this->cacheKey = 'query_' . md5(json_encode($this->params)); 171 | $data = Craft::$app->getCache()->get($this->cacheKey); 172 | 173 | if ($data) { 174 | return [ 175 | 'success' => true, 176 | 'message' => 'CraftController handleSingleQuery()', 177 | 'request' => $this->params, 178 | 'data' => $data, 179 | 'cached' => true, 180 | ]; 181 | } 182 | // If data is not found in cache, return null 183 | return null; 184 | } 185 | 186 | /** 187 | * Checks for a Craft element class in the given parameters. 188 | * It iterates over a class map array and checks if any of the keys in the parameters match the keys in the class map. 189 | * If a match is found, it sets the craftElementClass variable to the corresponding class and removes the key from the parameters array. 190 | * Finally, it returns an array with the craftElementClass and the updated parameters array. 191 | */ 192 | private function checkForCraftElementClass() 193 | { 194 | $classMap = [ 195 | 'entries' => Entry::class, 196 | 'users' => User::class, 197 | 'assets' => Asset::class, 198 | 'categories' => Category::class, 199 | 'tags' => Tag::class, 200 | 'globals' => GlobalSet::class, 201 | 'matrixBlocks' => MatrixBlock::class, 202 | 'addresses' => Address::class, 203 | ]; 204 | 205 | foreach ($classMap as $param => $class) { 206 | if (isset($this->params[$param])) { 207 | $craftElementClass = $class; 208 | unset($this->params[$param]); 209 | return $craftElementClass; 210 | } 211 | } 212 | 213 | return null; 214 | } 215 | 216 | /** 217 | * Checks if any of the parameters match any of the components in the CraftVariable class. 218 | * If a match is found, it returns the corresponding custom class and removes the matched parameter from the params array. 219 | */ 220 | private function checkForCustomClass() 221 | { 222 | if ($this->craftElementClass) { 223 | return null; 224 | } 225 | 226 | $craftVariable = new CraftVariable(); 227 | $components = $craftVariable->components; 228 | 229 | // If any of the params match any of the components, return that component 230 | foreach (array_keys($components) as $component) { 231 | if (in_array($component, array_keys($this->params))) { 232 | $customClass = $components[$component]; 233 | unset($this->params[$component]); 234 | return $customClass; 235 | } 236 | } 237 | 238 | return null; 239 | } 240 | 241 | private function processQueryBuilderParams($queryBuilder) 242 | { 243 | foreach ($this->params as $param => $value) { 244 | if (in_array($param, ['select', 'with', 'prune', 'asArray', 'paginate', 'one', 'executeMethod'])) { 245 | continue; 246 | } 247 | 248 | /** 249 | * Casts the value to a numeric type by adding 0. 250 | * This ensures the value passed is numeric. 251 | */ 252 | if (is_numeric($value)) { 253 | $value = $value + 0; 254 | } 255 | 256 | if ($value) { 257 | /** 258 | * Parses a comma-separated string into an array of values. 259 | * Trims whitespace and converts special values like 'null', 'true', 'false' 260 | * to their actual types. 261 | * If a single non-array value is provided, puts it into an array. 262 | */ 263 | if (is_string($value)) { 264 | $values = explode(',', $value); 265 | $values = array_map(function ($item) { 266 | $trimmedItem = trim($item, " '\""); 267 | if (strtolower($trimmedItem) === 'null') { 268 | return null; 269 | } elseif (is_numeric($trimmedItem)) { 270 | return $trimmedItem + 0; 271 | } elseif (strtolower($trimmedItem) === 'true') { 272 | return true; 273 | } elseif (strtolower($trimmedItem) === 'false') { 274 | return false; 275 | } else { 276 | return $trimmedItem; 277 | } 278 | }, $values); 279 | } else { 280 | $values = [$value]; 281 | } 282 | 283 | /** 284 | * Checks if the given query builder parameter is a callable method, 285 | * calls it with the given values, and reassigns the result back to 286 | * the query builder. If not callable, calls the parameter as a 287 | * method with the given values. 288 | */ 289 | if (is_callable([$queryBuilder, $param])) { 290 | if (is_object($queryBuilder)) { 291 | $queryBuilder = $queryBuilder->$param(...$values); 292 | } else { 293 | throw new \Exception("Expected \$queryBuilder to be an object."); 294 | } 295 | } else { 296 | $queryBuilder = $queryBuilder->$param(...$values); 297 | } 298 | } else { 299 | /** 300 | * Tries to call the $param() method on the $queryBuilder. 301 | * If it throws an exception, calls $param() again with null as the argument. 302 | */ 303 | try { 304 | $queryBuilder = $queryBuilder->$param(); 305 | } catch (\Throwable $th) { 306 | $queryBuilder = $queryBuilder->$param(null); 307 | } 308 | } 309 | } 310 | return $queryBuilder; 311 | } 312 | 313 | private function handleElementQueryPagination($queryBuilder, $paginate) 314 | { 315 | // Set limit(null) to return all results and then paginate them 316 | $paginator = new Paginator((clone $queryBuilder)->limit(null), [ 317 | 'currentPage' => $paginate, 318 | 'pageSize' => $this->limit, 319 | ]); 320 | 321 | $paginationInfo = Paginate::create($paginator); 322 | $data = $paginator->getPageResults(); 323 | 324 | return [$data, $paginationInfo]; 325 | } 326 | 327 | private function handleCache($data) 328 | { 329 | if ($this->cacheKey) { 330 | // Create a dependency on the 'entry_updated' tag 331 | // In order for this to work, we need to invalidate the cache tag on entry save (TagDependency::invalidate(Craft::$app->getCache(), 'entry_updated')) 332 | $dependency = new TagDependency(['tags' => 'entry_updated']); 333 | // Set the result to cache for 1 hour (3600 seconds) 334 | Craft::$app->getCache()->set($this->cacheKey, $data, 3600, $dependency); 335 | } 336 | } 337 | 338 | // private function pruneData($data) 339 | // { 340 | // $prune = json_decode($this->params['prune']); 341 | 342 | // // Loop through prune array and note items that contain parentheses 343 | // // These items will need to be handled differently 344 | // $parenthesesItems = []; 345 | // foreach ($prune as $key => $value) { 346 | // if (strpos($value, '(') !== false) { 347 | // $parenthesesItems[] = $value; 348 | // } 349 | // } 350 | // // Remove parentheses items from prune array 351 | // $prune = array_diff($prune, $parenthesesItems); 352 | 353 | // $nestedProperties = []; 354 | 355 | // // Given that each item in $parentheses is a string that has an $element property followed by a method call 356 | // // We need to loop through each item and get the nested property on the $element property 357 | // foreach ($parenthesesItems as $item) { 358 | // $item = explode('(', $item); 359 | // $elementProperty = trim($item[0]); 360 | // $methods = trim($item[1], ')'); 361 | // $methods = explode('.', $methods); 362 | // $methods = explode(',', $methods[0]); 363 | 364 | // //remove whitespace in $methods 365 | // $methods = array_map('trim', $methods); 366 | 367 | // foreach ($data as $key => $el) { 368 | 369 | // // TODO: Determine if we're accessing SuperTable, Matrix, or some other element 370 | // // Assume we're accessing SuperTable 371 | // $elements = $el[$elementProperty]->all(); 372 | 373 | // foreach ($elements as $element) { 374 | // if (!is_object($element)) { 375 | // continue; 376 | // } 377 | // $craftNamespace = 'craft\\elements\\'; // Specify the namespace you want to check 378 | 379 | // $className = get_class($element); 380 | // if (strpos($className, $craftNamespace) === 0) { 381 | // // This is a Craft element 382 | // foreach ($methods as $method) { 383 | // $value = $element->$method; 384 | // // add to $nestedProperties 385 | // $nestedProperties[$key][$elementProperty][$method] = $value; 386 | // } 387 | // } else if ($element instanceof \verbb\supertable\elements\SuperTableBlockElement) { 388 | // // If $element is an instance of SuperTableBlockElement 389 | // // Loop through each field in the SuperTable field layout 390 | // $values = []; 391 | // foreach ($element->getFieldLayout()->getCustomFields() as $field) { 392 | // // if $field->handle is in $methods, get the value 393 | // if (in_array($field->handle, $methods)) { 394 | // $value = $element->getFieldValue($field->handle); 395 | // // add to $nestedProperties 396 | // $nestedProperties[$key][$elementProperty][$field->handle] = $value; 397 | 398 | // // $data[$key][$entryProperty][$field->handle] = $value; 399 | // } 400 | 401 | // // // $data[$key][$entryProperty][$method] = $value; 402 | // } 403 | // } 404 | // } 405 | // } 406 | // } 407 | 408 | // if (is_array($data) && count($data) > 0) { 409 | // // .all() returns an array of objects 410 | // // Loop through data and prune each entry 411 | // foreach ($data as $key => $entry) { 412 | // $entry = $entry->toArray(); 413 | // $data[$key] = array_intersect_key($entry, array_flip($prune)); 414 | // } 415 | // } else { 416 | // // .one() returns a single object 417 | // $data = array_intersect_key($data->toArray(), array_flip($prune)); 418 | // } 419 | 420 | // // Merge $nestedProperties to $data 421 | // foreach ($nestedProperties as $key => $value) { 422 | // $data[$key] = array_merge($data[$key], $value); 423 | // } 424 | 425 | // return $data; 426 | // } 427 | 428 | function executeQuery($query, $method) 429 | { 430 | switch ($method) { 431 | case 'one': 432 | return $query->one(); 433 | case 'collect': 434 | return $query->collect(); 435 | case 'exists': 436 | return $query->exists(); 437 | case 'count': 438 | return $query->count(); 439 | case 'ids': 440 | return $query->ids(); 441 | case 'column': 442 | return $query->column(); 443 | default: 444 | return $query->all(); 445 | } 446 | } 447 | } 448 | -------------------------------------------------------------------------------- /types/craft-generated.d.ts: -------------------------------------------------------------------------------- 1 | export type Address = { 2 | ownerId: number | null; 3 | countryCode: string; 4 | administrativeArea: string | null; 5 | locality: string | null; 6 | dependentLocality: string | null; 7 | postalCode: string | null; 8 | sortingCode: string | null; 9 | addressLine1: string | null; 10 | addressLine2: string | null; 11 | organization: string | null; 12 | organizationTaxId: string | null; 13 | latitude: string | null; 14 | longitude: string | null; 15 | id: number | null; 16 | tempId: string | null; 17 | draftId: number | null; 18 | revisionId: number | null; 19 | isProvisionalDraft: boolean; 20 | uid: string | null; 21 | siteSettingsId: number | null; 22 | fieldLayoutId: number | null; 23 | structureId: number | null; 24 | contentId: number | null; 25 | enabled: boolean; 26 | archived: boolean; 27 | siteId: number | null; 28 | title: string | null; 29 | slug: string | null; 30 | uri: string | null; 31 | dateCreated: any | null; 32 | dateUpdated: any | null; 33 | dateLastMerged: any | null; 34 | dateDeleted: any | null; 35 | root: number | null; 36 | lft: number | null; 37 | rgt: number | null; 38 | level: number | null; 39 | searchScore: number | null; 40 | trashed: boolean; 41 | awaitingFieldValues: boolean; 42 | propagating: boolean; 43 | validatingRelatedElement: boolean; 44 | propagateAll: boolean; 45 | newSiteIds: Array; 46 | isNewForSite: boolean; 47 | resaving: boolean; 48 | duplicateOf: any | null; 49 | firstSave: boolean; 50 | mergingCanonicalChanges: boolean; 51 | updatingFromDerivative: boolean; 52 | previewing: boolean; 53 | hardDelete: boolean; 54 | fullName: string | null; 55 | firstName: string | null; 56 | lastName: string | null; 57 | }; 58 | export type AddressQuery = { 59 | ownerId: any; 60 | countryCode: any; 61 | administrativeArea: any; 62 | elementType: string; 63 | query: any | null; 64 | subQuery: any | null; 65 | contentTable: string | null; 66 | customFields: Array | null; 67 | inReverse: boolean; 68 | asArray: boolean; 69 | ignorePlaceholders: boolean; 70 | drafts: boolean | null; 71 | provisionalDrafts: boolean | null; 72 | draftId: number | null; 73 | draftOf: any; 74 | draftCreator: number | null; 75 | savedDraftsOnly: boolean; 76 | revisions: boolean | null; 77 | revisionId: number | null; 78 | revisionOf: number | null; 79 | revisionCreator: number | null; 80 | id: any; 81 | uid: any; 82 | siteSettingsId: any; 83 | fixedOrder: boolean; 84 | status: string | Array | null; 85 | archived: boolean; 86 | trashed: boolean | null; 87 | dateCreated: any; 88 | dateUpdated: any; 89 | siteId: any; 90 | unique: boolean; 91 | preferSites: Array | null; 92 | leaves: boolean; 93 | relatedTo: any; 94 | title: any; 95 | slug: any; 96 | uri: any; 97 | search: any; 98 | ref: any; 99 | with: string | Array | null; 100 | orderBy: any; 101 | withStructure: boolean | null; 102 | structureId: any; 103 | level: any; 104 | hasDescendants: boolean | null; 105 | ancestorOf: number | any | null; 106 | ancestorDist: number | null; 107 | descendantOf: number | any | null; 108 | descendantDist: number | null; 109 | siblingOf: number | any | null; 110 | prevSiblingOf: number | any | null; 111 | nextSiblingOf: number | any | null; 112 | positionedBefore: number | any | null; 113 | positionedAfter: number | any | null; 114 | select: Array | null; 115 | selectOption: string | null; 116 | distinct: boolean; 117 | from: Array | null; 118 | groupBy: Array | null; 119 | join: Array | null; 120 | having: string | Array | any | null; 121 | union: Array | null; 122 | withQueries: Array | null; 123 | params: Array | null; 124 | queryCacheDuration: number | boolean | null; 125 | queryCacheDependency: any | null; 126 | where: string | Array | any | null; 127 | limit: number | any | null; 128 | offset: number | any | null; 129 | indexBy: string | null; 130 | emulateExecution: boolean; 131 | }; 132 | export type Asset = { 133 | isFolder: boolean; 134 | sourcePath: Array | null; 135 | folderId: number | null; 136 | uploaderId: number | null; 137 | folderPath: string | null; 138 | kind: string | null; 139 | alt: string | null; 140 | size: number | null; 141 | keptFile: boolean | null; 142 | dateModified: any | null; 143 | newLocation: string | null; 144 | locationError: string | null; 145 | newFilename: string | null; 146 | newFolderId: number | null; 147 | tempFilePath: string | null; 148 | avoidFilenameConflicts: boolean; 149 | suggestedFilename: string | null; 150 | conflictingFilename: string | null; 151 | deletedWithVolume: boolean; 152 | keepFileOnDelete: boolean; 153 | id: number | null; 154 | tempId: string | null; 155 | draftId: number | null; 156 | revisionId: number | null; 157 | isProvisionalDraft: boolean; 158 | uid: string | null; 159 | siteSettingsId: number | null; 160 | fieldLayoutId: number | null; 161 | structureId: number | null; 162 | contentId: number | null; 163 | enabled: boolean; 164 | archived: boolean; 165 | siteId: number | null; 166 | title: string | null; 167 | slug: string | null; 168 | uri: string | null; 169 | dateCreated: any | null; 170 | dateUpdated: any | null; 171 | dateLastMerged: any | null; 172 | dateDeleted: any | null; 173 | root: number | null; 174 | lft: number | null; 175 | rgt: number | null; 176 | level: number | null; 177 | searchScore: number | null; 178 | trashed: boolean; 179 | awaitingFieldValues: boolean; 180 | propagating: boolean; 181 | validatingRelatedElement: boolean; 182 | propagateAll: boolean; 183 | newSiteIds: Array; 184 | isNewForSite: boolean; 185 | resaving: boolean; 186 | duplicateOf: any | null; 187 | firstSave: boolean; 188 | mergingCanonicalChanges: boolean; 189 | updatingFromDerivative: boolean; 190 | previewing: boolean; 191 | hardDelete: boolean; 192 | }; 193 | export type AssetQuery = { 194 | editable: boolean | null; 195 | savable: boolean | null; 196 | volumeId: any; 197 | folderId: any; 198 | uploaderId: number | null; 199 | filename: any; 200 | kind: any; 201 | hasAlt: boolean | null; 202 | width: any; 203 | height: any; 204 | size: any; 205 | dateModified: any; 206 | includeSubfolders: boolean; 207 | folderPath: string | null; 208 | withTransforms: any; 209 | elementType: string; 210 | query: any | null; 211 | subQuery: any | null; 212 | contentTable: string | null; 213 | customFields: Array | null; 214 | inReverse: boolean; 215 | asArray: boolean; 216 | ignorePlaceholders: boolean; 217 | drafts: boolean | null; 218 | provisionalDrafts: boolean | null; 219 | draftId: number | null; 220 | draftOf: any; 221 | draftCreator: number | null; 222 | savedDraftsOnly: boolean; 223 | revisions: boolean | null; 224 | revisionId: number | null; 225 | revisionOf: number | null; 226 | revisionCreator: number | null; 227 | id: any; 228 | uid: any; 229 | siteSettingsId: any; 230 | fixedOrder: boolean; 231 | status: string | Array | null; 232 | archived: boolean; 233 | trashed: boolean | null; 234 | dateCreated: any; 235 | dateUpdated: any; 236 | siteId: any; 237 | unique: boolean; 238 | preferSites: Array | null; 239 | leaves: boolean; 240 | relatedTo: any; 241 | title: any; 242 | slug: any; 243 | uri: any; 244 | search: any; 245 | ref: any; 246 | with: string | Array | null; 247 | orderBy: any; 248 | withStructure: boolean | null; 249 | structureId: any; 250 | level: any; 251 | hasDescendants: boolean | null; 252 | ancestorOf: number | any | null; 253 | ancestorDist: number | null; 254 | descendantOf: number | any | null; 255 | descendantDist: number | null; 256 | siblingOf: number | any | null; 257 | prevSiblingOf: number | any | null; 258 | nextSiblingOf: number | any | null; 259 | positionedBefore: number | any | null; 260 | positionedAfter: number | any | null; 261 | select: Array | null; 262 | selectOption: string | null; 263 | distinct: boolean; 264 | from: Array | null; 265 | groupBy: Array | null; 266 | join: Array | null; 267 | having: string | Array | any | null; 268 | union: Array | null; 269 | withQueries: Array | null; 270 | params: Array | null; 271 | queryCacheDuration: number | boolean | null; 272 | queryCacheDependency: any | null; 273 | where: string | Array | any | null; 274 | limit: number | any | null; 275 | offset: number | any | null; 276 | indexBy: string | null; 277 | emulateExecution: boolean; 278 | }; 279 | export type Category = { 280 | groupId: number | null; 281 | deletedWithGroup: boolean; 282 | id: number | null; 283 | tempId: string | null; 284 | draftId: number | null; 285 | revisionId: number | null; 286 | isProvisionalDraft: boolean; 287 | uid: string | null; 288 | siteSettingsId: number | null; 289 | fieldLayoutId: number | null; 290 | structureId: number | null; 291 | contentId: number | null; 292 | enabled: boolean; 293 | archived: boolean; 294 | siteId: number | null; 295 | title: string | null; 296 | slug: string | null; 297 | uri: string | null; 298 | dateCreated: any | null; 299 | dateUpdated: any | null; 300 | dateLastMerged: any | null; 301 | dateDeleted: any | null; 302 | root: number | null; 303 | lft: number | null; 304 | rgt: number | null; 305 | level: number | null; 306 | searchScore: number | null; 307 | trashed: boolean; 308 | awaitingFieldValues: boolean; 309 | propagating: boolean; 310 | validatingRelatedElement: boolean; 311 | propagateAll: boolean; 312 | newSiteIds: Array; 313 | isNewForSite: boolean; 314 | resaving: boolean; 315 | duplicateOf: any | null; 316 | firstSave: boolean; 317 | mergingCanonicalChanges: boolean; 318 | updatingFromDerivative: boolean; 319 | previewing: boolean; 320 | hardDelete: boolean; 321 | }; 322 | export type CategoryQuery = { 323 | editable: boolean; 324 | groupId: any; 325 | elementType: string; 326 | query: any | null; 327 | subQuery: any | null; 328 | contentTable: string | null; 329 | customFields: Array | null; 330 | inReverse: boolean; 331 | asArray: boolean; 332 | ignorePlaceholders: boolean; 333 | drafts: boolean | null; 334 | provisionalDrafts: boolean | null; 335 | draftId: number | null; 336 | draftOf: any; 337 | draftCreator: number | null; 338 | savedDraftsOnly: boolean; 339 | revisions: boolean | null; 340 | revisionId: number | null; 341 | revisionOf: number | null; 342 | revisionCreator: number | null; 343 | id: any; 344 | uid: any; 345 | siteSettingsId: any; 346 | fixedOrder: boolean; 347 | status: string | Array | null; 348 | archived: boolean; 349 | trashed: boolean | null; 350 | dateCreated: any; 351 | dateUpdated: any; 352 | siteId: any; 353 | unique: boolean; 354 | preferSites: Array | null; 355 | leaves: boolean; 356 | relatedTo: any; 357 | title: any; 358 | slug: any; 359 | uri: any; 360 | search: any; 361 | ref: any; 362 | with: string | Array | null; 363 | orderBy: any; 364 | withStructure: boolean | null; 365 | structureId: any; 366 | level: any; 367 | hasDescendants: boolean | null; 368 | ancestorOf: number | any | null; 369 | ancestorDist: number | null; 370 | descendantOf: number | any | null; 371 | descendantDist: number | null; 372 | siblingOf: number | any | null; 373 | prevSiblingOf: number | any | null; 374 | nextSiblingOf: number | any | null; 375 | positionedBefore: number | any | null; 376 | positionedAfter: number | any | null; 377 | select: Array | null; 378 | selectOption: string | null; 379 | distinct: boolean; 380 | from: Array | null; 381 | groupBy: Array | null; 382 | join: Array | null; 383 | having: string | Array | any | null; 384 | union: Array | null; 385 | withQueries: Array | null; 386 | params: Array | null; 387 | queryCacheDuration: number | boolean | null; 388 | queryCacheDependency: any | null; 389 | where: string | Array | any | null; 390 | limit: number | any | null; 391 | offset: number | any | null; 392 | indexBy: string | null; 393 | emulateExecution: boolean; 394 | }; 395 | export type EagerLoadPlan = { 396 | handle: string | null; 397 | alias: string | null; 398 | criteria: Array; 399 | all: boolean; 400 | count: boolean; 401 | when: null; 402 | nested: Array; 403 | }; 404 | export type ElementCollection = {}; 405 | export type ElementQuery = { 406 | elementType: string; 407 | query: any | null; 408 | subQuery: any | null; 409 | contentTable: string | null; 410 | customFields: Array | null; 411 | inReverse: boolean; 412 | asArray: boolean; 413 | ignorePlaceholders: boolean; 414 | drafts: boolean | null; 415 | provisionalDrafts: boolean | null; 416 | draftId: number | null; 417 | draftOf: any; 418 | draftCreator: number | null; 419 | savedDraftsOnly: boolean; 420 | revisions: boolean | null; 421 | revisionId: number | null; 422 | revisionOf: number | null; 423 | revisionCreator: number | null; 424 | id: any; 425 | uid: any; 426 | siteSettingsId: any; 427 | fixedOrder: boolean; 428 | status: string | Array | null; 429 | archived: boolean; 430 | trashed: boolean | null; 431 | dateCreated: any; 432 | dateUpdated: any; 433 | siteId: any; 434 | unique: boolean; 435 | preferSites: Array | null; 436 | leaves: boolean; 437 | relatedTo: any; 438 | title: any; 439 | slug: any; 440 | uri: any; 441 | search: any; 442 | ref: any; 443 | with: string | Array | null; 444 | orderBy: any; 445 | withStructure: boolean | null; 446 | structureId: any; 447 | level: any; 448 | hasDescendants: boolean | null; 449 | ancestorOf: number | any | null; 450 | ancestorDist: number | null; 451 | descendantOf: number | any | null; 452 | descendantDist: number | null; 453 | siblingOf: number | any | null; 454 | prevSiblingOf: number | any | null; 455 | nextSiblingOf: number | any | null; 456 | positionedBefore: number | any | null; 457 | positionedAfter: number | any | null; 458 | select: Array | null; 459 | selectOption: string | null; 460 | distinct: boolean; 461 | from: Array | null; 462 | groupBy: Array | null; 463 | join: Array | null; 464 | having: string | Array | any | null; 465 | union: Array | null; 466 | withQueries: Array | null; 467 | params: Array | null; 468 | queryCacheDuration: number | boolean | null; 469 | queryCacheDependency: any | null; 470 | where: string | Array | any | null; 471 | limit: number | any | null; 472 | offset: number | any | null; 473 | indexBy: string | null; 474 | emulateExecution: boolean; 475 | }; 476 | export type ElementQueryInterface = {}; 477 | export type ElementRelationParamParser = { 478 | fields: Array | null; 479 | }; 480 | export type Entry = { 481 | sectionId: number | null; 482 | postDate: any | null; 483 | expiryDate: any | null; 484 | deletedWithEntryType: boolean; 485 | _authorId: number | null; 486 | id: number | null; 487 | tempId: string | null; 488 | draftId: number | null; 489 | revisionId: number | null; 490 | isProvisionalDraft: boolean; 491 | uid: string | null; 492 | siteSettingsId: number | null; 493 | fieldLayoutId: number | null; 494 | structureId: number | null; 495 | contentId: number | null; 496 | enabled: boolean; 497 | archived: boolean; 498 | siteId: number | null; 499 | title: string | null; 500 | slug: string | null; 501 | uri: string | null; 502 | dateCreated: any | null; 503 | dateUpdated: any | null; 504 | dateLastMerged: any | null; 505 | dateDeleted: any | null; 506 | root: number | null; 507 | lft: number | null; 508 | rgt: number | null; 509 | level: number | null; 510 | searchScore: number | null; 511 | trashed: boolean; 512 | awaitingFieldValues: boolean; 513 | propagating: boolean; 514 | validatingRelatedElement: boolean; 515 | propagateAll: boolean; 516 | newSiteIds: Array; 517 | isNewForSite: boolean; 518 | resaving: boolean; 519 | duplicateOf: any | null; 520 | firstSave: boolean; 521 | mergingCanonicalChanges: boolean; 522 | updatingFromDerivative: boolean; 523 | previewing: boolean; 524 | hardDelete: boolean; 525 | }; 526 | export type EntryQuery = { 527 | editable: boolean | null; 528 | savable: boolean | null; 529 | sectionId: any; 530 | typeId: any; 531 | authorId: any; 532 | authorGroupId: any; 533 | postDate: any; 534 | before: any; 535 | after: any; 536 | expiryDate: any; 537 | elementType: string; 538 | query: any | null; 539 | subQuery: any | null; 540 | contentTable: string | null; 541 | customFields: Array | null; 542 | inReverse: boolean; 543 | asArray: boolean; 544 | ignorePlaceholders: boolean; 545 | drafts: boolean | null; 546 | provisionalDrafts: boolean | null; 547 | draftId: number | null; 548 | draftOf: any; 549 | draftCreator: number | null; 550 | savedDraftsOnly: boolean; 551 | revisions: boolean | null; 552 | revisionId: number | null; 553 | revisionOf: number | null; 554 | revisionCreator: number | null; 555 | id: any; 556 | uid: any; 557 | siteSettingsId: any; 558 | fixedOrder: boolean; 559 | status: string | Array | null; 560 | archived: boolean; 561 | trashed: boolean | null; 562 | dateCreated: any; 563 | dateUpdated: any; 564 | siteId: any; 565 | unique: boolean; 566 | preferSites: Array | null; 567 | leaves: boolean; 568 | relatedTo: any; 569 | title: any; 570 | slug: any; 571 | uri: any; 572 | search: any; 573 | ref: any; 574 | with: string | Array | null; 575 | orderBy: any; 576 | withStructure: boolean | null; 577 | structureId: any; 578 | level: any; 579 | hasDescendants: boolean | null; 580 | ancestorOf: number | any | null; 581 | ancestorDist: number | null; 582 | descendantOf: number | any | null; 583 | descendantDist: number | null; 584 | siblingOf: number | any | null; 585 | prevSiblingOf: number | any | null; 586 | nextSiblingOf: number | any | null; 587 | positionedBefore: number | any | null; 588 | positionedAfter: number | any | null; 589 | select: Array | null; 590 | selectOption: string | null; 591 | distinct: boolean; 592 | from: Array | null; 593 | groupBy: Array | null; 594 | join: Array | null; 595 | having: string | Array | any | null; 596 | union: Array | null; 597 | withQueries: Array | null; 598 | params: Array | null; 599 | queryCacheDuration: number | boolean | null; 600 | queryCacheDependency: any | null; 601 | where: string | Array | any | null; 602 | limit: number | any | null; 603 | offset: number | any | null; 604 | indexBy: string | null; 605 | emulateExecution: boolean; 606 | }; 607 | export type GlobalSet = { 608 | name: string | null; 609 | handle: string | null; 610 | sortOrder: number | null; 611 | id: number | null; 612 | tempId: string | null; 613 | draftId: number | null; 614 | revisionId: number | null; 615 | isProvisionalDraft: boolean; 616 | uid: string | null; 617 | siteSettingsId: number | null; 618 | fieldLayoutId: number | null; 619 | structureId: number | null; 620 | contentId: number | null; 621 | enabled: boolean; 622 | archived: boolean; 623 | siteId: number | null; 624 | title: string | null; 625 | slug: string | null; 626 | uri: string | null; 627 | dateCreated: any | null; 628 | dateUpdated: any | null; 629 | dateLastMerged: any | null; 630 | dateDeleted: any | null; 631 | root: number | null; 632 | lft: number | null; 633 | rgt: number | null; 634 | level: number | null; 635 | searchScore: number | null; 636 | trashed: boolean; 637 | awaitingFieldValues: boolean; 638 | propagating: boolean; 639 | validatingRelatedElement: boolean; 640 | propagateAll: boolean; 641 | newSiteIds: Array; 642 | isNewForSite: boolean; 643 | resaving: boolean; 644 | duplicateOf: any | null; 645 | firstSave: boolean; 646 | mergingCanonicalChanges: boolean; 647 | updatingFromDerivative: boolean; 648 | previewing: boolean; 649 | hardDelete: boolean; 650 | }; 651 | export type GlobalSetQuery = { 652 | editable: boolean; 653 | handle: string | Array | null; 654 | elementType: string; 655 | query: any | null; 656 | subQuery: any | null; 657 | contentTable: string | null; 658 | customFields: Array | null; 659 | inReverse: boolean; 660 | asArray: boolean; 661 | ignorePlaceholders: boolean; 662 | drafts: boolean | null; 663 | provisionalDrafts: boolean | null; 664 | draftId: number | null; 665 | draftOf: any; 666 | draftCreator: number | null; 667 | savedDraftsOnly: boolean; 668 | revisions: boolean | null; 669 | revisionId: number | null; 670 | revisionOf: number | null; 671 | revisionCreator: number | null; 672 | id: any; 673 | uid: any; 674 | siteSettingsId: any; 675 | fixedOrder: boolean; 676 | status: string | Array | null; 677 | archived: boolean; 678 | trashed: boolean | null; 679 | dateCreated: any; 680 | dateUpdated: any; 681 | siteId: any; 682 | unique: boolean; 683 | preferSites: Array | null; 684 | leaves: boolean; 685 | relatedTo: any; 686 | title: any; 687 | slug: any; 688 | uri: any; 689 | search: any; 690 | ref: any; 691 | with: string | Array | null; 692 | orderBy: any; 693 | withStructure: boolean | null; 694 | structureId: any; 695 | level: any; 696 | hasDescendants: boolean | null; 697 | ancestorOf: number | any | null; 698 | ancestorDist: number | null; 699 | descendantOf: number | any | null; 700 | descendantDist: number | null; 701 | siblingOf: number | any | null; 702 | prevSiblingOf: number | any | null; 703 | nextSiblingOf: number | any | null; 704 | positionedBefore: number | any | null; 705 | positionedAfter: number | any | null; 706 | select: Array | null; 707 | selectOption: string | null; 708 | distinct: boolean; 709 | from: Array | null; 710 | groupBy: Array | null; 711 | join: Array | null; 712 | having: string | Array | any | null; 713 | union: Array | null; 714 | withQueries: Array | null; 715 | params: Array | null; 716 | queryCacheDuration: number | boolean | null; 717 | queryCacheDependency: any | null; 718 | where: string | Array | any | null; 719 | limit: number | any | null; 720 | offset: number | any | null; 721 | indexBy: string | null; 722 | emulateExecution: boolean; 723 | }; 724 | export type MatrixBlock = { 725 | fieldId: number | null; 726 | primaryOwnerId: number | null; 727 | ownerId: number | null; 728 | typeId: number | null; 729 | sortOrder: number | null; 730 | dirty: boolean; 731 | collapsed: boolean; 732 | deletedWithOwner: boolean; 733 | saveOwnership: boolean; 734 | id: number | null; 735 | tempId: string | null; 736 | draftId: number | null; 737 | revisionId: number | null; 738 | isProvisionalDraft: boolean; 739 | uid: string | null; 740 | siteSettingsId: number | null; 741 | fieldLayoutId: number | null; 742 | structureId: number | null; 743 | contentId: number | null; 744 | enabled: boolean; 745 | archived: boolean; 746 | siteId: number | null; 747 | title: string | null; 748 | slug: string | null; 749 | uri: string | null; 750 | dateCreated: any | null; 751 | dateUpdated: any | null; 752 | dateLastMerged: any | null; 753 | dateDeleted: any | null; 754 | root: number | null; 755 | lft: number | null; 756 | rgt: number | null; 757 | level: number | null; 758 | searchScore: number | null; 759 | trashed: boolean; 760 | awaitingFieldValues: boolean; 761 | propagating: boolean; 762 | validatingRelatedElement: boolean; 763 | propagateAll: boolean; 764 | newSiteIds: Array; 765 | isNewForSite: boolean; 766 | resaving: boolean; 767 | duplicateOf: any | null; 768 | firstSave: boolean; 769 | mergingCanonicalChanges: boolean; 770 | updatingFromDerivative: boolean; 771 | previewing: boolean; 772 | hardDelete: boolean; 773 | }; 774 | export type MatrixBlockQuery = { 775 | fieldId: any; 776 | primaryOwnerId: any; 777 | ownerId: any; 778 | allowOwnerDrafts: boolean | null; 779 | allowOwnerRevisions: boolean | null; 780 | typeId: any; 781 | elementType: string; 782 | query: any | null; 783 | subQuery: any | null; 784 | contentTable: string | null; 785 | customFields: Array | null; 786 | inReverse: boolean; 787 | asArray: boolean; 788 | ignorePlaceholders: boolean; 789 | drafts: boolean | null; 790 | provisionalDrafts: boolean | null; 791 | draftId: number | null; 792 | draftOf: any; 793 | draftCreator: number | null; 794 | savedDraftsOnly: boolean; 795 | revisions: boolean | null; 796 | revisionId: number | null; 797 | revisionOf: number | null; 798 | revisionCreator: number | null; 799 | id: any; 800 | uid: any; 801 | siteSettingsId: any; 802 | fixedOrder: boolean; 803 | status: string | Array | null; 804 | archived: boolean; 805 | trashed: boolean | null; 806 | dateCreated: any; 807 | dateUpdated: any; 808 | siteId: any; 809 | unique: boolean; 810 | preferSites: Array | null; 811 | leaves: boolean; 812 | relatedTo: any; 813 | title: any; 814 | slug: any; 815 | uri: any; 816 | search: any; 817 | ref: any; 818 | with: string | Array | null; 819 | orderBy: any; 820 | withStructure: boolean | null; 821 | structureId: any; 822 | level: any; 823 | hasDescendants: boolean | null; 824 | ancestorOf: number | any | null; 825 | ancestorDist: number | null; 826 | descendantOf: number | any | null; 827 | descendantDist: number | null; 828 | siblingOf: number | any | null; 829 | prevSiblingOf: number | any | null; 830 | nextSiblingOf: number | any | null; 831 | positionedBefore: number | any | null; 832 | positionedAfter: number | any | null; 833 | select: Array | null; 834 | selectOption: string | null; 835 | distinct: boolean; 836 | from: Array | null; 837 | groupBy: Array | null; 838 | join: Array | null; 839 | having: string | Array | any | null; 840 | union: Array | null; 841 | withQueries: Array | null; 842 | params: Array | null; 843 | queryCacheDuration: number | boolean | null; 844 | queryCacheDependency: any | null; 845 | where: string | Array | any | null; 846 | limit: number | any | null; 847 | offset: number | any | null; 848 | indexBy: string | null; 849 | emulateExecution: boolean; 850 | }; 851 | export type Tag = { 852 | groupId: number | null; 853 | deletedWithGroup: boolean; 854 | id: number | null; 855 | tempId: string | null; 856 | draftId: number | null; 857 | revisionId: number | null; 858 | isProvisionalDraft: boolean; 859 | uid: string | null; 860 | siteSettingsId: number | null; 861 | fieldLayoutId: number | null; 862 | structureId: number | null; 863 | contentId: number | null; 864 | enabled: boolean; 865 | archived: boolean; 866 | siteId: number | null; 867 | title: string | null; 868 | slug: string | null; 869 | uri: string | null; 870 | dateCreated: any | null; 871 | dateUpdated: any | null; 872 | dateLastMerged: any | null; 873 | dateDeleted: any | null; 874 | root: number | null; 875 | lft: number | null; 876 | rgt: number | null; 877 | level: number | null; 878 | searchScore: number | null; 879 | trashed: boolean; 880 | awaitingFieldValues: boolean; 881 | propagating: boolean; 882 | validatingRelatedElement: boolean; 883 | propagateAll: boolean; 884 | newSiteIds: Array; 885 | isNewForSite: boolean; 886 | resaving: boolean; 887 | duplicateOf: any | null; 888 | firstSave: boolean; 889 | mergingCanonicalChanges: boolean; 890 | updatingFromDerivative: boolean; 891 | previewing: boolean; 892 | hardDelete: boolean; 893 | }; 894 | export type TagQuery = { 895 | groupId: any; 896 | elementType: string; 897 | query: any | null; 898 | subQuery: any | null; 899 | contentTable: string | null; 900 | customFields: Array | null; 901 | inReverse: boolean; 902 | asArray: boolean; 903 | ignorePlaceholders: boolean; 904 | drafts: boolean | null; 905 | provisionalDrafts: boolean | null; 906 | draftId: number | null; 907 | draftOf: any; 908 | draftCreator: number | null; 909 | savedDraftsOnly: boolean; 910 | revisions: boolean | null; 911 | revisionId: number | null; 912 | revisionOf: number | null; 913 | revisionCreator: number | null; 914 | id: any; 915 | uid: any; 916 | siteSettingsId: any; 917 | fixedOrder: boolean; 918 | status: string | Array | null; 919 | archived: boolean; 920 | trashed: boolean | null; 921 | dateCreated: any; 922 | dateUpdated: any; 923 | siteId: any; 924 | unique: boolean; 925 | preferSites: Array | null; 926 | leaves: boolean; 927 | relatedTo: any; 928 | title: any; 929 | slug: any; 930 | uri: any; 931 | search: any; 932 | ref: any; 933 | with: string | Array | null; 934 | orderBy: any; 935 | withStructure: boolean | null; 936 | structureId: any; 937 | level: any; 938 | hasDescendants: boolean | null; 939 | ancestorOf: number | any | null; 940 | ancestorDist: number | null; 941 | descendantOf: number | any | null; 942 | descendantDist: number | null; 943 | siblingOf: number | any | null; 944 | prevSiblingOf: number | any | null; 945 | nextSiblingOf: number | any | null; 946 | positionedBefore: number | any | null; 947 | positionedAfter: number | any | null; 948 | select: Array | null; 949 | selectOption: string | null; 950 | distinct: boolean; 951 | from: Array | null; 952 | groupBy: Array | null; 953 | join: Array | null; 954 | having: string | Array | any | null; 955 | union: Array | null; 956 | withQueries: Array | null; 957 | params: Array | null; 958 | queryCacheDuration: number | boolean | null; 959 | queryCacheDependency: any | null; 960 | where: string | Array | any | null; 961 | limit: number | any | null; 962 | offset: number | any | null; 963 | indexBy: string | null; 964 | emulateExecution: boolean; 965 | }; 966 | export type User = { 967 | photoId: number | null; 968 | active: boolean; 969 | pending: boolean; 970 | locked: boolean; 971 | suspended: boolean; 972 | admin: boolean; 973 | username: string | null; 974 | email: string | null; 975 | password: string | null; 976 | lastLoginDate: any | null; 977 | invalidLoginCount: number | null; 978 | lastInvalidLoginDate: any | null; 979 | lockoutDate: any | null; 980 | hasDashboard: boolean; 981 | passwordResetRequired: boolean; 982 | lastPasswordChangeDate: any | null; 983 | unverifiedEmail: string | null; 984 | newPassword: string | null; 985 | currentPassword: string | null; 986 | verificationCodeIssuedDate: any | null; 987 | verificationCode: string | null; 988 | lastLoginAttemptIp: string | null; 989 | authError: string | null; 990 | inheritorOnDelete: User | null; 991 | id: number | null; 992 | tempId: string | null; 993 | draftId: number | null; 994 | revisionId: number | null; 995 | isProvisionalDraft: boolean; 996 | uid: string | null; 997 | siteSettingsId: number | null; 998 | fieldLayoutId: number | null; 999 | structureId: number | null; 1000 | contentId: number | null; 1001 | enabled: boolean; 1002 | archived: boolean; 1003 | siteId: number | null; 1004 | title: string | null; 1005 | slug: string | null; 1006 | uri: string | null; 1007 | dateCreated: any | null; 1008 | dateUpdated: any | null; 1009 | dateLastMerged: any | null; 1010 | dateDeleted: any | null; 1011 | root: number | null; 1012 | lft: number | null; 1013 | rgt: number | null; 1014 | level: number | null; 1015 | searchScore: number | null; 1016 | trashed: boolean; 1017 | awaitingFieldValues: boolean; 1018 | propagating: boolean; 1019 | validatingRelatedElement: boolean; 1020 | propagateAll: boolean; 1021 | newSiteIds: Array; 1022 | isNewForSite: boolean; 1023 | resaving: boolean; 1024 | duplicateOf: any | null; 1025 | firstSave: boolean; 1026 | mergingCanonicalChanges: boolean; 1027 | updatingFromDerivative: boolean; 1028 | previewing: boolean; 1029 | hardDelete: boolean; 1030 | fullName: string | null; 1031 | firstName: string | null; 1032 | lastName: string | null; 1033 | }; 1034 | export type UserQuery = { 1035 | admin: boolean | null; 1036 | authors: boolean | null; 1037 | assetUploaders: boolean | null; 1038 | hasPhoto: boolean | null; 1039 | can: any; 1040 | groupId: any; 1041 | email: any; 1042 | username: any; 1043 | fullName: any; 1044 | firstName: any; 1045 | lastName: any; 1046 | lastLoginDate: any; 1047 | withGroups: boolean; 1048 | elementType: string; 1049 | query: any | null; 1050 | subQuery: any | null; 1051 | contentTable: string | null; 1052 | customFields: Array | null; 1053 | inReverse: boolean; 1054 | asArray: boolean; 1055 | ignorePlaceholders: boolean; 1056 | drafts: boolean | null; 1057 | provisionalDrafts: boolean | null; 1058 | draftId: number | null; 1059 | draftOf: any; 1060 | draftCreator: number | null; 1061 | savedDraftsOnly: boolean; 1062 | revisions: boolean | null; 1063 | revisionId: number | null; 1064 | revisionOf: number | null; 1065 | revisionCreator: number | null; 1066 | id: any; 1067 | uid: any; 1068 | siteSettingsId: any; 1069 | fixedOrder: boolean; 1070 | status: string | Array | null; 1071 | archived: boolean; 1072 | trashed: boolean | null; 1073 | dateCreated: any; 1074 | dateUpdated: any; 1075 | siteId: any; 1076 | unique: boolean; 1077 | preferSites: Array | null; 1078 | leaves: boolean; 1079 | relatedTo: any; 1080 | title: any; 1081 | slug: any; 1082 | uri: any; 1083 | search: any; 1084 | ref: any; 1085 | with: string | Array | null; 1086 | orderBy: any; 1087 | withStructure: boolean | null; 1088 | structureId: any; 1089 | level: any; 1090 | hasDescendants: boolean | null; 1091 | ancestorOf: number | any | null; 1092 | ancestorDist: number | null; 1093 | descendantOf: number | any | null; 1094 | descendantDist: number | null; 1095 | siblingOf: number | any | null; 1096 | prevSiblingOf: number | any | null; 1097 | nextSiblingOf: number | any | null; 1098 | positionedBefore: number | any | null; 1099 | positionedAfter: number | any | null; 1100 | select: Array | null; 1101 | selectOption: string | null; 1102 | distinct: boolean; 1103 | from: Array | null; 1104 | groupBy: Array | null; 1105 | join: Array | null; 1106 | having: string | Array | any | null; 1107 | union: Array | null; 1108 | withQueries: Array | null; 1109 | params: Array | null; 1110 | queryCacheDuration: number | boolean | null; 1111 | queryCacheDependency: any | null; 1112 | where: string | Array | any | null; 1113 | limit: number | any | null; 1114 | offset: number | any | null; 1115 | indexBy: string | null; 1116 | emulateExecution: boolean; 1117 | }; 1118 | --------------------------------------------------------------------------------