diff --git a/hmdb/src/main/resources/controllers/_graphql.js b/hmdb/src/main/resources/controllers/_graphql.js index a4881a2d..2de80bc1 100644 --- a/hmdb/src/main/resources/controllers/_graphql.js +++ b/hmdb/src/main/resources/controllers/_graphql.js @@ -1,16 +1,7 @@ const guillotineLib = require('/lib/guillotine'); const graphqlPlaygroundLib = require('/lib/graphql-playground'); -const libGraphQl = require('/lib/graphql'); -const schema = require('../guillotine/schema/schema'); +const { CORS_HEADERS } = require("../lib/headless/cors-headers"); -//────────────────────────────────────────────────────────────────────────────── -// Constants -//────────────────────────────────────────────────────────────────────────────── -const CORS_HEADERS = { - 'Access-Control-Allow-Headers': 'Content-Type', - 'Access-Control-Allow-Methods': 'POST, OPTIONS', - 'Access-Control-Allow-Origin': '*' -}; //────────────────────────────────────────────────────────────────────────────── // Methods @@ -39,21 +30,20 @@ exports.get = function (req) { }; }; -//const schema = guillotineLib.createSchema(); exports.post = function (req) { - let input = JSON.parse(req.body); - //log.info("--------------> Query:\n" + input.query); - const result = libGraphQl.execute(schema, input.query, input.variables); - // log.info("<-------------- Result:{\n" + JSON.stringify(result, null, 2)); + let params = { + query: input.query, + variables: input.variables + }; return { contentType: 'application/json', headers: CORS_HEADERS, - body: result + body: guillotineLib.execute(params) }; }; diff --git a/hmdb/src/main/resources/controllers/contentapi/_content.es6 b/hmdb/src/main/resources/controllers/contentapi/_content.es6 index 7c84fa2f..fb60d1ea 100644 --- a/hmdb/src/main/resources/controllers/contentapi/_content.es6 +++ b/hmdb/src/main/resources/controllers/contentapi/_content.es6 @@ -4,6 +4,14 @@ const portalLib = require('/lib/xp/portal'); const { getContentData } = require('../../lib/headless/contentapi/contentdata'); +const { CORS_HEADERS } = require("../../lib/headless/cors-headers"); + +exports.options = function () { + return { + contentType: 'text/plain;charset=utf-8', + headers: CORS_HEADERS + }; +}; const handlePost = (req) => { // query: HIGHLY RECOMMENDED: supply a query to override the fallback catch-all query with a BETTER SCALING, content-type-specific one. @@ -33,4 +41,4 @@ const handlePost = (req) => { exports.post = handlePost; // FIXME: only for testing, remove. - exports.get = handlePost; + //exports.get = handlePost; diff --git a/hmdb/src/main/resources/controllers/contentapi/_contentbase.es6 b/hmdb/src/main/resources/controllers/contentapi/_contentbase.es6 index 124f7719..481d710a 100644 --- a/hmdb/src/main/resources/controllers/contentapi/_contentbase.es6 +++ b/hmdb/src/main/resources/controllers/contentapi/_contentbase.es6 @@ -4,6 +4,21 @@ const portalLib = require('/lib/xp/portal'); const { getContentBase } = require('../../lib/headless/contentapi/contentbase'); +const { CORS_HEADERS } = require("../../lib/headless/cors-headers"); + +log.info("CORS_HEADERS (" + + (Array.isArray(CORS_HEADERS) ? + ("array[" + CORS_HEADERS.length + "]") : + (typeof CORS_HEADERS + (CORS_HEADERS && typeof CORS_HEADERS === 'object' ? (" with keys: " + JSON.stringify(Object.keys(CORS_HEADERS))) : "")) + ) + "): " + JSON.stringify(CORS_HEADERS, null, 2) +); + +exports.options = function () { + return { + contentType: 'text/plain;charset=utf-8', + headers: CORS_HEADERS + }; +}; const handlePost = (req) => { // idOrPath (mandatory if no override query is used): used in the default query. Can be a valid content UUID, or a (full) content path, eg. /mysite/persons/someone. Can be supplied direct param as here, or as part of the variables param (direct param has prescendence) @@ -34,4 +49,4 @@ const handlePost = (req) => { exports.post = handlePost; // FIXME: only for testing, remove. - exports.get = handlePost; + //exports.get = handlePost; diff --git a/hmdb/src/main/resources/lib/headless/contentapi/contentbase.es6 b/hmdb/src/main/resources/lib/headless/contentapi/contentbase.es6 index 2a04c63d..0aee954f 100644 --- a/hmdb/src/main/resources/lib/headless/contentapi/contentbase.es6 +++ b/hmdb/src/main/resources/lib/headless/contentapi/contentbase.es6 @@ -32,6 +32,6 @@ exports.getContentBase = (siteId, branch, idOrPath, query, variables = {}, maxCh } return branchInvalidError400(branch) || - idOrPathOrQueryInvalidError400(variables, query, 'No query was provided, and no id or path (iOrPath)') || + idOrPathOrQueryInvalidError400(variables, query, 'No query was provided, and no id or path (idOrPath)') || executeResult(siteId, branch, query || getContentBaseQuery(variables.maxChildren), variables); }; diff --git a/hmdb/src/main/resources/lib/headless/contentapi/execute.es6 b/hmdb/src/main/resources/lib/headless/contentapi/execute.es6 index 6ec9ea9d..dd8dc8ee 100644 --- a/hmdb/src/main/resources/lib/headless/contentapi/execute.es6 +++ b/hmdb/src/main/resources/lib/headless/contentapi/execute.es6 @@ -1,15 +1,18 @@ -const { contentNotFoundError404 } = require('validation'); +const {contentNotFoundError404} = require('validation'); const guillotineLib = require('/lib/guillotine'); +const {CORS_HEADERS} = require("../cors-headers"); + exports.executeResult = (siteId, branch, query, variables) => { // TODO: app repo is targeted - should that be overridable? - const content = guillotineLib.execute({ query, variables, siteId, branch }); + const content = guillotineLib.execute({query, variables, siteId, branch}); return contentNotFoundError404(content, variables, query) || { status: 200, body: content, contentType: 'application/json', + headers: CORS_HEADERS }; }; diff --git a/hmdb/src/main/resources/lib/headless/cors-headers.es6 b/hmdb/src/main/resources/lib/headless/cors-headers.es6 new file mode 100644 index 00000000..e1a4980d --- /dev/null +++ b/hmdb/src/main/resources/lib/headless/cors-headers.es6 @@ -0,0 +1,5 @@ +exports.CORS_HEADERS = { + 'Access-Control-Allow-Headers': 'Content-Type', + 'Access-Control-Allow-Methods': 'POST, OPTIONS', + 'Access-Control-Allow-Origin': '*' +}; diff --git a/next/enonic.connection.config.js b/next/enonic.connection.config.js new file mode 100644 index 00000000..ef78ad65 --- /dev/null +++ b/next/enonic.connection.config.js @@ -0,0 +1,38 @@ +const project = 'hmdb'; // <-- project identifier in path, e.g. 'default' in the URL /site/default/master +const appKey = "com.enonic.nextpoc.hmdb"; // <-- full app key = appName in hmdb/gradle.properties + +const appKeyUnderscored = appKey.replace(/\./g, '_'); +const appKeyDashed = appKey.replace(/\./g, '-'); + +const apiDomain = "http://localhost:8080"; + +const apiContentBase = '_contentbase'; +const apiContentFull = '_content'; + +const siteRootUrlMaster = `${apiDomain}/site/${project}/master`; +const siteRootUrlDraft = `${apiDomain}/site/${project}/draft`; + +// appName is the content _name of the root site content-item: +const contentApiUrlGetters = { + master: { + base: (appName) => `${siteRootUrlMaster}/${appName}/${apiContentBase}`, + full: (appName) => `${siteRootUrlMaster}/${appName}/${apiContentFull}` + }, + draft: { + base: (appName) => `${siteRootUrlDraft}/draft/${appName}/${apiContentBase}`, + full: (appName) => `${siteRootUrlDraft}/draft/${appName}/${apiContentFull}` + } +}; + +module.exports = { + project, + appKey, + appKeyUnderscored, + appKeyDashed, + apiDomain, + apiContentBase, + apiContentFull, + siteRootUrlDraft, + siteRootUrlMaster, + contentApiUrlGetters +}; diff --git a/next/enonic.connection.config.ts b/next/enonic.connection.config.ts deleted file mode 100644 index ca44befc..00000000 --- a/next/enonic.connection.config.ts +++ /dev/null @@ -1,25 +0,0 @@ -export const project = 'hmdb'; // <-- project identifier in path, e.g. 'default' in the URL /site/default/master -export const appKey = "com.enonic.nextpoc.hmdb"; // <-- full app key = appName in hmdb/gradle.properties - -export const appNameUnderscored = appKey.replace(/\./g, '_'); -export const appNameDashed = appKey.replace(/\./g, '-'); - -export const apiDomain = "http://localhost:8080"; - -export const apiContentBase = '_contentbase'; -export const apiContentFull = '_content'; - -export const siteRootUrlMaster = `${apiDomain}/site/${project}/master`; -export const siteRootUrlDraft = `${apiDomain}/site/${project}/draft`; - -// appName is the content _name of the root site content-item: -export const contentApiUrlGetters = { - master: { - base: (appName) => `${siteRootUrlMaster}/${appName}/${apiContentBase}`, - full: (appName) => `${siteRootUrlMaster}/${appName}/${apiContentFull}` - }, - draft: { - base: (appName) => `${siteRootUrlDraft}/draft/${appName}/${apiContentBase}`, - full: (appName) => `${siteRootUrlDraft}/draft/${appName}/${apiContentFull}` - } -}; diff --git a/next/next.config.js b/next/next.config.js index 0d607100..fd2dc79d 100644 --- a/next/next.config.js +++ b/next/next.config.js @@ -1,3 +1,26 @@ +const {apiDomain} = require("./enonic.connection.config"); + module.exports = { reactStrictMode: true, + async headers() { + return [ + { + source: '/:contentPath*', + headers: [ + { + key: 'Access-Control-Allow-Origin', + value: "*", + }, + { + key: 'Access-Control-Allow-Headers', + value: "Content-Type", + }, + { + key: 'Access-Control-Allow-Methods', + value: "GET,POST,OPTIONS", + }, + ], + }, + ] + }, } diff --git a/next/src/pages/[[...contentPath]].tsx b/next/src/pages/[[...contentPath]].tsx index b8307619..982f2ca2 100644 --- a/next/src/pages/[[...contentPath]].tsx +++ b/next/src/pages/[[...contentPath]].tsx @@ -2,6 +2,7 @@ import {fetchContent} from "../shared/data"; import {contentApiUrlGetters} from "../../enonic.connection.config"; import Custom500 from './500'; import Custom404 from './404'; +import React, { useState, useEffect } from 'react'; const { full: getContentFullUrl, @@ -28,59 +29,80 @@ type ContentApiBaseBody = { }; -const Page = ({error, contentBase}) => { - if (error) { +const Page = ({error, contentBase, freshen}) => { + /*if (error) { switch (error.code) { case 404: - return + return case 500: - return ; + return ; } - } + }*/ // TODO: general fallback page. Resolve specific pages above - return

