├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .github
├── ISSUE_TEMPLATE
│ ├── 1. Umbraco Heartcore Client.yml
│ └── config.yml
├── LICENSE.md
├── README.md
└── img
│ └── logo.png
├── .gitignore
├── .npmignore
├── LICENSE.md
├── api-extractor.json
├── etc
└── headless-client.api.md
├── examples
└── uploadImageFile.js
├── package-lock.json
├── package.json
├── samples
├── gridsome
│ ├── .env
│ ├── .gitignore
│ ├── README.md
│ ├── gridsome.config.js
│ ├── gridsome.server.js
│ ├── package-lock.json
│ ├── package.json
│ ├── src
│ │ ├── components
│ │ │ ├── Elements.vue
│ │ │ ├── Hero.vue
│ │ │ ├── MainNavigation.vue
│ │ │ ├── PageFooter.vue
│ │ │ ├── PageHeader.vue
│ │ │ ├── README.md
│ │ │ ├── UniqueSellingPoints.vue
│ │ │ └── elements
│ │ │ │ └── TextAndImage.vue
│ │ ├── favicon.png
│ │ ├── layouts
│ │ │ ├── Default.vue
│ │ │ └── README.md
│ │ ├── main.js
│ │ ├── pages
│ │ │ └── README.md
│ │ ├── templates
│ │ │ ├── Frontpage.vue
│ │ │ ├── README.md
│ │ │ └── Textpage.vue
│ │ ├── types.ts
│ │ └── vue-shims.d.ts
│ ├── static
│ │ └── README.md
│ └── tsconfig.json
├── koa
│ ├── README.md
│ ├── app.ts
│ ├── package-lock.json
│ ├── package.json
│ ├── public
│ │ ├── css
│ │ │ └── site.css
│ │ └── favicon.ico
│ ├── tsconfig.json
│ └── views
│ │ ├── _layout.njk
│ │ ├── frontpage.njk
│ │ ├── partials
│ │ ├── _elements.njk
│ │ ├── _footer.njk
│ │ ├── _hero.njk
│ │ ├── _mainNavigation.njk
│ │ ├── _uniqueSellingPoints.njk
│ │ └── elements
│ │ │ └── _textAndImage.njk
│ │ └── textpage.njk
└── vue
│ ├── .browserslistrc
│ ├── .editorconfig
│ ├── .env
│ ├── .eslintrc.js
│ ├── .gitignore
│ ├── README.md
│ ├── babel.config.js
│ ├── package-lock.json
│ ├── package.json
│ ├── public
│ ├── favicon.ico
│ └── index.html
│ ├── src
│ ├── App.vue
│ ├── assets
│ │ └── logo.svg
│ ├── client.ts
│ ├── components
│ │ ├── Elements.vue
│ │ ├── Hero.vue
│ │ ├── MainNavigation.vue
│ │ ├── PageFooter.vue
│ │ ├── PageHeader.vue
│ │ ├── UniqueSellingPoints.vue
│ │ └── elements
│ │ │ └── TextAndImage.vue
│ ├── main.ts
│ ├── router
│ │ └── index.ts
│ ├── shims-tsx.d.ts
│ ├── shims-vue.d.ts
│ ├── types.ts
│ └── views
│ │ ├── Frontpage.vue
│ │ ├── NotFound.vue
│ │ ├── Textpage.vue
│ │ └── UmbracoPage.vue
│ ├── tsconfig.json
│ └── vue.config.js
├── src
├── APIRequestError.ts
├── ApiRequest.ts
├── ApiResponse.ts
├── Client.ts
├── Clients
│ ├── AuthenticationClient.spec.ts
│ ├── AuthenticationClient.ts
│ ├── Delivery
│ │ ├── ContentDeliveryClient.ts
│ │ ├── DeliveryClient.ts
│ │ ├── MediaDeliveryClient.ts
│ │ ├── RedirectDeliveryClient.ts
│ │ └── index.ts
│ ├── Management
│ │ ├── ContentManagementClient.spec.ts
│ │ ├── ContentManagementClient.ts
│ │ ├── ManagementClient.ts
│ │ ├── MediaManagementClient.ts
│ │ ├── MemberManagementClient.spec.ts
│ │ ├── MemberManagementClient.ts
│ │ ├── __mocks__
│ │ │ ├── content.byId.json
│ │ │ ├── content.children.json
│ │ │ ├── content.create.json
│ │ │ ├── content.root.json
│ │ │ ├── member.byUsername.json
│ │ │ ├── member.create.json
│ │ │ └── member.createResetPasswordToken.json
│ │ └── index.ts
│ └── index.ts
├── Endpoint.ts
├── Endpoints.ts
├── RequestOptions
│ ├── ContentFilterOptions.ts
│ ├── ContentTypeOptions.ts
│ ├── CultureOptions.ts
│ ├── DepthOptions.ts
│ ├── HyperlinksOption.ts
│ ├── PageOptions.ts
│ ├── RequestOptions.ts
│ └── index.ts
├── Responses
│ ├── Content.ts
│ ├── ContentLanguageType.ts
│ ├── ContentManagementContent.ts
│ ├── ContentManagementMedia.ts
│ ├── ContentManagementMember.ts
│ ├── ContentManagerMediaType.ts
│ ├── ContentRelationType.ts
│ ├── ContentTypeBaseResponse.ts
│ ├── Form.ts
│ ├── Media.ts
│ ├── OAUthResponse.ts
│ ├── PagedResponse.ts
│ ├── Redirect.ts
│ └── index.ts
└── index.ts
├── tsconfig.json
└── tsconfig_spec.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | # Default settings:
7 | # A newline ending every file
8 | # Use 4 spaces as indentation
9 | [*]
10 | insert_final_newline = true
11 | end_of_line = lf
12 | indent_style = space
13 | indent_size = 4
14 |
15 | # Trim trailing whitespace, limited support.
16 | # https://github.com/editorconfig/editorconfig/wiki/Property-research:-Trim-trailing-spaces
17 | trim_trailing_whitespace = true
18 |
19 | [*.{css,html,js,json,ts,yml,yaml}]
20 | indent_size = 2
21 |
22 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | dist/
2 | docs/
3 | node_modules/
4 | samples/
5 |
6 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | parser: '@typescript-eslint/parser',
4 | parserOptions: {
5 | tsconfigRootDir: __dirname,
6 | project: ['./tsconfig*.json'],
7 | },
8 | plugins: [
9 | '@typescript-eslint',
10 | ],
11 | extends: [
12 | 'standard-with-typescript',
13 | ],
14 | rules: {
15 | '@typescript-eslint/strict-boolean-expressions': ['warn'],
16 | '@typescript-eslint/explicit-function-return-type': ['warn'],
17 | },
18 | overrides: [{
19 | files: ['*.spec.ts'],
20 | plugins: ['mocha'],
21 | extends: ['plugin:mocha/recommended'],
22 | rules: {
23 | 'no-unused-expressions': 'off',
24 | '@typescript-eslint/no-var-requires': 'off',
25 | }
26 | }]
27 | };
28 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/1. Umbraco Heartcore Client.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: 🔆 Umbraco Heartcore Client Issue
3 | description: "Create issues/feature requests for our Umbraco Heartcore NodeJs Client Library"
4 | body:
5 | - type: textarea
6 | id: "description"
7 | attributes:
8 | label: "Issue description"
9 | description: "Write a summary of the issue you want to report."
10 | validations:
11 | required: true
12 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: ⁉ Umbraco Heartcore Support Question
4 | url: https://www.umbraco.io
5 | about: For support questions please log into your umbraco.io account and use the chat in the bottom right corner to talk to our support team.
6 | - name: 🔒 Security Issue
7 | url: https://umbraco.com/about-us/trust-center/security-and-umbraco/how-to-report-a-vulnerability-in-umbraco/
8 | about: Please visit our security center to report any possible security issues
9 | - name: 🚀 Umbraco Heartcore Issues
10 | url: https://github.com/umbraco/Umbraco.Heartcore.Issues/issues
11 | about: For issues regarding Umbraco Heartcore please visit the corresponding issue tracker.
12 | - name: 💻 Umbraco Heartcore .NET Client Issues
13 | url: https://github.com/umbraco/Umbraco.Headless.Client.Net/issues
14 | about: For issues regarding the .NET client library for Heartcore please visit the corresponding issue tracker.
15 | - name: 📝 Umbraco Heartcore Forms Presentation
16 | url: https://github.com/umbraco/Umbraco.Headless.Forms.Presentation/issues
17 | about: For issues regarding the Umbraco Forms libraries please visit the corresponding issue tracker.
18 |
--------------------------------------------------------------------------------
/.github/LICENSE.md:
--------------------------------------------------------------------------------
1 | # The MIT License (MIT) #
2 |
3 | Copyright (c) 2013-present Umbraco
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/.github/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | # NodeJS Client Library for Umbraco Heartcore
8 |
9 | Umbraco Heartcore is the headless CMS version of Umbraco as a Service.
10 |
11 | This repository contains the Node.JS client library for the Umbraco Heartcore REST APIs.
12 |
13 | ## Prerequisites
14 |
15 | * [NodeJS](https://nodejs.org) 10 or newer
16 |
17 | ## Install
18 |
19 | ```bash
20 | > npm install @umbraco/headless-client
21 | ```
22 |
23 | ## Usage
24 |
25 | Create a client, then call commands on it
26 |
27 | ```typescript
28 |
29 | // client.ts
30 | import {Client} from '@umbraco/headless-client'
31 |
32 | const client = new Client({
33 | projectAlias: 'your-project-alias',
34 | apiKey: 'your-api-key',
35 | language: 'iso-code', // can be overwritten per method
36 | preview: true // true/false if the preview API should be used
37 | })
38 |
39 | export default client
40 |
41 | // rootLinks.ts
42 | async function rootLinks(client: Client) {
43 | const rootContent = await client.delivery.content.root()
44 |
45 | const childPages = rootContent.map(child => ({
46 | url: child._url,
47 | name: child.name
48 | }))
49 |
50 | return childPages
51 | }
52 |
53 | function linkGenerator(links: {url: string, name: string}[]) {
54 | return links.map(link => {
55 | return `${link.name}`
56 | })
57 | }
58 |
59 | async function main() {
60 | const rootLinks = await rootLinks(require('./client').default)
61 | const links = linkGenerator(rootLinks)
62 | console.log(links)
63 | }
64 |
65 | ```
66 |
67 | ## Documentation
68 |
69 | General documentation for Umbraco Heartcore can be found on [our.umbraco.com](https://our.umbraco.com/documentation/Umbraco-Heartcore/).
70 |
71 | API documentation for the Client library can be generated by running the following
72 |
73 | ```bash
74 | > npm install # install dependencies
75 | > npm run build # build the source
76 | > npm run docs # generate the documentation
77 | ```
78 |
79 | This will generate markdown files in `docs/api` that can be openend in any markdown viewer.
80 |
81 | A simple way to view them is to use [markserv](https://www.npmjs.com/package/markserv) by running
82 |
83 | ```bash
84 | > npx markserv docs/api
85 | ```
86 |
--------------------------------------------------------------------------------
/.github/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umbraco/Umbraco.Headless.Client.NodeJs/8b5cf81884509dcd018e9260181a0abe21136946/.github/img/logo.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Build output
9 | dist/
10 |
11 | # Runtime data
12 | pids
13 | *.pid
14 | *.seed
15 | *.pid.lock
16 |
17 | # Directory for instrumented libs generated by jscoverage/JSCover
18 | lib-cov
19 |
20 | # Coverage directory used by tools like istanbul
21 | coverage
22 |
23 | # nyc test coverage
24 | .nyc_output
25 |
26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
27 | .grunt
28 |
29 | # Bower dependency directory (https://bower.io/)
30 | bower_components
31 |
32 | # node-waf configuration
33 | .lock-wscript
34 |
35 | # Compiled binary addons (https://nodejs.org/api/addons.html)
36 | build/Release
37 |
38 | # Dependency directories
39 | node_modules/
40 | jspm_packages/
41 |
42 | # TypeScript v1 declaration files
43 | typings/
44 |
45 | # Optional npm cache directory
46 | .npm
47 |
48 | # Optional eslint cache
49 | .eslintcache
50 |
51 | # Optional REPL history
52 | .node_repl_history
53 |
54 | # Output of 'npm pack'
55 | *.tgz
56 |
57 | # Yarn Integrity file
58 | .yarn-integrity
59 |
60 | # local env files
61 | .env.local
62 | .env.*.local
63 |
64 | # next.js build output
65 | .next
66 | .idea
67 |
68 | # docs
69 | docs/
70 | temp/
71 | tsdoc-metadata.json
72 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | samples/
2 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | # The MIT License (MIT) #
2 |
3 | Copyright (c) 2013-present Umbraco
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/api-extractor.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
3 | "mainEntryPointFilePath": "dist/index.d.ts",
4 | "apiReport": {
5 | "enabled": true,
6 | "reportFolder": "etc"
7 | },
8 | "docModel": {
9 | "enabled": true,
10 | "apiJsonFilePath": "dist/umbraco-headless.api.json"
11 | },
12 | "dtsRollup": {
13 | "enabled": true
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/examples/uploadImageFile.js:
--------------------------------------------------------------------------------
1 | // Remember to install the following packages
2 | const { Client } = require("@umbraco/headless-client");
3 | const FormData = require("form-data");
4 | const fs = require("fs");
5 | const path = require("path");
6 |
7 | const client = new Client({
8 | projectAlias: "the-unique-alias", // https://[here is your alias].s1.umbraco.io/
9 | language: "en-US",
10 | apiKey: process.env.UMBRACO_KEY
11 | });
12 |
13 | // A file name for an image
14 | const IMAGE_NAME = "image.jpeg";
15 |
16 | // Start new form
17 | const form = new FormData();
18 |
19 | // Send information in 'content' field as JSON
20 | form.append(
21 | "content",
22 | JSON.stringify({
23 | mediaTypeAlias: "Image",
24 | // Media Folder UUID (the long one)
25 | parentId: "click-on-folder-and-then-info",
26 | name: "File name",
27 | umbracoFile: { src: IMAGE_NAME }
28 | })
29 | );
30 |
31 | // Note: By default, the alias for an upload field will be 'umbracoFile', which is
32 | // why the file stream is attached to this field. The management api expects the file to be a stream.
33 | // The client library will send the payload as 'multipart/form-data'.
34 |
35 | // Send file in 'umbracoFile' field
36 | form.append(
37 | "umbracoFile",
38 | fs.createReadStream(path.join(__dirname, IMAGE_NAME))
39 | );
40 |
41 | console.log(`Sending ${IMAGE_NAME} to Umbraco Media`);
42 |
43 | client.management.media
44 | .create(form)
45 | .then(res => {
46 | console.log("Umbraco Media response", res);
47 | })
48 | .catch(err => {
49 | const {
50 | jsonData: {
51 | error: { details }
52 | }
53 | } = err;
54 | console.log("Umbraco Media Error", details || err);
55 | });
56 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@umbraco/headless-client",
3 | "version": "0.9.0",
4 | "description": "Node.js client library for the Umbraco Headless APIs",
5 | "license": "MIT",
6 | "repository": {
7 | "type": "git",
8 | "url": "git+https://github.com/umbraco/Umbraco.Headless.Client.NodeJs.git"
9 | },
10 | "keywords": [
11 | "umbraco",
12 | "cms",
13 | "headless",
14 | "heartcore",
15 | "cloud"
16 | ],
17 | "author": "Umbraco (https://www.umbraco.com)",
18 | "bugs": {
19 | "url": "https://github.com/umbraco/Umbraco.Headless.Client.NodeJs/issues"
20 | },
21 | "homepage": "https://github.com/umbraco/Umbraco.Headless.Client.NodeJs#readme",
22 | "main": "index.js",
23 | "types": "headless-client.d.ts",
24 | "engines": {
25 | "node": ">=10.x"
26 | },
27 | "scripts": {
28 | "build": "tsc && cpy package.json .github/README.md dist",
29 | "docs": "api-extractor run --local && api-documenter markdown -i dist -o docs/api/",
30 | "start": "tsc -w",
31 | "test": "mocha -r ts-node/register 'src/**/*.spec.ts'",
32 | "lint": "eslint src --ext .js,.ts"
33 | },
34 | "dependencies": {
35 | "axios": "^0.21.1",
36 | "form-data": "^2.5.1"
37 | },
38 | "devDependencies": {
39 | "@microsoft/api-documenter": "^7.8.45",
40 | "@microsoft/api-extractor": "^7.9.14",
41 | "@types/chai": "^4.2.12",
42 | "@types/form-data": "^2.5.0",
43 | "@types/mocha": "^5.2.7",
44 | "@types/node": "^12.12.57",
45 | "@types/sinon": "^7.5.2",
46 | "@typescript-eslint/eslint-plugin": "^2.34.0",
47 | "@typescript-eslint/parser": "^2.34.0",
48 | "axios-mock-adapter": "^1.18.2",
49 | "chai": "^4.2.0",
50 | "cpy-cli": "^3.1.1",
51 | "eslint": "^6.8.0",
52 | "eslint-config-standard-with-typescript": "^11.0.1",
53 | "eslint-plugin-import": "^2.22.0",
54 | "eslint-plugin-mocha": "^6.3.0",
55 | "eslint-plugin-node": "^9.2.0",
56 | "eslint-plugin-promise": "^4.2.1",
57 | "eslint-plugin-standard": "^4.0.1",
58 | "mocha": "^7.2.0",
59 | "sinon": "^8.1.1",
60 | "ts-node": "^8.10.2",
61 | "typescript": "^3.9.10"
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/samples/gridsome/.env:
--------------------------------------------------------------------------------
1 | UMBRACO__PROJECTALIAS=
2 | UMBRACO__APIKEY=
3 |
--------------------------------------------------------------------------------
/samples/gridsome/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 | .cache
3 | .DS_Store
4 | src/.temp
5 | node_modules
6 | dist
7 | .env.*
8 |
--------------------------------------------------------------------------------
/samples/gridsome/README.md:
--------------------------------------------------------------------------------
1 | # Umbraco Heartcore Gridsome sample
2 |
3 | Gridsome sample site for Umbraco Heartcore.
4 |
5 | ## Features
6 |
7 | - [Node.js](https://nodejs.org/en/)
8 | - [Typescript](https://www.typescriptlang.org/)
9 | - [Gridsome](https://gridsome.org/)
10 |
11 | ## Prerequisites
12 |
13 | To run this sample you will need the following tools installed
14 |
15 | - [Node.js](https://nodejs.org/en/) 10 or newer
16 |
17 | ## Getting Started
18 |
19 | Before running the application, you need to copy `.env` to `.env.development` or `.env.production` and update it with your Umbraco Heartcore
20 | project alias (the project alias can be found in the [Umbraco Cloud Portal](https://www.s1.umbraco.io)) and an API Key if the Content Delivery API is protected.
21 |
22 | ```env
23 | UMBRACO__PROJECTALIAS=
24 | UMBRACO__APIKEY=
25 | ```
26 |
27 | In order to use the sample you will need an Umbraco Heartcore project with content, media and document types that correspond to those setup in the views and templates of the sample website. You can use `demo-headless` as the project alias to get started with the sample. The Project behind this alias has been used as the source of the sample, so its a good place to start.
28 |
29 | The `ApiKey` is not used in this sample and can thus be left blank. If you chose to protect the content exposed via the Content Delivery API then you will need an API-Key, but its an option that has to be actively turned on (or off - its off by default) via the Umbraco Backoffice in the Headless tree in the Settings section.
30 |
31 | ### Installation
32 |
33 | To install dependencies, run the following command
34 |
35 | ```bash
36 | > npm install
37 | ```
38 |
39 | ### Usage
40 |
41 | Run the following command to start the site
42 |
43 | ```bash
44 | > npm run develop
45 | ```
46 |
47 | This will start a dev webserver listening on `http://localhost:8081`
48 |
49 | ### Generate a static website
50 |
51 | To generate a static version of the website run the following command
52 |
53 | ```bash
54 | > npm run build
55 | ```
56 |
57 | This will generate the a static website in the `dist` directory.
58 |
--------------------------------------------------------------------------------
/samples/gridsome/gridsome.config.js:
--------------------------------------------------------------------------------
1 | // This is where project configuration and plugin options are located.
2 | // Learn more: https://gridsome.org/docs/config
3 |
4 | // Changes here require a server restart.
5 | // To restart press CTRL + C in terminal and run `gridsome develop`
6 |
7 | module.exports = {
8 | siteName: 'Gridsome',
9 | plugins: [{
10 | use: 'gridsome-plugin-typescript'
11 | }]
12 | }
13 |
--------------------------------------------------------------------------------
/samples/gridsome/gridsome.server.js:
--------------------------------------------------------------------------------
1 | const { Client } = require('@umbraco/headless-client')
2 |
3 | // Server API makes it possible to hook into various parts of Gridsome
4 | // on server-side and add custom data to the GraphQL data layer.
5 | // Learn more: https://gridsome.org/docs/server-api/
6 |
7 | // Changes here require a server restart.
8 | // To restart press CTRL + C in terminal and run `gridsome develop`
9 |
10 | const client = new Client({
11 | projectAlias: process.env.UMBRACO__PROJECTALIAS,
12 | apiKey: process.env.UMBRACO__APIKEY,
13 | })
14 |
15 | module.exports = function (api) {
16 | api.loadSource(async ({ addCollection, addMetadata, getCollection }) => {
17 |
18 | const contentCollection = addCollection({ typeName: 'Content' })
19 |
20 | const rootContents = await client.delivery.content.root()
21 | if (rootContents.length) {
22 | const root = rootContents[0]
23 | root._url = '/'
24 |
25 | addMetadata('footerLinks', root.footerLinks)
26 | addMetadata('footerTitle', root.footerTitle)
27 | }
28 |
29 | const addItem = async function(item) {
30 | const name = item.contentTypeAlias[0].toUpperCase() + item.contentTypeAlias.substr(1)
31 | const collection = getCollection(name) || addCollection({ typeName: name })
32 |
33 | collection.addNode({ id: item._id, ...item })
34 | contentCollection.addNode({ id: item._id, ...item })
35 |
36 | const children = await client.delivery.content.children(item._id)
37 | if (children.items) {
38 | for (const child of children.items) {
39 | await addItem(child)
40 | }
41 | }
42 | }
43 |
44 | for (const item of rootContents) {
45 | await addItem(item)
46 | }
47 |
48 | // Use the Data Store API here: https://gridsome.org/docs/data-store-api/
49 | })
50 |
51 | api.createPages(async ({ graphql, createPage }) => {
52 | // Use the Pages API here: https://gridsome.org/docs/pages-api/
53 | const { data } = await graphql(`
54 | {
55 | allTextpage {
56 | edges {
57 | node {
58 | id
59 | _url
60 | }
61 | }
62 | }
63 | allFrontpage {
64 | edges {
65 | node {
66 | id
67 | _url
68 | }
69 | }
70 | }
71 | }
72 | `)
73 |
74 | for (const { node } of data.allFrontpage.edges) {
75 | createPage({
76 | path: node._url,
77 | component: './src/templates/Frontpage.vue',
78 | context: {
79 | id: node.id,
80 | url: node._url,
81 | },
82 | })
83 | }
84 |
85 | for (const { node } of data.allTextpage.edges) {
86 | createPage({
87 | path: node._url,
88 | component: './src/templates/Textpage.vue',
89 | context: {
90 | id: node.id,
91 | url: node._url,
92 | },
93 | })
94 | }
95 | })
96 | }
97 |
--------------------------------------------------------------------------------
/samples/gridsome/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gridsome-sample",
3 | "private": true,
4 | "scripts": {
5 | "build": "gridsome build",
6 | "develop": "gridsome develop",
7 | "explore": "gridsome explore"
8 | },
9 | "dependencies": {
10 | "@umbraco/headless-client": "../../dist",
11 | "gridsome": "^0.7.20"
12 | },
13 | "devDependencies": {
14 | "gridsome-plugin-typescript": "^0.4.0",
15 | "ts-loader": "^6.2.2",
16 | "typescript": "^3.9.7",
17 | "vue-fragment": "^1.5.1"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/samples/gridsome/src/components/Elements.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
22 |
--------------------------------------------------------------------------------
/samples/gridsome/src/components/Hero.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ title }}
4 | {{ subtitle }}
5 |
6 |
7 |
8 |
21 |
22 |
55 |
--------------------------------------------------------------------------------
/samples/gridsome/src/components/MainNavigation.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 | query {
13 | mainNavigationItems:allContent(filter: { umbracoNaviHide: { eq: false }, _level: { eq: 2 }}, sort: [{ by: "sortOrder", order: ASC }]) {
14 | edges {
15 | node {
16 | id
17 | title:name
18 | url:_url
19 | }
20 | }
21 | }
22 | }
23 |
24 |
25 |
31 |
32 |
75 |
--------------------------------------------------------------------------------
/samples/gridsome/src/components/PageFooter.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
14 | query {
15 | metadata {
16 | links:footerLinks {
17 | name
18 | type
19 | url
20 | }
21 | title:footerTitle
22 | }
23 | }
24 |
25 |
26 |
35 |
36 |
96 |
--------------------------------------------------------------------------------
/samples/gridsome/src/components/PageHeader.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
21 |
22 |
63 |
--------------------------------------------------------------------------------
/samples/gridsome/src/components/README.md:
--------------------------------------------------------------------------------
1 | Add components that will be imported to Pages and Layouts to this folder.
2 | Learn more about components here: https://gridsome.org/docs/components/
3 |
4 | You can delete this file.
5 |
--------------------------------------------------------------------------------
/samples/gridsome/src/components/UniqueSellingPoints.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ title }}
4 |
13 |
14 |
15 |
16 |
27 |
28 |
83 |
--------------------------------------------------------------------------------
/samples/gridsome/src/components/elements/TextAndImage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ title }}
4 |
5 |
15 |
25 |
26 |
27 |
28 |
42 |
43 |
102 |
--------------------------------------------------------------------------------
/samples/gridsome/src/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umbraco/Umbraco.Headless.Client.NodeJs/8b5cf81884509dcd018e9260181a0abe21136946/samples/gridsome/src/favicon.png
--------------------------------------------------------------------------------
/samples/gridsome/src/layouts/Default.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | query {
13 | metadata {
14 | siteName
15 | }
16 | }
17 |
18 |
19 |
32 |
33 |
91 |
--------------------------------------------------------------------------------
/samples/gridsome/src/layouts/README.md:
--------------------------------------------------------------------------------
1 | Layout components are used to wrap pages and templates. Layouts should contain components like headers, footers or sidebars that will be used across the site.
2 |
3 | Learn more about Layouts: https://gridsome.org/docs/layouts/
4 |
5 | You can delete this file.
6 |
--------------------------------------------------------------------------------
/samples/gridsome/src/main.js:
--------------------------------------------------------------------------------
1 | // This is the main.js file. Import global CSS and scripts here.
2 | // The Client API can be used here. Learn more: gridsome.org/docs/client-api
3 |
4 | import DefaultLayout from '~/layouts/Default.vue'
5 | import Fragment from 'vue-fragment'
6 |
7 | export default function (Vue, { router, head, isClient }) {
8 | // Set default layout as a global component
9 | Vue.use(Fragment.Plugin)
10 | Vue.component('Layout', DefaultLayout)
11 | }
12 |
--------------------------------------------------------------------------------
/samples/gridsome/src/pages/README.md:
--------------------------------------------------------------------------------
1 | Pages are usually used for normal pages or for listing items from a GraphQL collection.
2 | Add .vue files here to create pages. For example **About.vue** will be **site.com/about**.
3 | Learn more about pages: https://gridsome.org/docs/pages/
4 |
5 | You can delete this file.
6 |
--------------------------------------------------------------------------------
/samples/gridsome/src/templates/Frontpage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | query($id: ID!) {
11 | frontpage(id: $id) {
12 | name
13 | heroTitle
14 | heroSubtitle
15 | heroImage {
16 | _url
17 | }
18 | uniqueSellingPointsTitle
19 | uniqueSellingPoints {
20 | title
21 | text
22 | image {
23 | _url
24 | }
25 | link {
26 | url
27 | #target
28 | name
29 | }
30 | }
31 | elements {
32 | contentTypeAlias
33 | title
34 | text
35 | image {
36 | _url
37 | }
38 | showLargeImage
39 | }
40 | }
41 | }
42 |
43 |
44 |
60 |
--------------------------------------------------------------------------------
/samples/gridsome/src/templates/README.md:
--------------------------------------------------------------------------------
1 | Templates for **GraphQL collections** should be added here.
2 | To create a template for a collection called `WordPressPost`
3 | create a file named `WordPressPost.vue` in this folder.
4 |
5 | Learn more: https://gridsome.org/docs/templates/
6 |
7 | You can delete this file.
8 |
--------------------------------------------------------------------------------
/samples/gridsome/src/templates/Textpage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | query($id: ID!) {
10 | textpage(id: $id) {
11 | name
12 | heroTitle
13 | heroSubtitle
14 | heroImage {
15 | _url
16 | }
17 | elements {
18 | contentTypeAlias
19 | title
20 | text
21 | image {
22 | _url
23 | }
24 | showLargeImage
25 | }
26 | }
27 | }
28 |
29 |
30 |
44 |
--------------------------------------------------------------------------------
/samples/gridsome/src/types.ts:
--------------------------------------------------------------------------------
1 | import { Content } from '@umbraco/headless-client'
2 |
3 | export type Link = {
4 | url: string
5 | target?: string
6 | name: string
7 | }
8 |
9 | export type Image = {
10 | _url: string
11 | }
12 |
13 | export type Hero = {
14 | image?: Image,
15 | title: string,
16 | subtitle?: string
17 | }
18 |
19 | export type UniqueSellingPoint = {
20 | image?: Image
21 | link?: Link
22 | text: string
23 | title?: string
24 | }
25 |
26 | export type HideInNavigation = {
27 | umbNaviHide: boolean
28 | }
29 |
30 | export type Element = {
31 | contentTypeAlias: string
32 | }
33 |
34 | export type TextAndImage = Element & {
35 | title: string
36 | text: string
37 | image?: Image
38 | showLargeImage: boolean
39 | }
40 |
41 | export type Elements = TextAndImage
42 |
43 | export type Frontpage = Content & HideInNavigation & Hero & UniqueSellingPoint & Elements[]
44 | export type Textpage = Content & HideInNavigation & Hero & Elements[]
45 |
--------------------------------------------------------------------------------
/samples/gridsome/src/vue-shims.d.ts:
--------------------------------------------------------------------------------
1 | declare module "*.vue" {
2 | import Vue from "vue";
3 | export default Vue;
4 | }
5 |
--------------------------------------------------------------------------------
/samples/gridsome/static/README.md:
--------------------------------------------------------------------------------
1 | Add static files here. Files in this directory will be copied directly to `dist` folder during build. For example, /static/robots.txt will be located at https://yoursite.com/robots.txt.
2 |
3 | This file should be deleted.
--------------------------------------------------------------------------------
/samples/gridsome/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "module": "es2015",
5 | "moduleResolution": "node",
6 | "noImplicitReturns": true,
7 | "outDir": "./built/",
8 | "sourceMap": true,
9 | "strict": true
10 | },
11 | "include": [
12 | "./src/**/*"
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/samples/koa/README.md:
--------------------------------------------------------------------------------
1 | # Umbraco Headless Node.js Koa sample
2 |
3 | Node.js Koa sample site for Umbraco Headless.
4 |
5 | ## Features
6 |
7 | - [Node.js](https://nodejs.org/en/)
8 | - [Typescript](https://www.typescriptlang.org/)
9 | - [Koa](https://koajs.com/)
10 | - [koa-logger](https://github.com/koajs/logger)
11 | - [koa-static](https://github.com/koajs/static)
12 | - [koa-views](https://github.com/queckezz/koa-views)
13 | - [Nunjucks](https://mozilla.github.io/nunjucks/)
14 |
15 | ## Prerequisites
16 |
17 | To run this sample you will need the following tools installed
18 |
19 | - [Node.js](https://nodejs.org/en/) 10 or newer
20 |
21 | ## Getting Started
22 |
23 | Before running the application, `package.json` needs to be updated with your Umbraco Headless
24 | project alias (the project alias can be found in the [Umbraco Cloud Portal](https://www.s1.umbraco.io)). If the Content Delivery API is protected the `apiKey` also needs to be updated.
25 |
26 | ```json
27 | {
28 | "umbraco": {
29 | "projectAlias": "",
30 | "apiKey": ""
31 | }
32 | }
33 | ```
34 |
35 | In order to use the sample you will need an Umbraco Headless project with content, media and document types that correspond to those setup in the views and templates of the sample website. You can use `demo-headless` as the project alias to get started with the sample. The Project behind this alias has been used as the source of the sample, so its a good place to start.
36 |
37 | The `ApiKey` is not used in this sample and can thus be left blank. If you chose to protect the content exposed via the Content Delivery API then you will need an API-Key, but its an option that has to be actively turned on (or off - its off by default) via the Umbraco Backoffice in the Headless tree in the Settings section.
38 |
39 | ### Installation
40 |
41 | To install dependencies, run the following command
42 |
43 | ```bash
44 | > npm install
45 | ```
46 |
47 | ### Usage
48 |
49 | Run the following command to start the site
50 |
51 | ```bash
52 | > npm start
53 | ```
54 |
55 | This will start the Koa webserver listening on `http://localhost:3000`
56 |
--------------------------------------------------------------------------------
/samples/koa/app.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 |
3 | import views from 'koa-views';
4 | import logger from 'koa-logger';
5 | import serve from "koa-static";
6 | import Koa from 'koa';
7 | import { Client } from "@umbraco/headless-client";
8 |
9 | const app = new Koa();
10 | const client = new Client({
11 | projectAlias: process.env.UMBRACO__PROJECTALIAS || require('./package.json').umbraco.projectAlias,
12 | apiKey: process.env.UMBRACO__APIKEY || require('./package.json').umbraco.apiKey
13 | })
14 |
15 | const getByUrl = async (cache: Object, path: string) => {
16 | const content = cache[path];
17 | if (content)
18 | return content;
19 |
20 | return cache[path] = await client.delivery.content.byUrl(path);
21 | }
22 |
23 | const getNavigationItems = async (ctx: Koa.ParameterizedContext, cache: Object) => {
24 | const root = await getByUrl(cache, '/');
25 | const children = await client.delivery.content.children(root._id);
26 |
27 | return (children.items as any)
28 | .filter(item => !item.umbNaviHide)
29 | .map(item => {
30 | return {
31 | title: item.name,
32 | url: item._url,
33 | isCurrent: item._url === ctx.path
34 | }
35 | });
36 | }
37 |
38 | const getFooter = async (ctx: Koa.ParameterizedContext, cache: Object) => {
39 | const root = await getByUrl(cache, '/');
40 |
41 | return {
42 | title: root.footerTitle,
43 | links: root.footerLinks
44 | };
45 | }
46 |
47 | // middleware
48 | app.use(logger());
49 | app.use(serve(path.join(__dirname, 'public')));
50 |
51 | app.use(views(path.join(__dirname, 'views'), {
52 | extension: 'njk',
53 | map: { njk: 'nunjucks' },
54 | options: { settings: { views: path.join(__dirname, 'views') } }
55 | }));
56 |
57 | // route definitions
58 | app.use(async (ctx: Koa.ParameterizedContext) => {
59 | try {
60 | const cache = {};
61 | const content = await getByUrl(cache, ctx.path);
62 | const navigationItems = await getNavigationItems(ctx, cache);
63 | const footer = await getFooter(ctx, cache);
64 |
65 | await ctx.render(content.contentTypeAlias, { content, mainNavigation: navigationItems, footer });
66 | } catch (err) {
67 | if (err.jsonData && err.jsonData.error.code === 'NotFound') {
68 | ctx.status = 404;
69 | return;
70 | }
71 | throw err;
72 | }
73 | });
74 |
75 | // listen
76 | const port = process.env.port || 3000;
77 | app.listen(port, () => console.log(`App listening on http://localhost:${port}`));
78 |
--------------------------------------------------------------------------------
/samples/koa/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "umbracos-headless-koa-sample",
3 | "version": "0.1.0",
4 | "description": "Umbraco Headless Koa sample",
5 | "private": true,
6 | "scripts": {
7 | "start": "ts-node-dev --respawn app.ts"
8 | },
9 | "author": "",
10 | "license": "MIT",
11 | "umbraco": {
12 | "projectAlias": "",
13 | "apiKey": ""
14 | },
15 | "dependencies": {
16 | "@umbraco/headless-client": "../../dist",
17 | "koa": "^2.11.0",
18 | "koa-logger": "^3.2.1",
19 | "koa-static": "^5.0.0",
20 | "koa-views": "^6.2.1",
21 | "nunjucks": "^3.2.1"
22 | },
23 | "devDependencies": {
24 | "@types/koa": "^2.0.51",
25 | "@types/koa-logger": "^3.1.1",
26 | "@types/koa-router": "^7.0.42",
27 | "@types/koa-static": "^4.0.1",
28 | "@types/koa-views": "^2.0.3",
29 | "ts-node-dev": "^1.0.0-pre.44",
30 | "typescript": "^3.7.2"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/samples/koa/public/css/site.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --content-width: 1360px;
3 | --grey: #F9F7F4;
4 | }
5 |
6 | *, ::after, ::before {
7 | box-sizing: border-box;
8 | }
9 |
10 | html {
11 | font-family: Lato, Arial, Helvetica, sans-serif;
12 | font-size: 18px;
13 | }
14 |
15 | body {
16 | padding: 0;
17 | margin: 0;
18 | }
19 |
20 | img {
21 | max-width: 100%;
22 | height: auto;
23 | }
24 |
25 | h1, h2, h3, h4, h5 {
26 | font-weight: 700;
27 | line-height: 1.3;
28 | margin: 0 0 20px;
29 | }
30 |
31 | ul {
32 | margin: 0;
33 | }
34 |
35 | a {
36 | color: #3544b1;
37 | text-decoration: none;
38 | }
39 |
40 | a:hover {
41 | text-decoration: underline;
42 | }
43 |
44 | /* Header */
45 |
46 | .page-header {
47 | background: white;
48 | position: absolute;
49 | max-width: var(--content-width);
50 | width: 100%;
51 | top: 0;
52 | left: 0;
53 | right: 0;
54 | margin: 0 auto;
55 | }
56 |
57 | .page-header__brand {
58 | display: inline-flex;
59 | padding: 5px 20px;
60 | color: #162335;
61 | }
62 |
63 | .page-header__brand:hover {
64 | text-decoration: none;
65 | }
66 |
67 | .page-header__brand-graphics {
68 | height: 40px;
69 | width: 115px;
70 | fill: #162335;
71 | }
72 |
73 | .page-header__nav-container {
74 | position: absolute;
75 | top: 0;
76 | right: 0;
77 | margin: 0;
78 | padding: 0 20px;
79 | }
80 |
81 | @media (min-width: 992px) {
82 | .page-header {
83 | background: transparent;
84 | top: 20px;
85 | }
86 | .page-header__brand {
87 | color: white;
88 | }
89 | .page-header__brand-graphics {
90 | fill: white;
91 | }
92 | .page-header__nav-container {
93 | top: 15px;
94 | }
95 | }
96 |
97 | /* Navigation */
98 |
99 | .nav {
100 | display: none;
101 | list-style: none;
102 | margin: 0;
103 | padding: 0;
104 | }
105 |
106 | .nav__item {
107 | display: inline;
108 | padding: 5px;
109 | }
110 |
111 | .nav__item-link {
112 | color: white;
113 | }
114 |
115 | .nav__item-link--current {
116 | text-decoration: underline;
117 | }
118 |
119 | .nav__item-link--current:hover {
120 | text-decoration-thickness: 3px;
121 | }
122 |
123 | @media (min-width: 992px) {
124 | .nav {
125 | display: block;
126 | }
127 | }
128 | /* Main */
129 |
130 | .main-content,
131 | .usp-container,
132 | .page-footer-container {
133 | display: grid;
134 | grid-template-columns: [full-start] 1fr [content-start] minmax(320px, var(--content-width))
135 | [content-end] 1fr [full-end];
136 | }
137 |
138 | /* Hero */
139 |
140 | .hero {
141 | grid-column: full-start / full-end;
142 | padding: 50px 5px 0 5px;
143 | min-height: 230px;
144 | background-position-x: 50%;
145 | background-repeat: no-repeat;
146 | background-size: cover;
147 | color: white;
148 | text-align: center;
149 | display: grid;
150 | align-content: center;
151 | }
152 |
153 | .hero__title {
154 | font-size: 2em;
155 | font-weight: 900;
156 | }
157 |
158 | .hero__subtitle {
159 | font-size: 1.1em;
160 | font-weight: 400;
161 | }
162 |
163 | @media (min-width: 992px) {
164 | .hero {
165 | height: 430px;
166 | }
167 | .hero__title {
168 | font-size: 4.2em;
169 | }
170 | }
171 |
172 | /* USP */
173 |
174 | .usp-container {
175 | background: var(--grey);
176 | padding-top: 50px;
177 | grid-column: full-start / full-end;
178 | }
179 |
180 | .usp-container__title {
181 | grid-column: content-start / content-end;
182 | font-size: 2.4em;
183 | text-align: center;
184 | }
185 |
186 | .usp-item-container {
187 | grid-column: content-start / content-end;
188 | display: grid;
189 | }
190 |
191 | .usp {
192 | display: grid;
193 | text-align: center;
194 | margin-bottom: 25px;
195 | padding: 0 20px;
196 | grid-template-rows: 120px auto auto auto;
197 | }
198 |
199 | .usp__image {
200 | grid-row: 1 / 1;
201 | margin: 0 auto 20px auto;
202 | }
203 |
204 | .usp__title {
205 | font-size: 1.5em;
206 | margin: 0 0 20px;
207 | }
208 |
209 | .usp__text {
210 | margin: 0 0 10px;
211 | }
212 |
213 | .usp__link {
214 | padding: 10px 20px;
215 | }
216 |
217 | @media (min-width: 992px) {
218 | .usp-item-container {
219 | grid-template-columns: 1fr 1fr 1fr;
220 | }
221 | }
222 |
223 | /* Text and Image */
224 |
225 | .text-and-image {
226 | margin: 20px 20px 0 20px;
227 | grid-column: content-start / content-end;
228 | display: grid;
229 | grid-template-rows: auto 1fr auto;
230 | }
231 |
232 | .text-and-image__title,
233 | .text-and-image__text {
234 | grid-column: 1 / 1;
235 | }
236 |
237 | .text-and-image__title {
238 | font-size: 1.5em;
239 | }
240 |
241 | .text-and-image__image {
242 | margin-bottom: 20px;
243 | grid-row: 2 / 2;
244 | }
245 |
246 | .text-and-image--image-left .text-and-image__title,
247 | .text-and-image--image-left .text-and-image__text {
248 | grid-column: 2 / -1;
249 | }
250 |
251 | .text-and-image--image-left .text-and-image__image {
252 | grid-column: 1;
253 | }
254 |
255 | @media (min-width: 992px) {
256 | .text-and-image {
257 | margin: 40px;
258 | grid-template-columns: 2fr 1fr;
259 | }
260 |
261 | .text-and-image__title {
262 | font-size: 2.8em;
263 | }
264 |
265 | .text-and-image__title,
266 | .text-and-image__text {
267 | padding: 0 30px 0 0;
268 | }
269 |
270 | .text-and-image__image {
271 | margin: 0 auto;
272 | align-self: center;
273 | grid-column: 2 / -1;
274 | grid-row: 1 / -1;
275 | }
276 |
277 | .text-and-image--image-left .text-and-image__title,
278 | .text-and-image--image-left .text-and-image__text {
279 | padding: 0 0 0 30px;
280 | }
281 | }
282 |
283 | /* Footer */
284 |
285 | .page-footer-container {
286 | grid-column: full-start / full-end;
287 | background: var(--grey);
288 | }
289 |
290 | .page-footer {
291 | margin: 50px 20px;
292 | color: #101f3c;
293 | grid-column: content-start / content-end;
294 | display: grid;
295 | grid-template-rows: auto 1fr;
296 | }
297 |
298 | .page-footer__title {
299 | font-size: 1.2em;
300 | }
301 |
302 | .page-footer__links {
303 | list-style-type: none;
304 | margin: 0 0 40px;
305 | padding: 0;
306 | }
307 |
308 | .page-footer__link,
309 | .page-footer__link:focus,
310 | .page-footer__link:visited {
311 | color: #8A8A8A;
312 | line-height: 2;
313 | }
314 |
315 | .page-footer__link:hover {
316 | color: #333;
317 | text-decoration: none;
318 | }
319 |
320 | .page-footer__graphics {
321 | height: 150px;
322 | widows: 250px;
323 | transform: translateX(-27%);
324 | }
325 |
326 | @media (min-width: 992px) {
327 | .page-footer {
328 | margin: 70px 20px 100px;
329 | }
330 |
331 | .page-footer__graphics {
332 | transform: none;
333 | align-self: center;
334 | margin: 0 auto;
335 | }
336 | }
337 |
--------------------------------------------------------------------------------
/samples/koa/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umbraco/Umbraco.Headless.Client.NodeJs/8b5cf81884509dcd018e9260181a0abe21136946/samples/koa/public/favicon.ico
--------------------------------------------------------------------------------
/samples/koa/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "target": "es5",
5 | "esModuleInterop": true,
6 | "lib": ["es2015"]
7 | },
8 | "exclude": [
9 | "node_modules"
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/samples/koa/views/_layout.njk:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{ content.name }}
7 |
8 |
9 |
10 |
16 |
17 | {% block content %}{% endblock %}
18 |
19 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/samples/koa/views/frontpage.njk:
--------------------------------------------------------------------------------
1 | {% extends "_layout.njk" %}
2 |
3 | {% block content %}
4 | {% include 'partials/_hero.njk' %}
5 | {% include 'partials/_uniqueSellingPoints.njk' %}
6 | {% include 'partials/_elements.njk' %}
7 | {% endblock %}
8 |
--------------------------------------------------------------------------------
/samples/koa/views/partials/_elements.njk:
--------------------------------------------------------------------------------
1 | {% for element in content.elements %}
2 | {% include 'partials/elements/_' + element.contentTypeAlias + '.njk' %}
3 | {% endfor %}
4 |
--------------------------------------------------------------------------------
/samples/koa/views/partials/_footer.njk:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/samples/koa/views/partials/_hero.njk:
--------------------------------------------------------------------------------
1 |
2 | {{ content.heroTitle }}
3 | {% if content.heroSubtitle %}
4 | {{ content.heroSubtitle }}
5 | {% endif %}
6 |
7 |
--------------------------------------------------------------------------------
/samples/koa/views/partials/_mainNavigation.njk:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/samples/koa/views/partials/_uniqueSellingPoints.njk:
--------------------------------------------------------------------------------
1 | {% if cotent.uniqueSellingPointsTitle or content.uniqueSellingPoints %}
2 |
3 | {% if content.uniqueSellingPointsTitle %}
4 | {{ content.uniqueSellingPointsTitle }}
5 | {% endif %}
6 | {% if content.uniqueSellingPoints %}
7 |
8 | {% for usp in content.uniqueSellingPoints %}
9 |
10 | {{ usp.title }}
11 | {% if usp.image %}
12 |
13 | {% endif %}
14 | {{ usp.text }}
15 | {% if usp.link %}
16 | {{ usp.link.name }}
17 | {% endif %}
18 |
19 | {% endfor %}
20 |
21 | {% endif %}
22 |
23 | {% endif %}
24 |
--------------------------------------------------------------------------------
/samples/koa/views/partials/elements/_textAndImage.njk:
--------------------------------------------------------------------------------
1 |
2 | {{ element.title }}
3 | {{ element.text | safe }}
4 | {% if element.showLargeImage and element.image %}
5 |
15 | {% elif element.image %}
16 |
26 | {% endif %}
27 |
28 |
29 |
--------------------------------------------------------------------------------
/samples/koa/views/textpage.njk:
--------------------------------------------------------------------------------
1 | {% extends "_layout.njk" %}
2 |
3 | {% block content %}
4 | {% include 'partials/_hero.njk' %}
5 | {% include 'partials/_elements.njk' %}
6 | {% endblock %}
7 |
--------------------------------------------------------------------------------
/samples/vue/.browserslistrc:
--------------------------------------------------------------------------------
1 | > 1%
2 | last 2 versions
3 |
--------------------------------------------------------------------------------
/samples/vue/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.{js,jsx,ts,tsx,vue}]
2 | indent_style = space
3 | indent_size = 2
4 | trim_trailing_whitespace = true
5 | insert_final_newline = true
6 |
--------------------------------------------------------------------------------
/samples/vue/.env:
--------------------------------------------------------------------------------
1 | VUE_APP_UMBRACO__PROJECTALIAS=
2 |
--------------------------------------------------------------------------------
/samples/vue/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | node: true
5 | },
6 | extends: [
7 | 'plugin:vue/essential',
8 | '@vue/standard',
9 | '@vue/typescript'
10 | ],
11 | rules: {
12 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
13 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
14 | 'eol-last': ['error', 'always'],
15 | 'no-unused-vars': 0
16 | },
17 | parserOptions: {
18 | parser: '@typescript-eslint/parser'
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/samples/vue/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 | # local env files
6 | .env.local
7 | .env.*.local
8 |
9 | # Log files
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 |
14 | # Editor directories and files
15 | .idea
16 | .vscode
17 | *.suo
18 | *.ntvs*
19 | *.njsproj
20 | *.sln
21 | *.sw?
22 |
--------------------------------------------------------------------------------
/samples/vue/README.md:
--------------------------------------------------------------------------------
1 | # Umbraco Heartcore Node.js Vue sample
2 |
3 | Node.js Vue sample site for Umbraco Heartcore
4 |
5 | ## Features
6 |
7 | - [Node.js](https://nodejs.org/en/)
8 | - [Typescript](https://www.typescriptlang.org/)
9 | - [Vue CLI](https://cli.vuejs.org/)
10 |
11 | ## Prerequisites
12 |
13 | To run this sample you will need the following tools installed
14 |
15 | - [Node.js](https://nodejs.org/en/) 10 or newer
16 |
17 | ## Getting Started
18 |
19 | Before running the application, you need to copy `.env` to `.env.local` and update it with your Umbraco Heartcore
20 | project alias (the project alias can be found in the [Umbraco Cloud Portal](https://www.s1.umbraco.io)).
21 |
22 | ```env
23 | VUE_APP_UMBRACO__PROJECTALIAS=
24 | ```
25 |
26 | In order to use the sample you will need an Umbraco Hearctore project with content, media and document types that correspond to those setup in the views and templates of the sample website. You can use `demo-headless` as the project alias to get started with the sample. The Project behind this alias has been used as the source of the sample, so its a good place to start.
27 |
28 | ### Installation
29 |
30 | To install dependencies, run the following command
31 |
32 | ```bash
33 | > npm install
34 | ```
35 |
36 | ### Usage
37 |
38 | Run the following command to start the site
39 |
40 | ```bash
41 | > npm run serve
42 | ```
43 |
44 | This will start a dev webserver listening on `http://localhost:8081`
45 |
--------------------------------------------------------------------------------
/samples/vue/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ['@vue/cli-plugin-babel/preset']
3 | }
4 |
--------------------------------------------------------------------------------
/samples/vue/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "serve": "vue-cli-service serve",
7 | "build": "vue-cli-service build",
8 | "lint": "vue-cli-service lint"
9 | },
10 | "dependencies": {
11 | "@umbraco/headless-client": "../../dist",
12 | "core-js": "^3.6.5",
13 | "vue": "^2.6.10",
14 | "vue-fragment": "^1.5.1",
15 | "vue-router": "^3.3.2"
16 | },
17 | "devDependencies": {
18 | "@typescript-eslint/eslint-plugin": "^2.34.0",
19 | "@typescript-eslint/parser": "^2.34.0",
20 | "@vue/cli-plugin-babel": "^4.2.3",
21 | "@vue/cli-plugin-eslint": "^4.2.3",
22 | "@vue/cli-plugin-router": "^4.2.3",
23 | "@vue/cli-plugin-typescript": "^4.4.1",
24 | "@vue/cli-service": "^4.4.1",
25 | "@vue/eslint-config-standard": "^5.1.2",
26 | "@vue/eslint-config-typescript": "^5.0.2",
27 | "eslint": "^6.8.0",
28 | "eslint-plugin-import": "^2.21.1",
29 | "eslint-plugin-node": "^11.1.0",
30 | "eslint-plugin-promise": "^4.2.1",
31 | "eslint-plugin-standard": "^4.0.1",
32 | "eslint-plugin-vue": "^6.2.2",
33 | "typescript": "~3.7.5",
34 | "vue-template-compiler": "^2.6.10"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/samples/vue/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umbraco/Umbraco.Headless.Client.NodeJs/8b5cf81884509dcd018e9260181a0abe21136946/samples/vue/public/favicon.ico
--------------------------------------------------------------------------------
/samples/vue/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/samples/vue/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
51 |
52 |
110 |
--------------------------------------------------------------------------------
/samples/vue/src/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/samples/vue/src/client.ts:
--------------------------------------------------------------------------------
1 | import { Client } from '@umbraco/headless-client'
2 |
3 | const client = new Client({
4 | projectAlias: process.env.VUE_APP_UMBRACO__PROJECTALIAS!,
5 | apiKey: process.env.VUE_APP_UMBRACO__APIKEY,
6 | language: 'en-US'
7 | })
8 |
9 | export default client
10 |
--------------------------------------------------------------------------------
/samples/vue/src/components/Elements.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
22 |
--------------------------------------------------------------------------------
/samples/vue/src/components/Hero.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ title }}
4 | {{ subtitle }}
5 |
6 |
7 |
8 |
21 |
22 |
55 |
--------------------------------------------------------------------------------
/samples/vue/src/components/MainNavigation.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
39 |
40 |
83 |
--------------------------------------------------------------------------------
/samples/vue/src/components/PageFooter.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
24 |
25 |
85 |
--------------------------------------------------------------------------------
/samples/vue/src/components/PageHeader.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
26 |
27 |
68 |
--------------------------------------------------------------------------------
/samples/vue/src/components/UniqueSellingPoints.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ title }}
4 |
20 |
21 |
22 |
23 |
34 |
35 |
90 |
--------------------------------------------------------------------------------
/samples/vue/src/components/elements/TextAndImage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ title }}
4 |
5 |
15 |
25 |
26 |
27 |
28 |
42 |
43 |
102 |
--------------------------------------------------------------------------------
/samples/vue/src/main.ts:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Fragment from 'vue-fragment'
3 |
4 | import App from './App.vue'
5 | import router from './router'
6 |
7 | Vue.config.productionTip = false
8 | Vue.use(Fragment.Plugin)
9 |
10 | new Vue({
11 | router,
12 | render: h => h(App)
13 | }).$mount('#app')
14 |
--------------------------------------------------------------------------------
/samples/vue/src/router/index.ts:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import VueRouter from 'vue-router'
3 |
4 | import UmbracoPage from '@/views/UmbracoPage.vue'
5 |
6 | Vue.use(VueRouter)
7 |
8 | const routes = [
9 | {
10 | path: '*',
11 | name: 'UmbracoPage',
12 | component: UmbracoPage
13 | }
14 | ]
15 |
16 | const router = new VueRouter({
17 | mode: 'history',
18 | base: process.env.BASE_URL,
19 | routes
20 | })
21 |
22 | export default router
23 |
--------------------------------------------------------------------------------
/samples/vue/src/shims-tsx.d.ts:
--------------------------------------------------------------------------------
1 | import Vue, { VNode } from 'vue'
2 |
3 | declare global {
4 | namespace JSX {
5 | // tslint:disable no-empty-interface
6 | interface Element extends VNode {}
7 | // tslint:disable no-empty-interface
8 | interface ElementClass extends Vue {}
9 | interface IntrinsicElements {
10 | [elem: string]: any;
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/samples/vue/src/shims-vue.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.vue' {
2 | import Vue from 'vue'
3 | export default Vue
4 | }
5 |
6 | declare module 'vue-fragment';
7 |
--------------------------------------------------------------------------------
/samples/vue/src/types.ts:
--------------------------------------------------------------------------------
1 | import { Content } from '@umbraco/headless-client'
2 |
3 | export type Link = {
4 | url: string
5 | target?: string
6 | name: string
7 | }
8 |
9 | export type Image = {
10 | _url: string
11 | }
12 |
13 | export type Hero = {
14 | image?: Image,
15 | title: string,
16 | subtitle?: string
17 | }
18 |
19 | export type UniqueSellingPoint = {
20 | image?: Image
21 | link?: Link
22 | text: string
23 | title?: string
24 | }
25 |
26 | export type HideInNavigation = {
27 | umbNaviHide: boolean
28 | }
29 |
30 | export type Element = {
31 | contentTypeAlias: string
32 | }
33 |
34 | export type TextAndImage = Element & {
35 | title: string
36 | text: string
37 | image?: Image
38 | showLargeImage: boolean
39 | }
40 |
41 | export type Elements = TextAndImage
42 |
43 | export type Frontpage = Content & HideInNavigation & Hero & UniqueSellingPoint & Elements[]
44 | export type Textpage = Content & HideInNavigation & Hero & Elements[]
45 |
--------------------------------------------------------------------------------
/samples/vue/src/views/Frontpage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
27 |
--------------------------------------------------------------------------------
/samples/vue/src/views/NotFound.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
16 |
--------------------------------------------------------------------------------
/samples/vue/src/views/Textpage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
24 |
--------------------------------------------------------------------------------
/samples/vue/src/views/UmbracoPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
61 |
--------------------------------------------------------------------------------
/samples/vue/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "module": "esnext",
5 | "strict": true,
6 | "jsx": "preserve",
7 | "importHelpers": true,
8 | "moduleResolution": "node",
9 | "experimentalDecorators": true,
10 | "esModuleInterop": true,
11 | "allowSyntheticDefaultImports": true,
12 | "sourceMap": true,
13 | "baseUrl": ".",
14 | "types": [
15 | "webpack-env"
16 | ],
17 | "paths": {
18 | "@/*": [
19 | "src/*"
20 | ]
21 | },
22 | "lib": [
23 | "esnext",
24 | "dom",
25 | "dom.iterable",
26 | "scripthost"
27 | ]
28 | },
29 | "include": [
30 | "src/**/*.ts",
31 | "src/**/*.tsx",
32 | "src/**/*.vue",
33 | "tests/**/*.ts",
34 | "tests/**/*.tsx"
35 | ],
36 | "exclude": [
37 | "node_modules"
38 | ]
39 | }
40 |
--------------------------------------------------------------------------------
/samples/vue/vue.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | css: {
3 | sourceMap: true
4 | },
5 | lintOnSave: false
6 | }
7 |
--------------------------------------------------------------------------------
/src/APIRequestError.ts:
--------------------------------------------------------------------------------
1 | import { AxiosResponse } from 'axios'
2 |
3 | /**
4 | * @public
5 | */
6 | export class APIRequestError extends Error {
7 | data?: any = undefined
8 |
9 | /**
10 | * @internal
11 | */
12 | constructor (message: string, public response: AxiosResponse) {
13 | super(message)
14 | this.name = 'APIRequestError'
15 |
16 | if (response && response.data) {
17 | this.data = response.data
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/ApiRequest.ts:
--------------------------------------------------------------------------------
1 | import { Endpoint, EndpointSource } from './Endpoint'
2 | import { ClientOptions, ProxyOptions } from './Client'
3 | import { APIRequestError } from './APIRequestError'
4 | import axios, { AxiosRequestConfig } from 'axios'
5 | import FormData from 'form-data'
6 |
7 | /** @internal */
8 | export class ApiRequest {
9 | constructor (
10 | private readonly options: ClientOptions | ProxyOptions,
11 | public endpoint: Endpoint,
12 | public data?: any
13 | ) {}
14 |
15 | public promise = async (): Promise => {
16 | const headers: any = {
17 | 'Content-Type': 'application/json',
18 | Accept: 'application/json+hal',
19 | 'api-version': '2.2'
20 | }
21 |
22 | if ('projectAlias' in this.options) {
23 | headers['umb-project-alias'] = this.options.projectAlias
24 | }
25 |
26 | if (this.endpoint.source === EndpointSource.CDN && this.options.language) {
27 | headers['Accept-Language'] = this.options.language
28 | }
29 |
30 | if ('apiKey' in this.options && this.options.apiKey) {
31 | headers['api-key'] = this.options.apiKey
32 | }
33 |
34 | const path = this.endpoint.getPath()
35 | let url = 'https://cdn.umbraco.io'
36 |
37 | if (this.endpoint.source === EndpointSource.ContentManagement) {
38 | url = 'apiProxyUrl' in this.options
39 | ? this.options.apiProxyUrl
40 | : 'https://api.umbraco.io'
41 | } else if ('cdnProxyUrl' in this.options) {
42 | url = this.options.cdnProxyUrl
43 | } else if (this.options.preview) {
44 | url = 'https://preview.umbraco.io'
45 | }
46 |
47 | url = url.endsWith('/') ? `${url}${path.substr(1)}` : `${url}${path}`
48 |
49 | const requestInit: AxiosRequestConfig = {
50 | url: url,
51 | method: this.endpoint.method,
52 | headers: {}
53 | }
54 |
55 | const method = this.endpoint.method.toLowerCase()
56 | if ((method === 'post' || method === 'put') && !!this.data) {
57 | if (this.data instanceof FormData) {
58 | headers['Content-Type'] = `multipart/form-data; boundary=${this.data.getBoundary()}`
59 | requestInit.data = this.data
60 | } else if (this.data instanceof URLSearchParams) {
61 | headers['Content-Type'] = 'application/x-www-form-urlencoded'
62 | requestInit.data = this.data
63 | } else {
64 | requestInit.data = JSON.stringify(this.data)
65 | }
66 | }
67 | requestInit.headers = headers
68 |
69 | if ('accessTokenResolver' in this.options) {
70 | // @ts-ignore
71 | const token = this.options.accessTokenResolver(requestInit)
72 | if (token) {
73 | requestInit.headers.Authorization = `Bearer ${token}`
74 | }
75 | }
76 |
77 | try {
78 | const response = await axios(requestInit)
79 | return response.data as R
80 | } catch (err) {
81 | throw new APIRequestError(err.message, err.response)
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/ApiResponse.ts:
--------------------------------------------------------------------------------
1 | /** @internal */
2 | export interface ApiResponse {
3 |
4 | _links: Links
5 | _embedded: Response
6 |
7 | }
8 |
9 | /** @internal */
10 | export interface ApiPagedResponse extends ApiResponse {
11 | _totalItems: number
12 | _totalPages: number
13 | _page: number
14 | _pageSize: number
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/src/Client.ts:
--------------------------------------------------------------------------------
1 | import { ManagementClient, DeliveryClient, AuthenticationClient } from './Clients'
2 | import { Endpoint } from './Endpoint'
3 | import { ApiRequest } from './ApiRequest'
4 |
5 | /**
6 | * Client Options
7 | * @public
8 | */
9 | export interface ClientOptions {
10 | /**
11 | * The Project Alias is a HTTP friendly version of the Project Name under your Umbraco Cloud account.
12 | */
13 | projectAlias: string
14 | /**
15 | * The default culture sent with all requests to the Content Delivery API, this can be overwritten per function
16 | */
17 | language?: string
18 | /**
19 | * An API Key is requierd when interacting with the Management API and when protection is enabled for the Delivery API
20 | */
21 | apiKey?: string
22 | /**
23 | * Determines if the {@link DeliveryClient} should call the Preview API or the Content Delivery endpoints.
24 | *
25 | * @remarks
26 | * If true an apiKey must be supplied.
27 | */
28 | preview?: boolean
29 | /**
30 | * Used to retrieve access tokens for requests to the APIs.
31 | * @param request - The request that's about to be sent.
32 | * @returns an oauth token that should be used for this request or undefined if no token should be used.
33 | */
34 | accessTokenResolver?(request: { data?: any, headers: any, method: 'get'|'GET'|'post'|'POST'|'put'|'PUT'|'delete'|'DELETE', url: string }): string | undefined
35 | }
36 |
37 | /**
38 | * Proxy options
39 | * @public
40 | */
41 | export interface ProxyOptions {
42 | /**
43 | * A custom url for the Content Delivery endpoint.
44 | */
45 | cdnProxyUrl: string
46 | /**
47 | * A custom url for the Content Management endpoint.
48 | */
49 | apiProxyUrl: string
50 | /**
51 | * The default culture sent with all requests to the Content Delivery API, this can be overwritten per function
52 | */
53 | language?: string
54 | }
55 |
56 | /**
57 | * Entry class for accessing the Content Delivery and Content Management APIs.
58 | * @public
59 | *
60 | * @example
61 | *
62 | * To get started you need create a new instance of the `Client` passing {@link ClientOptions}.
63 | *
64 | * ```typescript
65 | * import { Client } from '@umbraco/headless-client'
66 | *
67 | * const client = new Client({
68 | * projectAlias: '',
69 | * apiKey: '',
70 | * language: '',
71 | * })
72 | * ```
73 | *
74 | * You might want to proxy your request through a server to hide the project alias and the api key,
75 | * this can be done by creating a new instance of the `Client` class passing in {@link ProxyOptions}.
76 | *
77 | * ```typescript
78 | * import { Client } from '@umbraco/headless-client'
79 | *
80 | * const client = new Client({
81 | * apiProxyUrl: '',
82 | * cdnProxyUrl: '',
83 | * language: '',
84 | * })
85 | * ```
86 | *
87 | */
88 | export class Client {
89 | /**
90 | * Constructs a new instance of the `Client` class with the given options.
91 | * @param options - The options. See {@link ClientOptions} or {@link ProxyOptions}.
92 | */
93 | constructor (public readonly options: ClientOptions | ProxyOptions) {
94 |
95 | }
96 |
97 | /**
98 | * Get Delivery client for fetching content and media from CDN.
99 | * See {@link DeliveryClient}
100 | */
101 | public readonly delivery = new DeliveryClient(this)
102 |
103 | /**
104 | * Get Manager Client for managing content on Umbraco Heartcore.
105 | * See {@link ManagementClient}
106 | */
107 | public readonly management = new ManagementClient(this)
108 |
109 | /**
110 | * Get Authentication Client for authenticating members and Backoffice users.
111 | * See {@link AuthenticationClient}
112 | */
113 | public readonly authentication = new AuthenticationClient(this)
114 |
115 | /**
116 | * Makes request from and [Endpoint]
117 | * @internal
118 | */
119 | public makeRequest = async (endpoint: Endpoint, data?: any): Promise => {
120 | const response = await new ApiRequest(this.options, endpoint, data).promise()
121 | const items = this.getEmbeddedData(response)
122 | const pageData = this.getPagedData(response)
123 |
124 | if (pageData && items) {
125 | return {
126 | ...pageData,
127 | items
128 | }
129 | } else if (!pageData && items) {
130 | const { _embedded, _links, ...data } = response
131 | if (Object.keys(data).length) {
132 | return { ..._embedded, ...data }
133 | }
134 | return items
135 | }
136 | return response
137 | }
138 |
139 | /**
140 | * Sets the API to be used.
141 | * @param apikey - API Key
142 | * @deprecated Use `apiKey` in the constructor options instead.
143 | */
144 | public setAPIKey = (apikey: string) => {
145 | if('apiKey' in this.options) {
146 | this.options.apiKey = apikey
147 | } else {
148 | throw Error('Cannot set apiKey on ProxyOptions')
149 | }
150 | }
151 |
152 | /**
153 | * @deprecated Use `options.apiKey` instead.
154 | */
155 | public getAPIKey = () => {
156 | if('apiKey' in this.options) {
157 | return this.options.apiKey
158 | } else {
159 | throw Error('Cannot set apiKey on ProxyOptions')
160 | }
161 | }
162 |
163 | private readonly getEmbeddedData = (response: any) => {
164 | if (Object.prototype.hasOwnProperty.call(response, '_embedded')) {
165 | const keys = Object.keys(response._embedded)
166 | const keyCount = keys.length
167 | if (keyCount === 1) {
168 | const key = keys[0]
169 | return response._embedded[key]
170 | }
171 | }
172 |
173 | return null
174 | }
175 |
176 | private readonly getPagedData = (response: any) => {
177 | const lookForProps = ['_totalItems', '_totalPages', '_page', '_pageSize']
178 | const keys = Object.keys(response)
179 |
180 | for (let i = 0; i < lookForProps.length; i++) {
181 | const needle = lookForProps[i]
182 | if (!keys.includes(needle)) return null
183 | }
184 |
185 | const object: any = {}
186 | lookForProps.forEach(key => {
187 | object[key.replace(/^_/, '')] = response[key]
188 | })
189 |
190 | return object
191 | }
192 | }
193 |
--------------------------------------------------------------------------------
/src/Clients/AuthenticationClient.spec.ts:
--------------------------------------------------------------------------------
1 | import 'mocha'
2 | import { expect } from 'chai'
3 | import axios from 'axios'
4 | import MockAdapter from 'axios-mock-adapter'
5 |
6 | import { Client } from '../Client'
7 |
8 | describe('AuthenticationClient', function () {
9 | let client: Client
10 | let axiosMock: MockAdapter
11 |
12 | before(function () {
13 | client = new Client({ projectAlias: 'my-project' })
14 | axiosMock = new MockAdapter(axios)
15 | })
16 |
17 | after(function () {
18 | axiosMock.restore()
19 | })
20 |
21 | afterEach(function () {
22 | axiosMock.reset()
23 | })
24 |
25 | describe('#authenticateMember()', function () {
26 | it('calls the oauth endpoint', async function () {
27 | axiosMock.onPost('https://cdn.umbraco.io/member/oauth/token').reply(200, {})
28 |
29 | const result = await client.authentication.authenticateMember('jane@example.com', 'myPassword')
30 |
31 | expect(result).to.not.be.undefined
32 | expect(axiosMock.history.post.length).to.be.eq(1)
33 | expect(axiosMock.history.post[0].headers['Content-Type']).to.be.eq('application/x-www-form-urlencoded')
34 | expect(axiosMock.history.post[0].data).to.be.eq('grant_type=password&username=jane%40example.com&password=myPassword')
35 | })
36 | })
37 |
38 | describe('#authenticateUser()', function () {
39 | it('calls the oauth endpoint', async function () {
40 | axiosMock.onPost('https://api.umbraco.io/oauth/token').reply(200, {})
41 |
42 | const result = await client.authentication.authenticateUser('john@example.com', 'pass1234')
43 |
44 | expect(result).to.not.be.undefined
45 | expect(axiosMock.history.post.length).to.be.eq(1)
46 | expect(axiosMock.history.post[0].headers['Content-Type']).to.be.eq('application/x-www-form-urlencoded')
47 | expect(axiosMock.history.post[0].data).to.be.eq('grant_type=password&username=john%40example.com&password=pass1234')
48 | })
49 | })
50 | })
51 |
--------------------------------------------------------------------------------
/src/Clients/AuthenticationClient.ts:
--------------------------------------------------------------------------------
1 | import { Client, ClientOptions } from '../Client'
2 | import { ApiRequest } from '../ApiRequest'
3 | import { Endpoints } from '../Endpoints'
4 | import { OAUthResponse } from '../Responses'
5 |
6 | /**
7 | * AuthenticationClient is used to authenticate members and Backoffice users.
8 | * @public
9 | *
10 | * @example
11 | * The {@link AuthenticationClient} must be accessed through {@link Client}.
12 | *
13 | * ```typescript
14 | * import { Client } from '@umbraco/headless-client'
15 | *
16 | * const client = new Client({
17 | * projectAlias: '',
18 | * apiKey: '',
19 | * language: '',
20 | * })
21 | *
22 | * const authClient = client.authentication
23 | * ```
24 | */
25 | export class AuthenticationClient {
26 | /**
27 | * @internal
28 | */
29 | constructor (
30 | private readonly client: Client
31 | ) {
32 |
33 | }
34 |
35 | /**
36 | * Authenticate a member using username and password.
37 | * @param username - The members username.
38 | * @param password - The members password.
39 | * @returns a Promise resolving to a {@link OAUthResponse}
40 | */
41 | async authenticateMember (username: string, password: string) {
42 | const data = new URLSearchParams()
43 | data.append('grant_type', 'password')
44 | data.append('username', username)
45 | data.append('password', password)
46 |
47 | const options = { projectAlias: '' }
48 | if ('projectAlias' in this.client.options) {
49 | options.projectAlias = this.client.options.projectAlias
50 | }
51 |
52 | return new ApiRequest(options, Endpoints.authentication.member(), data).promise()
53 | }
54 |
55 | /**
56 | * Authenticate a Backoffice user using username and password.
57 | * @param username - The users username.
58 | * @param password - The users password.
59 | * @returns a Promise resolving to a {@link OAUthResponse}
60 | */
61 | async authenticateUser (username: string, password: string) {
62 | const data = new URLSearchParams()
63 | data.append('grant_type', 'password')
64 | data.append('username', username)
65 | data.append('password', password)
66 |
67 | const options = { projectAlias: '' }
68 | if ('projectAlias' in this.client.options) {
69 | options.projectAlias = this.client.options.projectAlias
70 | }
71 |
72 | return new ApiRequest(options, Endpoints.authentication.user(), data).promise()
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/Clients/Delivery/ContentDeliveryClient.ts:
--------------------------------------------------------------------------------
1 | import { Client } from '../../Client'
2 | import { Endpoints } from '../../Endpoints'
3 | import { Endpoint } from '../../Endpoint'
4 | import {
5 | ContentDeliveryAncestorsOptions, ContentDeliveryByContentTypeOptions,
6 | ContentDeliveryByIdOptions,
7 | ContentDeliveryByUrlOptions, ContentDeliveryChildrenOptions, ContentDeliveryDescendantsOptions,
8 | ContentDeliveryRootOptions, ContentDeliverySearchOptions, ContentDeliveryFilterOptions} from '../../RequestOptions'
9 | import { Content } from '../../Responses'
10 | import { PagedResponse } from '../../Responses/PagedResponse'
11 | import { ContentFilter } from '../../RequestOptions/ContentFilterOptions'
12 |
13 | /**
14 | * ContentDeliveryClient is used to access the Content part of the Content Delivery API.
15 | * @public
16 | *
17 | * @example
18 | * The {@link ContentDeliveryClient} must be accessed through {@link Client}.
19 | *
20 | * ```typescript
21 | * import { Client } from '@umbraco/headless-client'
22 | *
23 | * const client = new Client({
24 | * projectAlias: '',
25 | * apiKey: '',
26 | * language: '',
27 | * })
28 | *
29 | * const contentClient = client.delivery.content
30 | * ```
31 | */
32 | export class ContentDeliveryClient {
33 | /** @internal */
34 | constructor (private readonly client: Client) {}
35 |
36 | private readonly makeRequest = async (endpoint: Endpoint, data?: any): Promise => {
37 | const result = await this.client.makeRequest(endpoint, data)
38 | return result
39 | }
40 |
41 | /**
42 | * Fetch all Content at the root.
43 | *
44 | * @param options - Request options. See {@link ContentDeliveryRootOptions}.
45 | * @returns a `Promise` that resolves to an array of {@link Content}.
46 | */
47 | async root (options?: ContentDeliveryRootOptions) {
48 | return this.makeRequest(Endpoints.delivery.content.root(options))
49 | }
50 |
51 | /**
52 | * Fetch a single Content item by its id.
53 | * @param id - GUID id of the Content item.
54 | * @param options - Request options. See {@link ContentDeliveryByIdOptions}.
55 | * @returns a `Promise` that resolves to a {@link Content} if found, otherwise `undefined`.
56 | */
57 | async byId (id: string, options?: ContentDeliveryByIdOptions) {
58 | try {
59 | return await this.makeRequest(Endpoints.delivery.content.byId(id, options))
60 | } catch (err) {
61 | if (err.response && err.response.status === 404) {
62 | return undefined
63 | }
64 | throw err
65 | }
66 | }
67 |
68 | /**
69 | * Feth a single Contint item by its Url.
70 | * @param url - Url for the content to retrieve.
71 | * @param options - Request options. See {@link ContentDeliveryByUrlOptions}.
72 | * @returns a `Promise` that resolves to a {@link Content} if found, otherwise `undefined`.
73 | */
74 | async byUrl< T extends Content> (url: string, options?: ContentDeliveryByUrlOptions) {
75 | try {
76 | return await this.makeRequest(Endpoints.delivery.content.byUrl(url, options))
77 | } catch (err) {
78 | if (err.response && err.response.status === 404) {
79 | return undefined
80 | }
81 | throw err
82 | }
83 | }
84 |
85 | /**
86 | * Fetch children for a Content item.
87 | * @param id - GUID id of the Content item.
88 | * @param options - Request options. See {@link ContentDeliveryChildrenOptions}.
89 | * @returns a `Promise` that resolves to a {@link PagedResponse} of {@link Content} if found, otherwise `undefined`.
90 | */
91 | async children (id: string, options?: ContentDeliveryChildrenOptions): Promise | undefined> {
92 | try {
93 | return await this.makeRequest(Endpoints.delivery.content.children(id, options))
94 | } catch (err) {
95 | if (err.response && err.response.status === 404) {
96 | return undefined
97 | }
98 | throw err
99 | }
100 | }
101 |
102 | /**
103 | * Fetch ancestors for a content item.
104 | * @param id - GUID id of the Content item.
105 | * @param options - Request options. See {@link ContentDeliveryAncestorsOptions}.
106 | * @returns a `Promise` that resolves to an array of {@link Content} if found, otherwise `undefined`.
107 | */
108 | async ancestors (id: string, options?: ContentDeliveryAncestorsOptions) {
109 | try {
110 | return await this.makeRequest(Endpoints.delivery.content.ancestors(id, options))
111 | } catch (err) {
112 | if (err.response && err.response.status === 404) {
113 | return undefined
114 | }
115 | throw err
116 | }
117 | }
118 |
119 | /**
120 | * Fetch descendants for a content item.
121 | * @param id - GUID id of the Content item.
122 | * @param options - Request options. See {@link ContentDeliveryDescendantsOptions}.
123 | * @returns a `Promise` that resolves to an array of {@link Content} if found, otherwise `undefined`.
124 | */
125 | async descendants (id: string, options?: ContentDeliveryDescendantsOptions) {
126 | try {
127 | return await this.makeRequest(Endpoints.delivery.content.descendants(id, options))
128 | } catch (err) {
129 | if (err.response && err.response.status === 404) {
130 | return undefined
131 | }
132 | throw err
133 | }
134 | }
135 |
136 | /**
137 | * Fetch Content of a specific type.
138 | * @param contentType - Content Type to filter by.
139 | * @param options - Request options. See {@link ContentDeliveryByContentTypeOptions}
140 | * @returns a `Promise` that resolves to a {@link PagedResponse} of {@link Content}.
141 | */
142 | async byContentType (contentType: string, options?: ContentDeliveryByContentTypeOptions): Promise> {
143 | return this.makeRequest(Endpoints.delivery.content.byContentType(contentType, options))
144 | }
145 |
146 | /**
147 | * Filter for Content containing specific property values
148 | * @param contentFilter - Filter object. See {@link ContentFilter}
149 | * @param options - Request options. See {@link ContentDeliveryFilterOptions}
150 | * @returns a `Promise` that resolves to a {@link PagedResponse} of {@link Content}.
151 | */
152 | async filter (body: ContentFilter, options?: ContentDeliveryFilterOptions): Promise> {
153 | return this.makeRequest(Endpoints.delivery.content.filter(options), body)
154 | }
155 |
156 | /**
157 | * Search for Content containing term,
158 | * @param term - Term to search for
159 | * @param options - Request options. See {@link ContentDeliverySearchOptions}
160 | * @returns a `Promise` that resolves to a {@link PagedResponse} of {@link Content}.
161 | */
162 | async search (term: string, options?: ContentDeliverySearchOptions): Promise> {
163 | return this.makeRequest(Endpoints.delivery.content.search(term, options))
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/src/Clients/Delivery/DeliveryClient.ts:
--------------------------------------------------------------------------------
1 | import { Client } from '../../Client'
2 | import { ContentDeliveryClient } from './ContentDeliveryClient'
3 | import { MediaDeliveryClient } from './MediaDeliveryClient'
4 | import { RedirectDeliveryClient } from './RedirectDeliveryClient'
5 |
6 | /**
7 | * DeliveryClient used to access the Content Delivery API.
8 | * @public
9 | *
10 | * @example
11 | * The {@link DeliveryClient} must be accessed through {@link Client}.
12 | *
13 | * ```typescript
14 | * import { Client } from '@umbraco/headless-client'
15 | *
16 | * const client = new Client({
17 | * projectAlias: '',
18 | * apiKey: '',
19 | * language: '',
20 | * })
21 | *
22 | * const deliveryClient = client.delivery
23 | * ```
24 | */
25 | export class DeliveryClient {
26 | /**
27 | * The Content client for the Content Delivery API.
28 | * See {@link ContentDeliveryClient}
29 | */
30 | public readonly content = new ContentDeliveryClient(this.client);
31 |
32 | /**
33 | * The Media client for the Content Delivery API.
34 | * See {@link MediaDeliveryClient}
35 | */
36 | public readonly media = new MediaDeliveryClient(this.client);
37 |
38 | /**
39 | * The Redirect client for the Content Delivery API.
40 | * See {@link RedirectDeliveryClient}
41 | */
42 | public readonly redirect = new RedirectDeliveryClient(this.client);
43 |
44 | /** @internal */
45 | constructor (private readonly client: Client) {}
46 | }
47 |
--------------------------------------------------------------------------------
/src/Clients/Delivery/MediaDeliveryClient.ts:
--------------------------------------------------------------------------------
1 | import { Client } from '../../Client'
2 | import { Endpoint } from '../../Endpoint'
3 | import { Endpoints } from '../../Endpoints'
4 | import { MediaDeliveryChildrenOptions } from '../../RequestOptions/index'
5 | import { Media } from '../../Responses/Media'
6 |
7 | /**
8 | * MediaDeliveryClient is used to access the Media part of the Content Delivery API.
9 | * @public
10 | *
11 | * @example
12 | * The {@link MediaDeliveryClient} must be accessed through {@link Client}.
13 | *
14 | * ```typescript
15 | * import { Client } from '@umbraco/headless-client'
16 | *
17 | * const client = new Client({
18 | * projectAlias: '',
19 | * apiKey: '',
20 | * language: '',
21 | * })
22 | *
23 | * const mediaClient = client.delivery.media
24 | * ```
25 | */
26 | export class MediaDeliveryClient {
27 | /** @internal */
28 | constructor (private readonly client: Client) {}
29 |
30 | private readonly makeRequest = async (endpoint: Endpoint, data?: any) => {
31 | const result = await this.client.makeRequest(endpoint, data)
32 | return result
33 | }
34 |
35 | /**
36 | * Fetch all Media at the root.
37 | *
38 | * @returns a `Promise` that resolves to an array of {@link Media}.
39 | */
40 | async root () {
41 | return this.makeRequest(Endpoints.delivery.media.root())
42 | }
43 |
44 | /**
45 | * Fetch a single Media item by its id.
46 | * @param id - GUID id of the Media item.
47 | * @returns a `Promise` that resolves to a {@link Media} if found, otherwise `undefined`.
48 | */
49 | async byId (id: string) {
50 | try {
51 | return await this.makeRequest(Endpoints.delivery.media.byId(id))
52 | } catch (err) {
53 | if (err.response && err.response.status === 404) {
54 | return undefined
55 | }
56 | throw err
57 | }
58 | }
59 |
60 | /**
61 | * Fetch children for a Media item.
62 | * @param id - GUID id of the Media item.
63 | * @param options - Request options. See {@link MediaDeliveryChildrenOptions}.
64 | * @returns a `Promise` that resolves to a {@link PagedResponse} of {@link Media} if found, otherwise `undefined`.
65 | */
66 | async children (id: string, options?: MediaDeliveryChildrenOptions) {
67 | try {
68 | return await this.makeRequest(Endpoints.delivery.media.children(id, options))
69 | } catch (err) {
70 | if (err.response && err.response.status === 404) {
71 | return undefined
72 | }
73 | throw err
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/Clients/Delivery/RedirectDeliveryClient.ts:
--------------------------------------------------------------------------------
1 | import { Client } from '../../Client';
2 | import { Endpoint } from '../../Endpoint';
3 | import { Endpoints } from '../../Endpoints';
4 | import { ContentDeliveryRedirectOptions } from '../../RequestOptions';
5 | import { Redirect } from '../../Responses/Redirect';
6 |
7 | /**
8 | * RedirectDeliveryClient is used to access the Redirect part of the Content Delivery API.
9 | * @public
10 | *
11 | * @example
12 | * The {@link RedirectDeliveryClient} must be accessed through {@link Client}.
13 | *
14 | * ```typescript
15 | * import { Client } from '@umbraco/headless-client'
16 | *
17 | * const client = new Client({
18 | * projectAlias: '',
19 | * apiKey: '',
20 | * language: '',
21 | * })
22 | *
23 | * const mediaClient = client.delivery.redirect
24 | * ```
25 | */
26 | export class RedirectDeliveryClient {
27 | /** @internal **/
28 | constructor(private readonly client: Client) { }
29 |
30 | private readonly makeRequest = async (endpoint: Endpoint, data?: any): Promise => {
31 | const result = await this.client.makeRequest(endpoint, data);
32 | return result;
33 | };
34 |
35 | /**
36 | * Fetch all redirects
37 | * @param options - Request options. See {@link ContentDeliveryRedirectOptions}
38 | * @returns a `Promise` that resolves to a {@link PagedResponse} of {@link Redirect}
39 | */
40 | async getAll(options?: ContentDeliveryRedirectOptions) {
41 | return this.makeRequest(Endpoints.delivery.redirect.getAll(options));
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/Clients/Delivery/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ContentDeliveryClient'
2 | export * from './MediaDeliveryClient'
3 | export * from './RedirectDeliveryClient'
4 | export * from './DeliveryClient'
5 |
--------------------------------------------------------------------------------
/src/Clients/Management/ContentManagementClient.spec.ts:
--------------------------------------------------------------------------------
1 | import 'mocha'
2 | import { expect } from 'chai'
3 | import axios from 'axios'
4 | import MockAdapter from 'axios-mock-adapter'
5 | import FormData from 'form-data'
6 |
7 | import { Client } from '../../Client'
8 | import { ContentManagementContentRequest } from '../../Responses'
9 |
10 | const API_ROOT = 'https://api.umbraco.io/content'
11 |
12 | describe('ContentManagementClient', function () {
13 | let client: Client
14 | let axiosMock: MockAdapter
15 |
16 | before(function () {
17 | client = new Client({ projectAlias: 'my-project', apiKey: 'my-api-key' })
18 | axiosMock = new MockAdapter(axios)
19 | })
20 |
21 | after(function () {
22 | axiosMock.restore()
23 | })
24 |
25 | afterEach(function () {
26 | axiosMock.reset()
27 | })
28 |
29 | describe('#root()', function () {
30 | it('can retrieve content', async function () {
31 | axiosMock.onGet(API_ROOT).reply(200, require('./__mocks__/content.root.json'))
32 |
33 | const result = await client.management.content.root()
34 | expect(result.length).to.be.eq(1)
35 | })
36 | })
37 |
38 | describe('#byId()', function () {
39 | it('can retrieve content', async function () {
40 | axiosMock.onGet(`${API_ROOT}/3de82763-c4bb-4bca-8f79-7b211b3ffffa`).reply(200, require('./__mocks__/content.byId.json'))
41 |
42 | const result = await client.management.content.byId('3de82763-c4bb-4bca-8f79-7b211b3ffffa')
43 |
44 | expect(result).to.not.be.undefined
45 | // @ts-ignore
46 | expect(result.name.$invariant).to.be.eq('Unicorn')
47 | })
48 |
49 | it('returns undefined when not found', async function () {
50 | const result = await client.management.content.byId('3de82763-c4bb-4bca-8f79-7b211b3ffffa')
51 |
52 | expect(result).to.be.undefined
53 | })
54 | })
55 |
56 | describe('#children()', function () {
57 | it('can retrieve content', async function () {
58 | axiosMock.onGet(`${API_ROOT}/8007e923-e62a-4ac1-a33f-caf3052582f4/children`).reply(200, require('./__mocks__/content.children.json'))
59 |
60 | const result = await client.management.content.children('8007e923-e62a-4ac1-a33f-caf3052582f4')
61 |
62 | expect(result).to.not.be.undefined
63 | // @ts-ignore
64 | expect(result.items.length).to.be.eq(3)
65 | })
66 |
67 | it('returns undefined when not found', async function () {
68 | const result = await client.management.content.byId('3de82763-c4bb-4bca-8f79-7b211b3ffffa/children')
69 |
70 | expect(result).to.be.undefined
71 | })
72 | })
73 |
74 | describe('#create()', function () {
75 | it('should accept a json object', async function () {
76 | axiosMock.onPost(API_ROOT).reply(201, require('./__mocks__/content.create.json'))
77 |
78 | const data: ContentManagementContentRequest = {
79 | name: {
80 | $invariant: 'Another one'
81 | },
82 | contentTypeAlias: 'blogpost',
83 | parentId: '8007e923-e62a-4ac1-a33f-caf3052582f4',
84 | sortOrder: 0,
85 | seoMetaDescription: {
86 | $invariant: ''
87 | },
88 | keywords: {
89 | $invariant: []
90 | },
91 | umbNaviHide: {
92 | $invariant: '0'
93 | },
94 | pageTitle: {
95 | $invariant: 'Another one'
96 | },
97 | categories: {
98 | $invariant: [
99 | 'cg16',
100 | 'codegarden',
101 | 'umbraco'
102 | ]
103 | }
104 | }
105 |
106 | const result = await client.management.content.create(data)
107 |
108 | expect(result.name.$invariant).to.be.eq('Another one')
109 | expect(axiosMock.history.post.length).to.be.eq(1)
110 | expect(axiosMock.history.post[0].data).to.be.eq(JSON.stringify(data))
111 | })
112 |
113 | it('should accept FormData object', async function () {
114 | axiosMock.onPost(API_ROOT).reply(201, require('./__mocks__/content.create.json'))
115 |
116 | const data = new FormData()
117 |
118 | data.append('content', JSON.stringify({
119 | name: {
120 | $invariant: 'Another one'
121 | },
122 | contentTypeAlias: 'blogpost',
123 | parentId: '8007e923-e62a-4ac1-a33f-caf3052582f4',
124 | sortOrder: 0,
125 | seoMetaDescription: {
126 | $invariant: ''
127 | },
128 | keywords: {
129 | $invariant: []
130 | },
131 | umbNaviHide: {
132 | $invariant: '0'
133 | },
134 | pageTitle: {
135 | $invariant: 'Another one'
136 | },
137 | categories: {
138 | $invariant: [
139 | 'cg16',
140 | 'codegarden',
141 | 'umbraco'
142 | ]
143 | }
144 | }))
145 |
146 | const result = await client.management.content.create(data)
147 |
148 | expect(result.name.$invariant).to.be.eq('Another one')
149 | expect(axiosMock.history.post.length).to.be.eq(1)
150 | expect(axiosMock.history.post[0].data).to.be.eq(data)
151 | })
152 | })
153 |
154 | describe('#publish()', function () {
155 | it('call publish endpoint', async function () {
156 | axiosMock.onPut(`${API_ROOT}/3de82763-c4bb-4bca-8f79-7b211b3ffffa/publish?culture=en-US`).reply(200, {})
157 |
158 | await client.management.content.publish('3de82763-c4bb-4bca-8f79-7b211b3ffffa', { culture: 'en-US' })
159 |
160 | expect(axiosMock.history.put.length).to.be.eq(1)
161 | })
162 |
163 | it('returns undefined when not found', async function () {
164 | const result = await client.management.content.publish('3de82763-c4bb-4bca-8f79-7b211b3ffffa')
165 |
166 | expect(result).to.be.undefined
167 | })
168 | })
169 |
170 | describe('#unPublish()', function () {
171 | it('calls unpublish endpoint', async function () {
172 | axiosMock.onPut(`${API_ROOT}/3de82763-c4bb-4bca-8f79-7b211b3ffffa/unpublish?culture=en-US`).reply(200, {})
173 |
174 | await client.management.content.unPublish('3de82763-c4bb-4bca-8f79-7b211b3ffffa', { culture: 'en-US' })
175 |
176 | expect(axiosMock.history.put.length).to.be.eq(1)
177 | })
178 |
179 | it('returns undefined when not found', async function () {
180 | const result = await client.management.content.unPublish('3de82763-c4bb-4bca-8f79-7b211b3ffffa')
181 |
182 | expect(result).to.be.undefined
183 | })
184 | })
185 |
186 | describe('#delete()', function () {
187 | it('calls delete endpoint', async function () {
188 | axiosMock.onDelete(`${API_ROOT}/3de82763-c4bb-4bca-8f79-7b211b3ffffa`).reply(200, {})
189 |
190 | await client.management.content.delete('3de82763-c4bb-4bca-8f79-7b211b3ffffa')
191 |
192 | expect(axiosMock.history.delete.length).to.be.eq(1)
193 | })
194 |
195 | it('returns undefined when not found', async function () {
196 | const result = await client.management.content.delete('3de82763-c4bb-4bca-8f79-7b211b3ffffa')
197 |
198 | expect(result).to.be.undefined
199 | })
200 | })
201 |
202 | describe('#update()', function () {
203 | it('should accept a json object', async function () {
204 | axiosMock.onPut(`${API_ROOT}/041067a0-74f5-4d03-92af-40c3c0aa13e7`).reply(201, require('./__mocks__/content.create.json'))
205 |
206 | const data: ContentManagementContentRequest = {
207 | name: {
208 | $invariant: 'Another one'
209 | },
210 | contentTypeAlias: 'blogpost',
211 | parentId: '8007e923-e62a-4ac1-a33f-caf3052582f4',
212 | sortOrder: 0,
213 | seoMetaDescription: {
214 | $invariant: ''
215 | },
216 | keywords: {
217 | $invariant: []
218 | },
219 | umbNaviHide: {
220 | $invariant: '0'
221 | },
222 | pageTitle: {
223 | $invariant: 'Another one'
224 | },
225 | categories: {
226 | $invariant: [
227 | 'cg16',
228 | 'codegarden',
229 | 'umbraco'
230 | ]
231 | }
232 | }
233 |
234 | const result = await client.management.content.update('041067a0-74f5-4d03-92af-40c3c0aa13e7', data)
235 |
236 | expect(result).to.not.be.undefined
237 | // @ts-ignore
238 | expect(result.name.$invariant).to.be.eq('Another one')
239 | expect(axiosMock.history.put.length).to.be.eq(1)
240 | expect(axiosMock.history.put[0].data).to.be.eq(JSON.stringify(data))
241 | })
242 |
243 | it('should accept FormData object', async function () {
244 | axiosMock.onPut(`${API_ROOT}/041067a0-74f5-4d03-92af-40c3c0aa13e7`).reply(201, require('./__mocks__/content.create.json'))
245 |
246 | const data = new FormData()
247 |
248 | data.append('content', JSON.stringify({
249 | name: {
250 | $invariant: 'Another one'
251 | },
252 | contentTypeAlias: 'blogpost',
253 | parentId: '8007e923-e62a-4ac1-a33f-caf3052582f4',
254 | sortOrder: 0,
255 | seoMetaDescription: {
256 | $invariant: ''
257 | },
258 | keywords: {
259 | $invariant: []
260 | },
261 | umbNaviHide: {
262 | $invariant: '0'
263 | },
264 | pageTitle: {
265 | $invariant: 'Another one'
266 | },
267 | categories: {
268 | $invariant: [
269 | 'cg16',
270 | 'codegarden',
271 | 'umbraco'
272 | ]
273 | }
274 | }))
275 |
276 | const result = await client.management.content.update('041067a0-74f5-4d03-92af-40c3c0aa13e7', data)
277 |
278 | expect(result).to.not.be.undefined
279 | // @ts-ignore
280 | expect(result.name.$invariant).to.be.eq('Another one')
281 | expect(axiosMock.history.put.length).to.be.eq(1)
282 | expect(axiosMock.history.put[0].data).to.be.eq(data)
283 | })
284 |
285 | it('returns undefined when not found', async function () {
286 | const data = {
287 | name: {
288 | $invariant: 'Another one'
289 | },
290 | contentTypeAlias: 'blogpost'
291 | }
292 |
293 | const result = await client.management.content.update('3de82763-c4bb-4bca-8f79-7b211b3ffffa', data)
294 |
295 | expect(result).to.be.undefined
296 | })
297 | })
298 | })
299 |
--------------------------------------------------------------------------------
/src/Clients/Management/ContentManagementClient.ts:
--------------------------------------------------------------------------------
1 | import FormData from 'form-data'
2 |
3 | import { Client } from '../../Client'
4 | import { PagedResponse, ContentManagementContent, ContentManagementContentRequest } from '../../Responses'
5 | import { Endpoint } from '../../Endpoint'
6 | import { Endpoints } from '../../Endpoints'
7 | import { APIContentChildrenOptions, APIContentPublishOptions, APIContentUnpublishOptions } from '../../RequestOptions'
8 |
9 | /**
10 | * ContentManagementClient is used to access the Content part of the Content Management API.
11 | * @public
12 | *
13 | * @example
14 | * The {@link ContentManagementClient} must be accessed through {@link Client}.
15 | *
16 | * ```typescript
17 | * import { Client } from '@umbraco/headless-client'
18 | *
19 | * const client = new Client({
20 | * projectAlias: '',
21 | * apiKey: '',
22 | * language: '',
23 | * })
24 | *
25 | * const contentClient = client.management.content
26 | * ```
27 | */
28 | export class ContentManagementClient {
29 | /**
30 | * @internal
31 | */
32 | constructor (
33 | private readonly client: Client
34 | ) {
35 | }
36 |
37 | private readonly makeRequest = async (endpoint: Endpoint, data?: any) => {
38 | return this.client.makeRequest(endpoint, data)
39 | }
40 |
41 | /**
42 | * Fetch all content at the root of the tree, which the authorized user has access to according to the 'Start node'-permissions.
43 | * @returns a `Promise` that resolves to an array of {@link ContentManagementContent},
44 | */
45 | async root () {
46 | return this.makeRequest(Endpoints.management.content.root())
47 | }
48 |
49 | /**
50 | * Fetch a single Content item by its id.
51 | * @param id - GUID id of the Content item.
52 | * @returns a `Promise` that resolves to a {@link ContentManagementContent} if found, otherwise `undefined`.
53 | */
54 | async byId (id: string) {
55 | try {
56 | return await this.makeRequest(Endpoints.management.content.byId(id))
57 | } catch (err) {
58 | if (err.response && err.response.status === 404) {
59 | return undefined
60 | }
61 | throw err
62 | }
63 | }
64 |
65 | /**
66 | * Fetch all children of a Content item.
67 | * @param id - GUID id of the Content item.
68 | * @param options - Request options. See {@link APIContentChildrenOptions}.
69 | * @returns a `Promise` that resolves to a {@link PagedResponse} of {@link ContentManagementContent} if found, otherwise `undefined`.
70 | */
71 | async children (id: string, options?: APIContentChildrenOptions): Promise | undefined> {
72 | try {
73 | return await this.makeRequest(Endpoints.management.content.children(id, options))
74 | } catch (err) {
75 | if (err.response && err.response.status === 404) {
76 | return undefined
77 | }
78 | throw err
79 | }
80 | }
81 |
82 | /**
83 | * Create a new Content item.
84 | * @param body - The Content to create. See {@link ContentManagementContentRequest}.
85 | * @returns a `Promise` that resolves to the newly created {@link ContentManagementContent}.
86 | *
87 | * @example
88 | * ```typescript
89 | * const content = await client.management.content.create({
90 | * name: {
91 | * $invariant: '',
92 | * },
93 | * contentTypeAlias: '',
94 | * parentId: '',
95 | * })
96 | * ```
97 | *
98 | * If the Content Type includes an `Upload` or an `Image Cropper` property and you want to upload a file you need to pass a `FormData` object to the function instead,
99 | *
100 | * ```typescript
101 | * import FormData from `form-data`
102 | * import fs from 'fs'
103 | * import path from 'path'
104 | *
105 | * const data = new FormData()
106 | *
107 | * data.append(JSON.stringify({
108 | * name: {
109 | * $invariant: '',
110 | * },
111 | * contentTypeAlias: '',
112 | * parentId: '',
113 | * // if myFile is of type `Upload` and is culture variant
114 | * myFile: {
115 | * 'en-US': 'my-file.txt',
116 | * },
117 | * // if myImage is of type `Image Cropper` and is culture invariant
118 | * myImage: {
119 | * $invariant: {
120 | * src: 'my-image.jpg',
121 | * },
122 | * },
123 | * }))
124 | *
125 | * data.append('myFile.en-US', fs.createReadStream(path.join(__dirname, 'my-file.txt')))
126 | * data.append('myImage.$invariant', fs.createReadStream(path.join(__dirname, 'my-image.txt')))
127 | *
128 | * const content = await client.management.content.create(data)
129 | * ```
130 | *
131 | * See {@link https://our.umbraco.com/documentation/Umbraco-Heartcore/API-Documentation/Content-Management/content/#create-content} for more info on the structure of the document.
132 | */
133 | async create (body: ContentManagementContentRequest | FormData) {
134 | return this.makeRequest(Endpoints.management.content.create(), body)
135 | }
136 |
137 | /**
138 | * Publish a Content item.
139 | * @param id - GUID id of the Content item.
140 | * @param options - Request options. See {@link APIContentPublishOptions}.
141 | * @returns a `Promise` that resolves to a {@link ContentManagementContent} if found, otherwise `undefined`.
142 | */
143 | async publish (id: string, options?: APIContentPublishOptions) {
144 | try {
145 | return await this.makeRequest(Endpoints.management.content.publish(id, options))
146 | } catch (err) {
147 | if (err.response && err.response.status === 404) {
148 | return undefined
149 | }
150 | throw err
151 | }
152 | }
153 |
154 | /**
155 | * Unpublish a Content item.
156 | * @param id - GUID id of the Content item.
157 | * @param options - Request options. See {@link APIContentPublishOptions}.
158 | * @returns a `Promise` that resolves to a {@link ContentManagementContent} if found, otherwise `undefined`.
159 | */
160 | async unPublish (id: string, options?: APIContentUnpublishOptions) {
161 | try {
162 | return await this.makeRequest(Endpoints.management.content.unPublish(id, options))
163 | } catch (err) {
164 | if (err.response && err.response.status === 404) {
165 | return undefined
166 | }
167 | throw err
168 | }
169 | }
170 |
171 | /**
172 | * Update a Content item.
173 | * @param id - GUID id of the Content item.
174 | * @param body - Content to update, must be a complete Content item including all cultures. See {@link ContentManagementContentRequest}.
175 | * @returns a `Promise` that resolves to a {@link ContentManagementContent} of the updated Content item if found, otherwise `undefined`.
176 | *
177 | * @example
178 | * ```typescript
179 | * const content = await client.management.content.update('', {
180 | * name: {
181 | * $invariant: '',
182 | * },
183 | * contentTypeAlias: '',
184 | * parentId: '',
185 | * })
186 | * ```
187 | *
188 | * If the Content Type includes an `Upload` or an `Image Cropper` property and you want to upload a file you need to pass a `FormData` object to the function instead,
189 | *
190 | * ```typescript
191 | * import FormData from `form-data`
192 | * import fs from 'fs'
193 | * import path from 'path'
194 | *
195 | * const data = new FormData()
196 | *
197 | * data.append(JSON.stringify({
198 | * name: {
199 | * $invariant: '',
200 | * },
201 | * contentTypeAlias: '',
202 | * parentId: '',
203 | * // if myFile is of type `Upload` and is culture variant
204 | * myFile: {
205 | * 'en-US': 'my-file.txt',
206 | * },
207 | * // if myImage is of type `Image Cropper` and is culture invariant
208 | * myImage: {
209 | * $invariant: {
210 | * src: 'my-image.jpg',
211 | * },
212 | * },
213 | * }))
214 | *
215 | * data.append('myFile.en-US', fs.createReadStream(path.join(__dirname, 'my-file.txt')))
216 | * data.append('myImage.$invariant', fs.createReadStream(path.join(__dirname, 'my-image.txt')))
217 | *
218 | * const content = await client.management.content.update('', data)
219 | * ```
220 | *
221 | * See {@link https://our.umbraco.com/documentation/Umbraco-Heartcore/API-Documentation/Content-Management/content/#update-content} for more info on the structure of the document.
222 | */
223 | async update (id: string, body: ContentManagementContentRequest | FormData) {
224 | try {
225 | return await this.makeRequest(Endpoints.management.content.update(id), body)
226 | } catch (err) {
227 | if (err.response && err.response.status === 404) {
228 | return undefined
229 | }
230 | throw err
231 | }
232 | }
233 |
234 | /**
235 | * Delete a Content item.
236 | * @param id - GUID id of the Content item.
237 | * @returns a `Promise` that resolves to a {@link ContentManagementContent} of the deleted Content item if found, otherwise `undefined`.
238 | */
239 | async delete (id: string) {
240 | try {
241 | return await this.makeRequest(Endpoints.management.content.delete(id))
242 | } catch (err) {
243 | if (err.response && err.response.status === 404) {
244 | return undefined
245 | }
246 | throw err
247 | }
248 | }
249 | }
250 |
--------------------------------------------------------------------------------
/src/Clients/Management/ManagementClient.ts:
--------------------------------------------------------------------------------
1 | import { Client } from '../../Client'
2 | import { ContentManagementClient } from './ContentManagementClient'
3 | import { MediaManagementClient } from './MediaManagementClient'
4 | import { MemberManagementClient } from './MemberManagementClient'
5 | import { Endpoint } from '../../Endpoint'
6 | import { Endpoints } from '../../Endpoints'
7 | import {
8 | ContentLanguageType,
9 | ContentMemberCreateGroupType,
10 | ContentMemberGroupType,
11 | ContentMemberTypeType,
12 | ContentTypeBase,
13 | ContentRelationType,
14 | ContentRelationTypeType,
15 | CreateContentLanguageType,
16 | MediaTypeContentManager,
17 | Form
18 | } from '../../Responses'
19 |
20 | /**
21 | * ManagementClient is used to access the Content Management API.
22 | * @public
23 | *
24 | * @example
25 | * The {@link ManagementClient} must be accessed through {@link Client}.
26 | *
27 | * ```typescript
28 | * import { Client } from '@umbraco/headless-client'
29 | *
30 | * const client = new Client({
31 | * projectAlias: '',
32 | * apiKey: '',
33 | * language: '',
34 | * })
35 | *
36 | * const managementClient = client.management
37 | * ```
38 | */
39 | export class ManagementClient {
40 | /**
41 | * @internal
42 | */
43 | constructor (
44 | private readonly client: Client
45 | ) {
46 |
47 | }
48 |
49 | private readonly makeRequest = async (endpoint: Endpoint, data?: any) => {
50 | return this.client.makeRequest(endpoint, data)
51 | }
52 |
53 | /**
54 | * The Content client for the Content Management API.
55 | * See {@link ContentManagementClient}
56 | */
57 | public readonly content = new ContentManagementClient(this.client)
58 |
59 | /**
60 | * The Media client for the Media Management API.
61 | * See {@link MediaManagementClient}
62 | */
63 | public readonly media = new MediaManagementClient(this.client)
64 |
65 | /**
66 | * The Member client for the Member Management API.
67 | * See {@link MemberManagementClient}
68 | */
69 | public readonly member = new MemberManagementClient(this.client)
70 |
71 | /**
72 | * ContentType API
73 | */
74 | get contentType () {
75 | return {
76 | /**
77 | * Fetch all content types
78 | */
79 | all: async() => this.makeRequest(Endpoints.management.contentType.all()),
80 |
81 | /**
82 | * Find content type by alias
83 | * @param alias Alias for the content type
84 | */
85 | byAlias: async (alias: string) : Promise => this.makeRequest(Endpoints.management.contentType.byAlias(alias))
86 | }
87 | }
88 |
89 | /**
90 | * Media API
91 | */
92 | get mediaType () {
93 | return {
94 | /**
95 | * Fetch all media types
96 | */
97 | all: async () : Promise => this.makeRequest(Endpoints.management.mediaType.all()),
98 |
99 | /**
100 | * Find media type by alias
101 | * @param alias Alias of the media type querying for
102 | */
103 | byAlias: async (alias: string) : Promise => this.makeRequest(Endpoints.management.mediaType.byAlias(alias))
104 | }
105 | }
106 |
107 | /**
108 | * Language API
109 | */
110 | get language () {
111 | return {
112 | /**
113 | * Fetch all languages
114 | */
115 | all: async() => this.makeRequest(Endpoints.management.language.all()),
116 |
117 | /**
118 | * Find language by ISO code
119 | * @param id ISO Code for the language (e.g. en-US)
120 | */
121 | byISOCode: async(id: string) => this.makeRequest(Endpoints.management.language.byISOCode(id)),
122 |
123 | /**
124 | * Create a language
125 | * @param data Data for creating language object
126 | */
127 | create: async(data: CreateContentLanguageType) => this.makeRequest(Endpoints.management.language.create(), data),
128 |
129 | /**
130 | * Update a language
131 | * @param id ISO Code for the language (e.g. en-US)
132 | * @param data Data for updating language object
133 | */
134 | update: async(id: string, data: CreateContentLanguageType) => this.makeRequest(Endpoints.management.language.update(id), data),
135 |
136 | /**
137 | * Delete a language
138 | * @param id ISO Code for the language (e.g. en-US)
139 | */
140 | delete: async(id: string) => this.makeRequest(Endpoints.management.language.delete(id))
141 |
142 | }
143 | }
144 |
145 | /**
146 | * Relation API
147 | */
148 | get relation () {
149 | return {
150 | /**
151 | * Find relation by id
152 | * @param id GUID part of an Umbraco UDI
153 | */
154 | byId: async (id: string) : Promise => this.makeRequest(Endpoints.management.relation.byId(id)),
155 |
156 | /**
157 | * Find relation by alias
158 | * @param alias Alias of the relation querying for
159 | */
160 | byAlias: async (alias: string) : Promise => this.makeRequest(Endpoints.management.relation.byAlias(alias)),
161 |
162 | /**
163 | * Fetch child for relation with id
164 | * @param id GUID part of an Umbraco UDI
165 | */
166 | byChild: async (id: string) : Promise => this.makeRequest(Endpoints.management.relation.byChild(id)),
167 |
168 | /**
169 | * Fetch parent for relation with id
170 | * @param id GUID part of an Umbraco UDI
171 | */
172 | byParent: async (id: string) : Promise => this.makeRequest(Endpoints.management.relation.byParent(id)),
173 |
174 | /**
175 | * Create a relation
176 | * @param data Data for creating relation object
177 | */
178 | create: async (data: any) : Promise => this.makeRequest(Endpoints.management.relation.create(), data),
179 |
180 | /**
181 | * Delete relation with id
182 | * @param id GUID part of an Umbraco UDI
183 | */
184 | delete: async (id: string) : Promise => this.makeRequest(Endpoints.management.relation.delete(id))
185 | }
186 | }
187 |
188 | /**
189 | * RelationType API
190 | */
191 | get relationType () {
192 | return {
193 | /**
194 | * Fetch relation type by alias
195 | * @param alias Alias for the relation type queryed for
196 | */
197 | byAlias: async (alias: string) : Promise => this.makeRequest(Endpoints.management.relationType.byAlias(alias))
198 | }
199 | }
200 |
201 | /**
202 | * MemberGroup API
203 | */
204 | get memberGroup () {
205 | return {
206 | /**
207 | * Fetch member group by name
208 | * @param name The name of the group
209 | */
210 | byName: async (name: string) => this.makeRequest(Endpoints.management.memberGroup.byName(name)),
211 |
212 | /**
213 | * Create a member group
214 | * @param data Data for creating a member group
215 | */
216 | create: async (data: ContentMemberCreateGroupType) => this.makeRequest(Endpoints.management.memberGroup.create(), data),
217 |
218 | /**
219 | * Delete member group
220 | * @param name Name of the member group to be removed
221 | */
222 | delete: async (name: string) => this.makeRequest(Endpoints.management.memberGroup.delete(name))
223 | }
224 | }
225 |
226 | /**
227 | * MemberType API
228 | */
229 | get memberType () {
230 | return {
231 | /**
232 | * Fetch all member types
233 | */
234 | all: async() => this.makeRequest(Endpoints.management.memberType.all()),
235 |
236 | /**
237 | * Find by alias
238 | * @param alias Alias for the member type to be found.
239 | */
240 | byAlias: async(alias: string) => this.makeRequest(Endpoints.management.memberType.byAlias(alias))
241 | }
242 | }
243 |
244 | /**
245 | * Forms API
246 | */
247 | get forms () {
248 | return {
249 | /**
250 | * Fetch all forms
251 | */
252 | all: async () : Promise