{JSON.stringify(contentBase)}

; + return
+

ContentBase: {JSON.stringify(contentBase)}

+

Error: {JSON.stringify(error)}

+
; }; +const Main = () => { + const [props, setProps] = useState({error: {}, contentBase: {}}); + + const freshen = async () => { + const p = await fetchContentBase(['hmdb', 'persons', 'keanu-reeves']); + + console.log("p:", p); + + // @ts-ignore + setProps(() => p); + }; + + return ; +} // this function also needs some serious refactoring, but for a quick and dirty // proof of concept it does the job. -export const getServerSideProps = async ({params}: Context) => { - const idOrPath = "/" + params.contentPath.join("/"); +/* - const appName = params.contentPath[0]; - const contentFullUrl = getContentFullUrl(appName); +export const getServerSideProps = async ({params}: Context) => ({ + props: await fetchContentBase(params.contentPath) +}); +*/ + +export const fetchContentBase = async (contentPath: string[]) => { + const idOrPath = "/" + contentPath.join("/"); + const appName = contentPath[0]; + //const contentFullUrl = getContentFullUrl(appName); const contentBaseUrl = getContentBaseUrl(appName); const body: ContentApiBaseBody = {idOrPath}; - const contentBase = await fetchContent( + const result = await fetchContent( contentBaseUrl, body ) .then(json => { - if (!(json?.data?.guillotine || {}).get) { + if (!json?.data?.guillotine?.get) { console.error('Data fetched from contentBase API:', json); - return { props: { error: { code: 404} } }; + return { error: { code: 404 }}; } }) + .then(validJson => ({ + // @ts-ignore + contentBase: validJson.data.guillotine.get + })) .catch((err) => { return { - props: { - error: { - code: 500, - message: err.message - } + error: { + code: 500, + message: err.message } }; }); - - return { - props: { - contentBase, - }, - }; + return result; }; -export default Page; +export default Main; + diff --git a/next/src/pages/rendering/client/index.tsx b/next/src/pages/rendering/client/index.tsx new file mode 100644 index 00000000..95e87001 --- /dev/null +++ b/next/src/pages/rendering/client/index.tsx @@ -0,0 +1,124 @@ +import Head from "next/head"; +import { useState } from "react"; +import * as React from "react"; + +import {PersonList} from "../../../shared/data/queries/getPersons"; +import {MovieList} from "../../../shared/data/queries/getMovies"; +import {fetchContentBase} from "../../[[...contentPath]]"; +//import {fetchPersons} from "../../persons"; +//import {fetchMovies} from "../../movies"; + + +export type Timestamped = { + data: T + timestamp: string; +}; +const timestamp = async (data: T): Promise> => ({ + data, + timestamp: new Date().toISOString(), +}); + + + +/*export const fetchStampedPersons = async (): Promise> => { + const persons: PersonList = await fetchPersons(); + return await timestamp(persons); +} +export const fetchStampedMovies = async (): Promise> => { + const movies: MovieList = await fetchMovies(); + return await timestamp(movies); +}*/ + +const Page: React.FC = () => { + return ( +
+ + Client-side: Next.js data poc + +

Client-side

+

+ This page contains dynamically rendered data. To fetch (or refetch) + data, click the appropriate button below. +

+ fetchContentBase(['/hmdb/persons/keanu-reeves'])} sectionName={"People"} /> + fetchContentBase(['/hmdb/movies/the-matrix'])} sectionName={"Movies"} /> +
+ ); +}; + +type DataList = PersonList | MovieList; + +type DataDisplayProps = { + fetchData: () => Promise>; + sectionName: string; +}; + +type RemoteData = + | { status: "NotAsked"; data: undefined, timestamp: undefined } + | { status: "Loading"; data?: DataList, timestamp: undefined } + | { status: "Success"; data: DataList, timestamp: string } + | { status: "Error"; message: string; data?: DataList, timestamp: undefined }; + +const DataDisplay: React.FC = ({ + fetchData, + sectionName, +}) => { + const [remoteData, setRemoteData] = useState({ + status: "NotAsked", + data: undefined, + timestamp: undefined + }); + + const getData = () => { + setRemoteData({ status: "Loading", data: remoteData.data, timestamp: undefined}); + fetchData() + .then((data) => { + setRemoteData({ status: "Success", data: data.data, timestamp: data.timestamp }); + }) + .catch((err) => { + setRemoteData({ + status: "Error", + message: err.message, + data: remoteData.data, + timestamp: undefined + }); + }); + }; + + return ( +
+

{sectionName}

+ + {remoteData.status === "Error" && ( +

+ There was an error when fetching data, but there may still be some + data displayed below. The error was: {remoteData.message} +

+ )} + {!remoteData.data ? ( +

There is no data available right now. How about fetching some?

+ ) : ( + <> + { + remoteData.timestamp && +

+ I got this data at{" "} + +

+ } +
    + {remoteData.data.map((p) => ( +
  • {p.displayName}
  • + ))} +
+ + )} +
+ ); +}; + +export default Page; diff --git a/next/src/pages_old/movies/[movieSubPath].tsx b/next/src/pages_old/movies/[movieSubPath].tsx index ea2e3f06..8da9eff6 100644 --- a/next/src/pages_old/movies/[movieSubPath].tsx +++ b/next/src/pages_old/movies/[movieSubPath].tsx @@ -1,7 +1,7 @@ import {GetServerSideProps, GetStaticProps} from "next"; import {fetchContentGet} from "../../shared/data"; import getMovieQuery, {Movie} from "../../shared/data/queries/getMovie"; -import {appNameUnderscored} from "../../../enonic.connection.config"; +import {appKeyUnderscored} from "../../../enonic.connection.config"; import MoviePage from "../../components/templates/movie"; @@ -21,7 +21,7 @@ export default Page; export const fetchMovie = async (personSubPath): Promise => { - const movieQuery = getMovieQuery(appNameUnderscored, personSubPath); + const movieQuery = getMovieQuery(appKeyUnderscored, personSubPath); return fetchContentGet(movieQuery); } diff --git a/next/src/pages_old/movies/index.tsx b/next/src/pages_old/movies/index.tsx index 2df37077..861d38f3 100644 --- a/next/src/pages_old/movies/index.tsx +++ b/next/src/pages_old/movies/index.tsx @@ -1,7 +1,7 @@ import {GetServerSideProps, GetStaticProps} from "next"; import {fetchContentChildren} from "../../shared/data"; import getMoviesQuery, {MovieList} from "../../shared/data/queries/getMovies"; -import {appNameUnderscored} from "../../../enonic.connection.config"; +import {appKeyUnderscored} from "../../../enonic.connection.config"; import ListPage from "../../components/templates/list"; type Props = { @@ -16,7 +16,7 @@ export default Page; -const moviesQuery = getMoviesQuery(appNameUnderscored); +const moviesQuery = getMoviesQuery(appKeyUnderscored); export const fetchMovies = async (): Promise => fetchContentChildren(moviesQuery); diff --git a/next/src/pages_old/persons/[personSubPath].tsx b/next/src/pages_old/persons/[personSubPath].tsx index 21a58d6d..7ae8c798 100644 --- a/next/src/pages_old/persons/[personSubPath].tsx +++ b/next/src/pages_old/persons/[personSubPath].tsx @@ -1,7 +1,7 @@ import {GetServerSideProps, GetStaticProps} from "next"; import {fetchContentGet} from "../../shared/data"; import getPersonQuery, {Person} from "../../shared/data/queries/getPerson"; -import {appNameUnderscored} from "../../../enonic.connection.config"; +import {appKeyUnderscored} from "../../../enonic.connection.config"; import PersonPage from "../../components/templates/person"; type Props = { @@ -20,7 +20,7 @@ export default Page; export const fetchPerson = async (personSubPath): Promise => { - const personQuery = getPersonQuery(appNameUnderscored, personSubPath); + const personQuery = getPersonQuery(appKeyUnderscored, personSubPath); return fetchContentGet(personQuery); } diff --git a/next/src/pages_old/persons/index.tsx b/next/src/pages_old/persons/index.tsx index 6d5b8e15..d9bbb433 100644 --- a/next/src/pages_old/persons/index.tsx +++ b/next/src/pages_old/persons/index.tsx @@ -1,7 +1,7 @@ import {GetServerSideProps, GetStaticProps} from "next"; import {fetchContentChildren} from "../../shared/data"; import getPersonsQuery, {PersonList} from "../../shared/data/queries/getPersons"; -import {appNameUnderscored} from "../../../enonic.connection.config"; +import {appKeyUnderscored} from "../../../enonic.connection.config"; import ListPage from '../../components/templates/list'; @@ -17,7 +17,7 @@ export default Page; -const personsQuery = getPersonsQuery(appNameUnderscored); +const personsQuery = getPersonsQuery(appKeyUnderscored); export const fetchPersons = async (): Promise => fetchContentChildren(personsQuery);