Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff731a8755
|
No files matched your search
@@ -1,12 +1,12 @@
|
||||
version: 2.1
|
||||
setup: true
|
||||
orbs:
|
||||
qqq-orb: kingsrook/qqq-orb@0.8
|
||||
qqq-orb: kingsrook/qqq-orb@0.7.12
|
||||
workflows:
|
||||
setup:
|
||||
jobs:
|
||||
- qqq-orb/generate_pipeline:
|
||||
orb_version: '0.8'
|
||||
orb_version: 0.7.12
|
||||
filters:
|
||||
branches:
|
||||
ignore: automation/qbit-rc-pending
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
name: build
|
||||
on:
|
||||
push:
|
||||
jobs:
|
||||
maven:
|
||||
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/maven.yaml@v1
|
||||
secrets: inherit
|
||||
@@ -1,5 +1,5 @@
|
||||
profile: qbit
|
||||
orb_version: '0.8'
|
||||
orb_version: 0.7.12
|
||||
default_branch: develop
|
||||
java_version: '21'
|
||||
browsers: true
|
||||
|
||||
@@ -18,15 +18,6 @@ This file provides guidance to Codex when working with code in this repository.
|
||||
|
||||
**Entry point:** `CmsQBitProducer implements QBitMetaDataProducer<CmsQBitConfig>` in `com.kingsrook.qbits.cms`. The producer builds every table, possible-value source, app and widget itself (no package scanning); `CmsFeature` flags switch feature areas on and off. The schema is a prefix-parameterized Liquibase changelog the host includes and runs (`src/main/resources/qbit-cms/db/changelog/`); the qbit never runs DDL.
|
||||
|
||||
Packages:
|
||||
|
||||
- `CmsQBitConfig`, `CmsFeature`, `CmsTables` (logical names and table definitions), `CmsRuntime`, `CmsSchemaCheck` (startup check), `CmsStartupMode`.
|
||||
- `core/model`: CORE entities. Their `TABLE_NAME` and possible-value source names are logical; the builder adds the prefix.
|
||||
- `metadata`: `CmsTableBuilder` (tables, possible values, joins, child lists, associations, permission rules), `CmsAppBuilder` and `CmsAreas` (apps and homes).
|
||||
- `home`: `CmsHomeRenderer` and `CmsHtml` (one renderer for every home; reads run with `PermissionEnforcement.ENFORCE`).
|
||||
- `security`: `CmsRole`, `CmsSecurity`, `CmsOpenReadsPermissionChecker`.
|
||||
- Schema: `src/main/resources/qbit-cms/db/changelog/qbit-cms.changelog.yaml`. Its changesets are append-only once released, and `docs/ddl/postgres.sql` is generated from it.
|
||||
|
||||
## Naming (owner rule)
|
||||
|
||||
- Tables are lowercase snake_case prefixed with the full qbit name: `cms_<name>`. The QQQ table name and the backend (SQL) name are identical; possible-value sources take the table name.
|
||||
|
||||
@@ -5,13 +5,4 @@ All notable changes to this project are documented here. The format follows Keep
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- Repository scaffold: build, CI and repository standard files.
|
||||
- `CmsQBitProducer` and `CmsQBitConfig`: table prefix `cms_`, name prefix `cms`, permission base `cms`, namespaces for a second install, validation, and a collision check against names already on the instance.
|
||||
- `CmsFeature` flags with dependencies. `CORE` is implemented; the other features are rejected until they ship.
|
||||
- CORE tables as standard QQQ tables: site, domain, redirect, redirect type, page, page type, page section, page section type, snippet, menu and menu item. Each comes with a possible-value source, child lists on parent records, associations (`page.sections`, `menu.items`) and field audit when the host has QQQ audit tables.
|
||||
- The CMS app with one home per area (Sites, Pages, Navigation). The homes show only real data read as the user and are readable at any width.
|
||||
- Permissions: per-table rules, `CmsRole` bundles, `CmsSecurity.applySessionPermissions`, and `openReads`.
|
||||
- Startup schema check (`startupMode` `FAIL_FAST`, `DEGRADED`, `SKIP`).
|
||||
|
||||
### Schema changes
|
||||
- `1.0.0` baseline: a prefix-parameterized Liquibase changelog (`qbit-cms/db/changelog/qbit-cms.changelog.yaml`, logical path `qbit-cms/changelog`). It creates the CORE tables with the kof22 column types, adds their foreign keys and indexes, and seeds generic reference data. Feature sections are gated by `cms.features`, and a name collision halts the install. Reference DDL is in `docs/ddl/postgres.sql`.
|
||||
- Repository scaffold: `CmsQBitConfig` and `CmsQBitProducer` (table prefix `cms_`, permissions `cms.*`), build, CI and repository standard files.
|
||||
@@ -18,15 +18,6 @@ This file provides guidance to Claude Code when working with code in this reposi
|
||||
|
||||
**Entry point:** `CmsQBitProducer implements QBitMetaDataProducer<CmsQBitConfig>` in `com.kingsrook.qbits.cms`. The producer builds every table, possible-value source, app and widget itself (no package scanning); `CmsFeature` flags switch feature areas on and off. The schema is a prefix-parameterized Liquibase changelog the host includes and runs (`src/main/resources/qbit-cms/db/changelog/`); the qbit never runs DDL.
|
||||
|
||||
Packages:
|
||||
|
||||
- `CmsQBitConfig`, `CmsFeature`, `CmsTables` (logical names and table definitions), `CmsRuntime`, `CmsSchemaCheck` (startup check), `CmsStartupMode`.
|
||||
- `core/model`: CORE entities. Their `TABLE_NAME` and possible-value source names are logical; the builder adds the prefix.
|
||||
- `metadata`: `CmsTableBuilder` (tables, possible values, joins, child lists, associations, permission rules), `CmsAppBuilder` and `CmsAreas` (apps and homes).
|
||||
- `home`: `CmsHomeRenderer` and `CmsHtml` (one renderer for every home; reads run with `PermissionEnforcement.ENFORCE`).
|
||||
- `security`: `CmsRole`, `CmsSecurity`, `CmsOpenReadsPermissionChecker`.
|
||||
- Schema: `src/main/resources/qbit-cms/db/changelog/qbit-cms.changelog.yaml`. Its changesets are append-only once released, and `docs/ddl/postgres.sql` is generated from it.
|
||||
|
||||
## Naming (owner rule)
|
||||
|
||||
- Tables are lowercase snake_case prefixed with the full qbit name: `cms_<name>`. The QQQ table name and the backend (SQL) name are identical; possible-value sources take the table name.
|
||||
|
||||
@@ -5,101 +5,27 @@
|
||||
|
||||
> QQQ QBit: a headless CMS (sites, pages, menus, blog, media, projects, photos and publishing) as standard QQQ tables.
|
||||
|
||||
**Status:** design approved in [QRun-IO/qqq#1083](https://github.com/QRun-IO/qqq/pull/1083) (see [docs/DESIGN.md](docs/DESIGN.md)). The CORE feature is implemented; MEDIA, BLOG, PROJECTS, PHOTOS, PUBLISHING and STREAM are next. Nothing is published yet.
|
||||
|
||||
The CMS is extracted from the kof22 website, which runs it in production. Its data lives in ordinary QQQ tables in your own database, secured by QQQ permissions. It brings an admin app with one home per area, and a schema you run with your own Liquibase.
|
||||
**Status:** design approved: [QRun-IO/qqq#1083](https://github.com/QRun-IO/qqq/pull/1083) (see [docs/DESIGN.md](docs/DESIGN.md)); implementation in progress. Nothing is published yet.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Java 21.
|
||||
- QQQ 4.1 (built against `4.1.0-RC.4`) with an RDBMS backend: PostgreSQL 14+ in production, H2 for tests.
|
||||
- QQQ 4.1 (built against `4.1.0-RC.4`) with an RDBMS backend (PostgreSQL in production, H2 for tests).
|
||||
- QQQ Next UI 1.0 (`1.0.0-RC.14` or later). Material is not supported.
|
||||
- Liquibase in your migration pipeline (or the reference DDL in `docs/ddl/postgres.sql`).
|
||||
|
||||
## Install
|
||||
|
||||
1. **Add the dependency** `com.kingsrook.qbits:qbit-cms` (once released).
|
||||
2. **Run the schema.** Put the qbit jar on your Liquibase classpath and include its changelog. Set its properties *before* the include (the first definition wins):
|
||||
Not yet published. Once released, add `com.kingsrook.qbits:qbit-cms` and produce the qbit into your `QInstance`:
|
||||
|
||||
```yaml
|
||||
databaseChangeLog:
|
||||
- property: { name: cms.tablePrefix, value: cms_ } # must equal CmsQBitConfig.tableNamePrefix
|
||||
- property: { name: cms.features, value: CORE } # the features you install
|
||||
- include: { file: qbit-cms/db/changelog/qbit-cms.changelog.yaml }
|
||||
```
|
||||
```java
|
||||
new CmsQBitProducer()
|
||||
.withQBitConfig(new CmsQBitConfig().withBackendName("rdbms"))
|
||||
.produce(qInstance);
|
||||
```
|
||||
|
||||
The changelog never touches a table it did not create. If a table with one of its names already exists, it halts with a message telling you to choose another prefix. It seeds only generic reference data (redirect, page and section types); set `cms.seedReferenceData` to `false` to skip that.
|
||||
3. **Produce the qbit** after your backends (and the parent app, if any) are on the instance:
|
||||
## Naming
|
||||
|
||||
```java
|
||||
new CmsQBitProducer()
|
||||
.withQBitConfig(new CmsQBitConfig()
|
||||
.withBackendName("rdbms")
|
||||
.withRole("cms-admins", CmsRole.ADMIN)
|
||||
.withRole("cms-editors", CmsRole.EDITOR))
|
||||
.produce(qInstance);
|
||||
```
|
||||
|
||||
4. **Grant permissions** from your authentication customizer: `CmsSecurity.applySessionPermissions(session, userGroups, config)`.
|
||||
|
||||
At startup the producer checks that the changelog ran for every enabled feature and that the tables exist. `startupMode` `FAIL_FAST` (the default) refuses to start and lists what is missing. `DEGRADED` starts and shows the problem on the CMS home. `SKIP` does not check (unit tests without a database).
|
||||
|
||||
## Configuration reference (`CmsQBitConfig`)
|
||||
|
||||
| Setting | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `backendName` | required | The RDBMS backend for every CMS table |
|
||||
| `tableNamePrefix` | `cms_` | Table name prefix (QQQ and SQL names are equal); must equal the changelog's `cms.tablePrefix` |
|
||||
| `namePrefix` | `cms` | Prefix of app and widget names (`cmsPages`, `cmsPagesHome`) |
|
||||
| `permissionBaseName` | `cms` | Base of qbit-wide permissions (`cms.admin`, `cms.publish`) |
|
||||
| `features` | every available feature | `CmsFeature` set; `CORE` is required, and a feature this version does not implement yet is rejected |
|
||||
| `appName`, `appLabel`, `parentAppName` | `cms`, `CMS`, none | The top app and where it nests in your app tree |
|
||||
| `openReads` | `false` | When true, reading CMS tables needs no permission (writes always do) |
|
||||
| `startupMode` | `FAIL_FAST` | `FAIL_FAST`, `DEGRADED` or `SKIP` (see above) |
|
||||
| `roleMapping` | empty | Identity-provider group name to `CmsRole` |
|
||||
|
||||
A namespace (`withNamespace("siteTwo")`) fills in all three prefixes for a second install in the same instance (`site_two_page`, `siteTwoPages`, `site_two.admin`).
|
||||
|
||||
## Features
|
||||
|
||||
| Feature | Tables | Status |
|
||||
|---|---|---|
|
||||
| `CORE` | site, domain, redirect, redirect type, page, page type, page section, page section type, snippet, menu, menu item | available |
|
||||
| `MEDIA` | media, media type, storage SPI (`qbit-object-storage` for S3) | next |
|
||||
| `BLOG` | blog, blog type, post, tags, authors, post media, versions | next |
|
||||
| `PROJECTS`, `PHOTOS`, `PUBLISHING`, `STREAM`, `DATA_HEALTH_RULES` | see the design | planned |
|
||||
|
||||
## The admin
|
||||
|
||||
The qbit adds one app (`CMS`) with a home, and one sub-app per area, each with its own home:
|
||||
|
||||
- **Sites:** sites (domains are a list on each site) and redirects; Settings has redirect types.
|
||||
- **Pages:** pages (sections and child pages are lists on each page) and snippets; Settings has page types and page section types.
|
||||
- **Navigation:** menus (items are a list on each menu).
|
||||
|
||||
The nav shows only main tables and reference data; child tables are edited on their parents' records. Each home shows only real data read as the current user: counts, drafts, inactive rows and recently changed records, readable at any width in light and dark mode. The qbit never sets the instance's home app.
|
||||
|
||||
## Permissions
|
||||
|
||||
Every CMS table has QQQ permission rules. The table permissions are `<table>.read`, `.insert`, `.edit` and `.delete` (for example `cms_page.edit`). The qbit-wide permissions are `cms.admin` and `cms.publish`.
|
||||
|
||||
| Role | Grants |
|
||||
|---|---|
|
||||
| `ADMIN` | everything, including reference data, `cms.admin` and `cms.publish` |
|
||||
| `EDITOR` | read everything; write content tables |
|
||||
| `PUBLISHER` | `EDITOR` plus `cms.publish` |
|
||||
| `DRAFTER` | read everything; insert and edit content (no deletes, no publishing) |
|
||||
| `VIEWER` | read everything |
|
||||
|
||||
The qbit is built for QQQ core permission enforcement (`qInstance.setEnforcePermissionsInCoreActions(true)`, the default from QQQ 4.2).
|
||||
|
||||
## Operational tables
|
||||
|
||||
All CMS data is in normal, visible QQQ tables. Nothing is hidden and there is no side store. The schema is in `docs/ddl/postgres.sql` (generated from the changelog).
|
||||
|
||||
## Upgrading
|
||||
|
||||
Read the CHANGELOG's "Schema changes" before deploying. Run your migrations before the new app version. Released changesets never change; new versions only append changesets.
|
||||
Tables are `cms_<name>` (lowercase snake_case, backend name equal to the table name). Permissions are `cms.<table>.<action>` and `cms.<action>`.
|
||||
|
||||
## Adopting in another app
|
||||
|
||||
@@ -111,7 +37,7 @@ Follow the QQQ [adoption checklist](https://github.com/QRun-IO/qqq/blob/develop/
|
||||
./mvnw verify
|
||||
```
|
||||
|
||||
Always use the Maven wrapper (Maven 3.9.11); Maven 3.10 cannot read the parent POM. The PostgreSQL tests use Testcontainers and are skipped when Docker is not available.
|
||||
Always use the Maven wrapper (Maven 3.9.11); Maven 3.10 cannot read the parent POM.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
||||
@@ -1,372 +0,0 @@
|
||||
-- qbit-cms reference DDL (PostgreSQL), generated from the qbit's Liquibase changelog
|
||||
-- (src/main/resources/qbit-cms/db/changelog/qbit-cms.changelog.yaml) with cms.tablePrefix=cms_ and every feature on.
|
||||
-- For hosts that do not run Liquibase. The changelog is the source of truth; regenerate on schema changes.
|
||||
|
||||
|
||||
|
||||
CREATE TABLE public.cms_domain (
|
||||
id bigint NOT NULL,
|
||||
name character varying(40) NOT NULL,
|
||||
short_name character varying(20) NOT NULL,
|
||||
description character varying(255),
|
||||
fqdn character varying(255) NOT NULL,
|
||||
is_primary boolean NOT NULL,
|
||||
site_id bigint NOT NULL,
|
||||
create_date timestamp without time zone NOT NULL,
|
||||
modify_date timestamp without time zone NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE public.cms_domain ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
||||
SEQUENCE NAME public.cms_domain_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1
|
||||
);
|
||||
|
||||
CREATE TABLE public.cms_menu (
|
||||
id bigint NOT NULL,
|
||||
name character varying(100) NOT NULL,
|
||||
slug character varying(100) NOT NULL,
|
||||
description character varying(255),
|
||||
site_id bigint NOT NULL,
|
||||
menu_type character varying(50) NOT NULL,
|
||||
is_active boolean,
|
||||
create_date timestamp without time zone NOT NULL,
|
||||
modify_date timestamp without time zone NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE public.cms_menu ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
||||
SEQUENCE NAME public.cms_menu_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1
|
||||
);
|
||||
|
||||
CREATE TABLE public.cms_menu_item (
|
||||
id bigint NOT NULL,
|
||||
menu_id bigint NOT NULL,
|
||||
parent_id bigint,
|
||||
name character varying(100) NOT NULL,
|
||||
slug character varying(100) NOT NULL,
|
||||
item_type character varying(50) NOT NULL,
|
||||
link_url character varying(500),
|
||||
route_path character varying(200),
|
||||
target character varying(20),
|
||||
icon character varying(100),
|
||||
description character varying(255),
|
||||
display_order integer,
|
||||
is_active boolean,
|
||||
css_classes character varying(200),
|
||||
create_date timestamp without time zone NOT NULL,
|
||||
modify_date timestamp without time zone NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE public.cms_menu_item ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
||||
SEQUENCE NAME public.cms_menu_item_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1
|
||||
);
|
||||
|
||||
CREATE TABLE public.cms_page (
|
||||
id bigint NOT NULL,
|
||||
name character varying(100) NOT NULL,
|
||||
slug character varying(100) NOT NULL,
|
||||
title character varying(200) NOT NULL,
|
||||
content text,
|
||||
page_type_id bigint NOT NULL,
|
||||
template character varying(50),
|
||||
is_published boolean NOT NULL,
|
||||
publish_date timestamp without time zone,
|
||||
sort_order integer NOT NULL,
|
||||
parent_page_id bigint,
|
||||
site_id bigint NOT NULL,
|
||||
meta_description character varying(500),
|
||||
meta_keywords character varying(255),
|
||||
og_image character varying(255),
|
||||
create_date timestamp without time zone NOT NULL,
|
||||
modify_date timestamp without time zone NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE public.cms_page ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
||||
SEQUENCE NAME public.cms_page_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1
|
||||
);
|
||||
|
||||
CREATE TABLE public.cms_page_section (
|
||||
id bigint NOT NULL,
|
||||
name character varying(100) NOT NULL,
|
||||
css_class_name character varying(255),
|
||||
section_type_id bigint NOT NULL,
|
||||
content text,
|
||||
sort_order integer NOT NULL,
|
||||
page_id bigint NOT NULL,
|
||||
create_date timestamp without time zone NOT NULL,
|
||||
modify_date timestamp without time zone NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE public.cms_page_section ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
||||
SEQUENCE NAME public.cms_page_section_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1
|
||||
);
|
||||
|
||||
CREATE TABLE public.cms_page_section_type (
|
||||
id bigint NOT NULL,
|
||||
name character varying(100) NOT NULL,
|
||||
code character varying(50) NOT NULL,
|
||||
description character varying(255),
|
||||
is_active boolean NOT NULL,
|
||||
sort_order integer NOT NULL,
|
||||
create_date timestamp without time zone NOT NULL,
|
||||
modify_date timestamp without time zone NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE public.cms_page_section_type ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
||||
SEQUENCE NAME public.cms_page_section_type_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1
|
||||
);
|
||||
|
||||
CREATE TABLE public.cms_page_type (
|
||||
id bigint NOT NULL,
|
||||
name character varying(100) NOT NULL,
|
||||
code character varying(50) NOT NULL,
|
||||
description character varying(255),
|
||||
is_active boolean NOT NULL,
|
||||
sort_order integer NOT NULL,
|
||||
create_date timestamp without time zone NOT NULL,
|
||||
modify_date timestamp without time zone NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE public.cms_page_type ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
||||
SEQUENCE NAME public.cms_page_type_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1
|
||||
);
|
||||
|
||||
CREATE TABLE public.cms_redirect (
|
||||
id bigint NOT NULL,
|
||||
name character varying(100) NOT NULL,
|
||||
description character varying(500),
|
||||
source_path character varying(500) NOT NULL,
|
||||
target_path character varying(500) NOT NULL,
|
||||
redirect_type_id bigint NOT NULL,
|
||||
match_type character varying(20) NOT NULL,
|
||||
is_active boolean NOT NULL,
|
||||
priority integer NOT NULL,
|
||||
preserve_query_params boolean NOT NULL,
|
||||
hit_count bigint NOT NULL,
|
||||
last_hit_date timestamp without time zone,
|
||||
expiry_date timestamp without time zone,
|
||||
site_id bigint NOT NULL,
|
||||
create_date timestamp without time zone NOT NULL,
|
||||
modify_date timestamp without time zone NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE public.cms_redirect ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
||||
SEQUENCE NAME public.cms_redirect_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1
|
||||
);
|
||||
|
||||
CREATE TABLE public.cms_redirect_type (
|
||||
id bigint NOT NULL,
|
||||
name character varying(100) NOT NULL,
|
||||
code character varying(50) NOT NULL,
|
||||
description character varying(500),
|
||||
http_status_code integer NOT NULL,
|
||||
is_active boolean NOT NULL,
|
||||
sort_order integer NOT NULL,
|
||||
create_date timestamp without time zone NOT NULL,
|
||||
modify_date timestamp without time zone NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE public.cms_redirect_type ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
||||
SEQUENCE NAME public.cms_redirect_type_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1
|
||||
);
|
||||
|
||||
CREATE TABLE public.cms_site (
|
||||
id bigint NOT NULL,
|
||||
name character varying(40) NOT NULL,
|
||||
short_name character varying(20) NOT NULL,
|
||||
description character varying(255),
|
||||
create_date timestamp without time zone NOT NULL,
|
||||
modify_date timestamp without time zone NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE public.cms_site ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
||||
SEQUENCE NAME public.cms_site_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1
|
||||
);
|
||||
|
||||
CREATE TABLE public.cms_snippet (
|
||||
id bigint NOT NULL,
|
||||
name character varying(100) NOT NULL,
|
||||
short_name character varying(50) NOT NULL,
|
||||
content text,
|
||||
css_class_name character varying(255),
|
||||
is_active boolean NOT NULL,
|
||||
sort_order integer NOT NULL,
|
||||
site_id bigint NOT NULL,
|
||||
create_date timestamp without time zone NOT NULL,
|
||||
modify_date timestamp without time zone NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE public.cms_snippet ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
||||
SEQUENCE NAME public.cms_snippet_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1
|
||||
);
|
||||
|
||||
ALTER TABLE ONLY public.cms_domain
|
||||
ADD CONSTRAINT pk_cms_domain PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY public.cms_menu
|
||||
ADD CONSTRAINT pk_cms_menu PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY public.cms_menu_item
|
||||
ADD CONSTRAINT pk_cms_menu_item PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY public.cms_page
|
||||
ADD CONSTRAINT pk_cms_page PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY public.cms_page_section
|
||||
ADD CONSTRAINT pk_cms_page_section PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY public.cms_page_section_type
|
||||
ADD CONSTRAINT pk_cms_page_section_type PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY public.cms_page_type
|
||||
ADD CONSTRAINT pk_cms_page_type PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY public.cms_redirect
|
||||
ADD CONSTRAINT pk_cms_redirect PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY public.cms_redirect_type
|
||||
ADD CONSTRAINT pk_cms_redirect_type PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY public.cms_site
|
||||
ADD CONSTRAINT pk_cms_site PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY public.cms_snippet
|
||||
ADD CONSTRAINT pk_cms_snippet PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY public.cms_domain
|
||||
ADD CONSTRAINT uq_cms_domain_fqdn UNIQUE (fqdn);
|
||||
|
||||
ALTER TABLE ONLY public.cms_domain
|
||||
ADD CONSTRAINT uq_cms_domain_name UNIQUE (name);
|
||||
|
||||
ALTER TABLE ONLY public.cms_domain
|
||||
ADD CONSTRAINT uq_cms_domain_short_name UNIQUE (short_name);
|
||||
|
||||
ALTER TABLE ONLY public.cms_menu
|
||||
ADD CONSTRAINT uq_cms_menu_slug_site_id UNIQUE (slug, site_id);
|
||||
|
||||
ALTER TABLE ONLY public.cms_page_section_type
|
||||
ADD CONSTRAINT uq_cms_page_section_type_code UNIQUE (code);
|
||||
|
||||
ALTER TABLE ONLY public.cms_page_section_type
|
||||
ADD CONSTRAINT uq_cms_page_section_type_name UNIQUE (name);
|
||||
|
||||
ALTER TABLE ONLY public.cms_page
|
||||
ADD CONSTRAINT uq_cms_page_slug_parent_page_id UNIQUE (slug, parent_page_id);
|
||||
|
||||
ALTER TABLE ONLY public.cms_page
|
||||
ADD CONSTRAINT uq_cms_page_slug_site_id UNIQUE (slug, site_id);
|
||||
|
||||
ALTER TABLE ONLY public.cms_page_type
|
||||
ADD CONSTRAINT uq_cms_page_type_code UNIQUE (code);
|
||||
|
||||
ALTER TABLE ONLY public.cms_page_type
|
||||
ADD CONSTRAINT uq_cms_page_type_name UNIQUE (name);
|
||||
|
||||
ALTER TABLE ONLY public.cms_redirect_type
|
||||
ADD CONSTRAINT uq_cms_redirect_type_code UNIQUE (code);
|
||||
|
||||
ALTER TABLE ONLY public.cms_site
|
||||
ADD CONSTRAINT uq_cms_site_name UNIQUE (name);
|
||||
|
||||
ALTER TABLE ONLY public.cms_snippet
|
||||
ADD CONSTRAINT uq_cms_snippet_short_name_site_id UNIQUE (short_name, site_id);
|
||||
|
||||
CREATE INDEX idx_cms_menu_item_menu_id ON public.cms_menu_item USING btree (menu_id);
|
||||
|
||||
CREATE INDEX idx_cms_page_section_page_id ON public.cms_page_section USING btree (page_id);
|
||||
|
||||
CREATE INDEX idx_cms_page_site_id_is_published ON public.cms_page USING btree (site_id, is_published);
|
||||
|
||||
CREATE INDEX idx_cms_redirect_site_id_source_path ON public.cms_redirect USING btree (site_id, source_path);
|
||||
|
||||
ALTER TABLE ONLY public.cms_domain
|
||||
ADD CONSTRAINT fk_cms_domain_site_id FOREIGN KEY (site_id) REFERENCES public.cms_site(id);
|
||||
|
||||
ALTER TABLE ONLY public.cms_menu_item
|
||||
ADD CONSTRAINT fk_cms_menu_item_menu_id FOREIGN KEY (menu_id) REFERENCES public.cms_menu(id);
|
||||
|
||||
ALTER TABLE ONLY public.cms_menu_item
|
||||
ADD CONSTRAINT fk_cms_menu_item_parent_id FOREIGN KEY (parent_id) REFERENCES public.cms_menu_item(id);
|
||||
|
||||
ALTER TABLE ONLY public.cms_menu
|
||||
ADD CONSTRAINT fk_cms_menu_site_id FOREIGN KEY (site_id) REFERENCES public.cms_site(id);
|
||||
|
||||
ALTER TABLE ONLY public.cms_page
|
||||
ADD CONSTRAINT fk_cms_page_page_type_id FOREIGN KEY (page_type_id) REFERENCES public.cms_page_type(id);
|
||||
|
||||
ALTER TABLE ONLY public.cms_page
|
||||
ADD CONSTRAINT fk_cms_page_parent_page_id FOREIGN KEY (parent_page_id) REFERENCES public.cms_page(id);
|
||||
|
||||
ALTER TABLE ONLY public.cms_page_section
|
||||
ADD CONSTRAINT fk_cms_page_section_page_id FOREIGN KEY (page_id) REFERENCES public.cms_page(id);
|
||||
|
||||
ALTER TABLE ONLY public.cms_page_section
|
||||
ADD CONSTRAINT fk_cms_page_section_section_type_id FOREIGN KEY (section_type_id) REFERENCES public.cms_page_section_type(id);
|
||||
|
||||
ALTER TABLE ONLY public.cms_page
|
||||
ADD CONSTRAINT fk_cms_page_site_id FOREIGN KEY (site_id) REFERENCES public.cms_site(id);
|
||||
|
||||
ALTER TABLE ONLY public.cms_redirect
|
||||
ADD CONSTRAINT fk_cms_redirect_redirect_type_id FOREIGN KEY (redirect_type_id) REFERENCES public.cms_redirect_type(id);
|
||||
|
||||
ALTER TABLE ONLY public.cms_redirect
|
||||
ADD CONSTRAINT fk_cms_redirect_site_id FOREIGN KEY (site_id) REFERENCES public.cms_site(id);
|
||||
|
||||
ALTER TABLE ONLY public.cms_snippet
|
||||
ADD CONSTRAINT fk_cms_snippet_site_id FOREIGN KEY (site_id) REFERENCES public.cms_site(id);
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 34 KiB |
@@ -18,19 +18,17 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<properties>
|
||||
<qqq.version>4.1.0-RC.26</qqq.version> <!-- feeds qbit-build-parent's qqq-bom-pom import -->
|
||||
<revision>0.1.0-RC.2</revision>
|
||||
<revision>0.1.0-SNAPSHOT</revision>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<jacoco.version>0.8.15</jacoco.version>
|
||||
<liquibase.version>4.33.0</liquibase.version>
|
||||
<testcontainers.version>1.21.4</testcontainers.version>
|
||||
</properties>
|
||||
|
||||
<parent>
|
||||
<groupId>com.kingsrook</groupId>
|
||||
<artifactId>qbit-build-parent</artifactId>
|
||||
<version>2.2.1</version>
|
||||
<version>2.0.0</version>
|
||||
</parent>
|
||||
|
||||
<groupId>com.kingsrook.qbits</groupId>
|
||||
@@ -96,35 +94,6 @@
|
||||
<version>2.2.220</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-backend-module-postgres</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<!-- Demo server for local screenshots only (src/test/.../demo). -->
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-middleware-javalin</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-frontend-next</artifactId>
|
||||
<version>1.0.0-RC.38</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>${testcontainers.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<version>${testcontainers.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.liquibase</groupId>
|
||||
<artifactId>liquibase-core</artifactId>
|
||||
@@ -135,37 +104,6 @@
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-checkstyle-plugin</artifactId>
|
||||
<version>3.3.1</version>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.puppycrawl.tools</groupId>
|
||||
<artifactId>checkstyle</artifactId>
|
||||
<version>10.12.6</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<configuration>
|
||||
<configLocation>checkstyle/config.xml</configLocation>
|
||||
<headerLocation>checkstyle/license.txt</headerLocation>
|
||||
<encoding>UTF-8</encoding>
|
||||
<consoleOutput>true</consoleOutput>
|
||||
<failsOnError>false</failsOnError>
|
||||
<failOnViolation>true</failOnViolation>
|
||||
<violationSeverity>warning</violationSeverity>
|
||||
<includeTestSourceDirectory>false</includeTestSourceDirectory>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>validate</id>
|
||||
<phase>validate</phase>
|
||||
<goals>
|
||||
<goal>check</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.jacoco</groupId>
|
||||
<artifactId>jacoco-maven-plugin</artifactId>
|
||||
@@ -217,15 +155,14 @@
|
||||
<profiles>
|
||||
<!-- Built against qqq 4.1 (QBitMetaDataProducer). Until qbit-build-parent -->
|
||||
<!-- imports the released 4.1 BOM, this profile is active by default and -->
|
||||
<!-- imports qqq-bom-pom 4.1.0-RC.21 ahead of the parent's 4.0.0 BOM. -->
|
||||
<!-- imports qqq-bom-pom 4.1.0-RC.4 ahead of the parent's 4.0.0 BOM. -->
|
||||
<profile>
|
||||
<id>qqq-snapshot</id>
|
||||
<activation>
|
||||
<activeByDefault>true</activeByDefault>
|
||||
</activation>
|
||||
<properties>
|
||||
<!-- The published QQQ 4.1 core (Central JAR/POM), the one BOM version shared by the four app-QBit patches. -->
|
||||
<qqq.snapshot.version>4.1.0-RC.26</qqq.snapshot.version>
|
||||
<qqq.snapshot.version>4.1.0-RC.4</qqq.snapshot.version>
|
||||
</properties>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.cms;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The feature areas of the CMS (spec section 2.3). A host turns features on
|
||||
** and off in CmsQBitConfig.features; a feature that is off produces no meta
|
||||
** data, and its changesets are skipped by the cms.features changelog
|
||||
** property.
|
||||
**
|
||||
** CORE is always on. A feature that is not built yet (available = false) is
|
||||
** rejected by validation, so a host never believes it installed something
|
||||
** that is not there.
|
||||
*******************************************************************************/
|
||||
public enum CmsFeature
|
||||
{
|
||||
/** Sites, domains, redirects, pages, page sections, snippets, menus. */
|
||||
CORE(true),
|
||||
|
||||
/** The media library, media types and the storage SPI. */
|
||||
MEDIA(false),
|
||||
|
||||
/** Blogs, posts, tags, authors, post media and versions. */
|
||||
BLOG(false, MEDIA),
|
||||
|
||||
/** Projects, project categories and project updates. */
|
||||
PROJECTS(false, MEDIA),
|
||||
|
||||
/** Photo albums, folders, photos and photo tags. */
|
||||
PHOTOS(false, MEDIA),
|
||||
|
||||
/** Publishing processes, publications and receipts. */
|
||||
PUBLISHING(false),
|
||||
|
||||
/** The public content stream across content kinds. */
|
||||
STREAM(false, BLOG),
|
||||
|
||||
/** CMS Data Health rules, for hosts with a Data Health framework (D10). */
|
||||
DATA_HEALTH_RULES(false);
|
||||
|
||||
private final boolean available;
|
||||
private final List<CmsFeature> requires;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor.
|
||||
**
|
||||
** @param available whether this version of the qbit implements the feature
|
||||
** @param requires features this one needs
|
||||
*******************************************************************************/
|
||||
CmsFeature(boolean available, CmsFeature... requires)
|
||||
{
|
||||
this.available = available;
|
||||
this.requires = List.of(requires);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Whether this version of the qbit implements the feature.
|
||||
*******************************************************************************/
|
||||
public boolean isAvailable()
|
||||
{
|
||||
return (available);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The features this one needs (beyond CORE, which is always on).
|
||||
*******************************************************************************/
|
||||
public List<CmsFeature> getRequires()
|
||||
{
|
||||
return (requires);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Every feature this version implements: the default feature set.
|
||||
*******************************************************************************/
|
||||
public static Set<CmsFeature> allAvailable()
|
||||
{
|
||||
EnumSet<CmsFeature> rs = EnumSet.noneOf(CmsFeature.class);
|
||||
for(CmsFeature feature : values())
|
||||
{
|
||||
if(feature.available)
|
||||
{
|
||||
rs.add(feature);
|
||||
}
|
||||
}
|
||||
return (rs);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Validation messages for a feature set: unknown-in-this-version features,
|
||||
** missing dependencies, and PUBLISHING without BLOG or PROJECTS.
|
||||
**
|
||||
** @param features the requested features
|
||||
** @return the problems (empty when valid)
|
||||
*******************************************************************************/
|
||||
public static List<String> validate(Set<CmsFeature> features)
|
||||
{
|
||||
List<String> errors = new ArrayList<>();
|
||||
if(features == null || !features.contains(CORE))
|
||||
{
|
||||
errors.add("features must include CORE");
|
||||
return (errors);
|
||||
}
|
||||
for(CmsFeature feature : features)
|
||||
{
|
||||
if(!feature.available)
|
||||
{
|
||||
errors.add("feature " + feature + " is not available in this version of qbit-cms");
|
||||
}
|
||||
for(CmsFeature required : feature.requires)
|
||||
{
|
||||
if(!features.contains(required))
|
||||
{
|
||||
errors.add("feature " + feature + " requires " + required);
|
||||
}
|
||||
}
|
||||
}
|
||||
if(features.contains(PUBLISHING) && !features.contains(BLOG) && !features.contains(PROJECTS))
|
||||
{
|
||||
errors.add("feature PUBLISHING requires BLOG or PROJECTS");
|
||||
}
|
||||
return (errors);
|
||||
}
|
||||
}
|
||||
@@ -17,89 +17,34 @@
|
||||
package com.kingsrook.qbits.cms;
|
||||
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.kingsrook.qbits.cms.security.CmsRole;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitConfig;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Host-facing configuration for the CMS QBit (spec section 2.4).
|
||||
** Host-facing configuration for the CMS QBit.
|
||||
**
|
||||
** Tables are named tableNamePrefix + lowercase snake_case (cms_page by
|
||||
** default); the QQQ table name equals the backend (SQL) table name, and the
|
||||
** prefix must equal the cms.tablePrefix property the host gives the
|
||||
** changelog. Apps and widgets are named namePrefix + CamelCase (cmsPages).
|
||||
** Table permissions are the table name plus .read, .insert, .edit or .delete
|
||||
** (cms_page.edit); qbit-wide permissions are permissionBaseName + .action
|
||||
** (cms.admin).
|
||||
** Tables are named cms_<name> (lowercase snake_case, backend name equal
|
||||
** to the table name); permissions are cms.<action>.
|
||||
*******************************************************************************/
|
||||
public class CmsQBitConfig implements QBitConfig
|
||||
{
|
||||
public static final String DEFAULT_TABLE_NAME_PREFIX = "cms_";
|
||||
public static final String DEFAULT_NAME_PREFIX = "cms";
|
||||
public static final String DEFAULT_PERMISSION_BASE = "cms";
|
||||
public static final String DEFAULT_APP_LABEL = "CMS";
|
||||
public static final String CHANGELOG = "qbit-cms/db/changelog/qbit-cms.changelog.yaml";
|
||||
public static final String CHANGELOG_LOGICAL_FILE_PATH = "qbit-cms/changelog";
|
||||
public static final String TABLE_NAME_PREFIX = "cms_";
|
||||
public static final String PERMISSION_BASE_NAME = "cms";
|
||||
|
||||
private static final Pattern TABLE_PREFIX = Pattern.compile("^[a-z][a-z0-9_]{0,20}_$");
|
||||
private static final Pattern NAME_PREFIX = Pattern.compile("^[a-z][a-zA-Z0-9]{0,20}$");
|
||||
|
||||
private String backendName;
|
||||
private String tableNamePrefix = DEFAULT_TABLE_NAME_PREFIX;
|
||||
private String namePrefix = DEFAULT_NAME_PREFIX;
|
||||
private String permissionBaseName = DEFAULT_PERMISSION_BASE;
|
||||
private Set<CmsFeature> features = CmsFeature.allAvailable();
|
||||
private String appName;
|
||||
private String appLabel = DEFAULT_APP_LABEL;
|
||||
private String parentAppName;
|
||||
private Boolean openReads = false;
|
||||
private CmsStartupMode startupMode = CmsStartupMode.FAIL_FAST;
|
||||
private CmsSchemaVerification schemaVerification = CmsSchemaVerification.CHANGELOG_HISTORY;
|
||||
private Map<String, CmsRole> roleMapping = new LinkedHashMap<>();
|
||||
|
||||
private CmsRuntime runtime;
|
||||
private String backendName;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Validate against the instance: an RDBMS backend, well-formed prefixes,
|
||||
** a consistent feature set, and a parent app that exists.
|
||||
** The backend must be named.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void validate(QInstance qInstance, List<String> errors)
|
||||
{
|
||||
if(assertCondition(StringUtils.hasContent(backendName), "backendName is required for the CMS QBit", errors))
|
||||
{
|
||||
QBackendMetaData backend = qInstance.getBackend(backendName);
|
||||
if(assertCondition(backend != null, "backendName " + backendName + " is not a backend on this instance", errors))
|
||||
{
|
||||
assertCondition(backend instanceof RDBMSBackendMetaData, "backend " + backendName + " must be an RDBMS backend (the CMS uses SQL joins and views)", errors);
|
||||
}
|
||||
}
|
||||
assertCondition(tableNamePrefix != null && TABLE_PREFIX.matcher(tableNamePrefix).matches(),
|
||||
"tableNamePrefix must be lowercase letters, digits and underscores, ending in _ (for example cms_): " + tableNamePrefix, errors);
|
||||
assertCondition(namePrefix != null && NAME_PREFIX.matcher(namePrefix).matches(),
|
||||
"namePrefix must start with a lowercase letter and be letters and digits (for example cms): " + namePrefix, errors);
|
||||
assertCondition(StringUtils.hasContent(permissionBaseName), "permissionBaseName is required", errors);
|
||||
errors.addAll(CmsFeature.validate(features));
|
||||
if(StringUtils.hasContent(parentAppName))
|
||||
{
|
||||
assertCondition(qInstance.getApp(parentAppName) != null, "parentAppName " + parentAppName + " is not an app on this instance (define it before producing the CMS)", errors);
|
||||
}
|
||||
assertCondition(roleMapping != null, "roleMapping may be empty but not null", errors);
|
||||
assertCondition(startupMode != null, "startupMode is required", errors);
|
||||
assertCondition(schemaVerification != null, "schemaVerification is required", errors);
|
||||
assertCondition(StringUtils.hasContent(backendName), "backendName is required for the CMS QBit", errors);
|
||||
}
|
||||
|
||||
|
||||
@@ -115,78 +60,6 @@ public class CmsQBitConfig implements QBitConfig
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The physical (and QQQ) table name of a logical table name.
|
||||
**
|
||||
** @param logicalTableName e.g. "page"
|
||||
** @return e.g. "cms_page"
|
||||
*******************************************************************************/
|
||||
public String applyPrefix(String logicalTableName)
|
||||
{
|
||||
return (tableNamePrefix + logicalTableName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A prefixed app, widget or process name.
|
||||
**
|
||||
** @param suffix e.g. "Pages" or "PagesHome"
|
||||
** @return e.g. "cmsPages"
|
||||
*******************************************************************************/
|
||||
public String name(String suffix)
|
||||
{
|
||||
return (namePrefix + suffix);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The name of the CMS top app (appName, else the name prefix).
|
||||
*******************************************************************************/
|
||||
public String getEffectiveAppName()
|
||||
{
|
||||
return (StringUtils.hasContent(appName) ? appName : namePrefix);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A qbit-wide permission name.
|
||||
**
|
||||
** @param action e.g. "admin"
|
||||
** @return e.g. "cms.admin"
|
||||
*******************************************************************************/
|
||||
public String permission(String action)
|
||||
{
|
||||
return (permissionBaseName + "." + action);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Whether a feature is on.
|
||||
*******************************************************************************/
|
||||
public boolean hasFeature(CmsFeature feature)
|
||||
{
|
||||
return (features != null && features.contains(feature));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Description for logs (the config holds no secrets yet).
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return ("CmsQBitConfig{backendName=" + backendName + ", tableNamePrefix=" + tableNamePrefix + ", namePrefix=" + namePrefix
|
||||
+ ", features=" + features + ", app=" + getEffectiveAppName() + ", parentAppName=" + parentAppName + ", openReads=" + openReads
|
||||
+ ", startupMode=" + startupMode + ", roleMapping=" + roleMapping + "}");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for backendName
|
||||
*******************************************************************************/
|
||||
@@ -216,382 +89,4 @@ public class CmsQBitConfig implements QBitConfig
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for tableNamePrefix
|
||||
*******************************************************************************/
|
||||
public String getTableNamePrefix()
|
||||
{
|
||||
return (this.tableNamePrefix);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for tableNamePrefix
|
||||
*******************************************************************************/
|
||||
public void setTableNamePrefix(String tableNamePrefix)
|
||||
{
|
||||
this.tableNamePrefix = tableNamePrefix;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for tableNamePrefix
|
||||
*******************************************************************************/
|
||||
public CmsQBitConfig withTableNamePrefix(String tableNamePrefix)
|
||||
{
|
||||
this.tableNamePrefix = tableNamePrefix;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for namePrefix
|
||||
*******************************************************************************/
|
||||
public String getNamePrefix()
|
||||
{
|
||||
return (this.namePrefix);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for namePrefix
|
||||
*******************************************************************************/
|
||||
public void setNamePrefix(String namePrefix)
|
||||
{
|
||||
this.namePrefix = namePrefix;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for namePrefix
|
||||
*******************************************************************************/
|
||||
public CmsQBitConfig withNamePrefix(String namePrefix)
|
||||
{
|
||||
this.namePrefix = namePrefix;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for permissionBaseName
|
||||
*******************************************************************************/
|
||||
public String getPermissionBaseName()
|
||||
{
|
||||
return (this.permissionBaseName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for permissionBaseName
|
||||
*******************************************************************************/
|
||||
public void setPermissionBaseName(String permissionBaseName)
|
||||
{
|
||||
this.permissionBaseName = permissionBaseName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for permissionBaseName
|
||||
*******************************************************************************/
|
||||
public CmsQBitConfig withPermissionBaseName(String permissionBaseName)
|
||||
{
|
||||
this.permissionBaseName = permissionBaseName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for features
|
||||
*******************************************************************************/
|
||||
public Set<CmsFeature> getFeatures()
|
||||
{
|
||||
return (this.features);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for features
|
||||
*******************************************************************************/
|
||||
public void setFeatures(Set<CmsFeature> features)
|
||||
{
|
||||
this.features = features == null || features.isEmpty() ? EnumSet.noneOf(CmsFeature.class) : EnumSet.copyOf(features);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for features
|
||||
*******************************************************************************/
|
||||
public CmsQBitConfig withFeatures(Set<CmsFeature> features)
|
||||
{
|
||||
setFeatures(features);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for appName
|
||||
*******************************************************************************/
|
||||
public String getAppName()
|
||||
{
|
||||
return (this.appName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for appName
|
||||
*******************************************************************************/
|
||||
public void setAppName(String appName)
|
||||
{
|
||||
this.appName = appName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for appName
|
||||
*******************************************************************************/
|
||||
public CmsQBitConfig withAppName(String appName)
|
||||
{
|
||||
this.appName = appName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for appLabel
|
||||
*******************************************************************************/
|
||||
public String getAppLabel()
|
||||
{
|
||||
return (this.appLabel);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for appLabel
|
||||
*******************************************************************************/
|
||||
public void setAppLabel(String appLabel)
|
||||
{
|
||||
this.appLabel = appLabel;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for appLabel
|
||||
*******************************************************************************/
|
||||
public CmsQBitConfig withAppLabel(String appLabel)
|
||||
{
|
||||
this.appLabel = appLabel;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for parentAppName
|
||||
*******************************************************************************/
|
||||
public String getParentAppName()
|
||||
{
|
||||
return (this.parentAppName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for parentAppName
|
||||
*******************************************************************************/
|
||||
public void setParentAppName(String parentAppName)
|
||||
{
|
||||
this.parentAppName = parentAppName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for parentAppName
|
||||
*******************************************************************************/
|
||||
public CmsQBitConfig withParentAppName(String parentAppName)
|
||||
{
|
||||
this.parentAppName = parentAppName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for openReads
|
||||
*******************************************************************************/
|
||||
public Boolean getOpenReads()
|
||||
{
|
||||
return (this.openReads);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for openReads
|
||||
*******************************************************************************/
|
||||
public void setOpenReads(Boolean openReads)
|
||||
{
|
||||
this.openReads = openReads;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for openReads: when true, reading CMS tables needs no
|
||||
** permission (writes always do).
|
||||
*******************************************************************************/
|
||||
public CmsQBitConfig withOpenReads(Boolean openReads)
|
||||
{
|
||||
this.openReads = openReads;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for schemaVerification
|
||||
*******************************************************************************/
|
||||
public CmsSchemaVerification getSchemaVerification()
|
||||
{
|
||||
return (this.schemaVerification);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for schemaVerification
|
||||
*******************************************************************************/
|
||||
public void setSchemaVerification(CmsSchemaVerification schemaVerification)
|
||||
{
|
||||
this.schemaVerification = schemaVerification;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for schemaVerification
|
||||
*******************************************************************************/
|
||||
public CmsQBitConfig withSchemaVerification(CmsSchemaVerification schemaVerification)
|
||||
{
|
||||
this.schemaVerification = schemaVerification;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for startupMode
|
||||
*******************************************************************************/
|
||||
public CmsStartupMode getStartupMode()
|
||||
{
|
||||
return (this.startupMode);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for startupMode
|
||||
*******************************************************************************/
|
||||
public void setStartupMode(CmsStartupMode startupMode)
|
||||
{
|
||||
this.startupMode = startupMode;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for startupMode
|
||||
*******************************************************************************/
|
||||
public CmsQBitConfig withStartupMode(CmsStartupMode startupMode)
|
||||
{
|
||||
this.startupMode = startupMode;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for roleMapping
|
||||
*******************************************************************************/
|
||||
public Map<String, CmsRole> getRoleMapping()
|
||||
{
|
||||
return (this.roleMapping);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for roleMapping
|
||||
*******************************************************************************/
|
||||
public void setRoleMapping(Map<String, CmsRole> roleMapping)
|
||||
{
|
||||
this.roleMapping = roleMapping;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for roleMapping: identity-provider group name to CMS role.
|
||||
*******************************************************************************/
|
||||
public CmsQBitConfig withRoleMapping(Map<String, CmsRole> roleMapping)
|
||||
{
|
||||
this.roleMapping = roleMapping;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The install's runtime state, set when the qbit is produced (not part of
|
||||
** the config's JSON: tables carry their source qbit config to the frontend).
|
||||
*******************************************************************************/
|
||||
@JsonIgnore
|
||||
public CmsRuntime getRuntime()
|
||||
{
|
||||
return (this.runtime);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for runtime (the producer sets it).
|
||||
*******************************************************************************/
|
||||
public void setRuntime(CmsRuntime runtime)
|
||||
{
|
||||
this.runtime = runtime;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent adder for one roleMapping entry.
|
||||
*******************************************************************************/
|
||||
public CmsQBitConfig withRole(String groupName, CmsRole role)
|
||||
{
|
||||
if(this.roleMapping == null)
|
||||
{
|
||||
this.roleMapping = new LinkedHashMap<>();
|
||||
}
|
||||
this.roleMapping.put(groupName, role);
|
||||
return (this);
|
||||
}
|
||||
}
|
||||
@@ -17,57 +17,22 @@
|
||||
package com.kingsrook.qbits.cms;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import com.kingsrook.qbits.cms.metadata.CmsAppBuilder;
|
||||
import com.kingsrook.qbits.cms.metadata.CmsTableBuilder;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerMultiOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitMetaDataProducer;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Produces the CMS QBit: standard QQQ tables for the enabled features, their
|
||||
** possible-value sources, joins and child lists, the CMS app with one home
|
||||
** per area, and permission rules on everything (spec QRun-IO/qqq#1083).
|
||||
**
|
||||
** <pre>
|
||||
** new CmsQBitProducer()
|
||||
** .withQBitConfig(new CmsQBitConfig()
|
||||
** .withBackendName("rdbms")
|
||||
** .withRole("cms-admins", CmsRole.ADMIN))
|
||||
** .produce(qInstance);
|
||||
** </pre>
|
||||
**
|
||||
** The database comes from the qbit's Liquibase changelog
|
||||
** (CmsQBitConfig.CHANGELOG), which the host includes and runs; its
|
||||
** cms.tablePrefix property must equal the config's tableNamePrefix. At
|
||||
** startup the producer checks that the changelog ran (startupMode).
|
||||
**
|
||||
** The producer builds everything itself instead of scanning for component
|
||||
** producers, so the output depends only on the config.
|
||||
** Produces the CMS QBit. Component producers in this package are found by
|
||||
** package scanning.
|
||||
*******************************************************************************/
|
||||
public class CmsQBitProducer implements QBitMetaDataProducer<CmsQBitConfig>
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(CmsQBitProducer.class);
|
||||
|
||||
public static final String GROUP_ID = "com.kingsrook.qbits";
|
||||
public static final String ARTIFACT_ID = "qbit-cms";
|
||||
public static final String VERSION = "0.1.0";
|
||||
|
||||
private CmsQBitConfig qBitConfig;
|
||||
private String namespace;
|
||||
private String namespace;
|
||||
|
||||
|
||||
|
||||
@@ -94,9 +59,7 @@ public class CmsQBitProducer implements QBitMetaDataProducer<CmsQBitConfig>
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for the namespace. A second install in one instance needs
|
||||
** its own namespace; the namespace becomes the table prefix, name prefix
|
||||
** and permission base unless the config sets them.
|
||||
** Fluent setter for the namespace.
|
||||
*******************************************************************************/
|
||||
public CmsQBitProducer withNamespace(String namespace)
|
||||
{
|
||||
@@ -137,138 +100,4 @@ public class CmsQBitProducer implements QBitMetaDataProducer<CmsQBitConfig>
|
||||
return (qBitConfig != null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Validate, build and add everything to the instance, then check the
|
||||
** schema. Returns an empty output, so a host that also adds the output to
|
||||
** the instance adds nothing twice.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public MetaDataProducerMultiOutput produce(QInstance qInstance) throws QException
|
||||
{
|
||||
if(qBitConfig == null)
|
||||
{
|
||||
throw (new QException("CMS QBit configuration validation failed: no config was supplied (call withQBitConfig)"));
|
||||
}
|
||||
applyNamespace();
|
||||
|
||||
List<String> errors = new ArrayList<>();
|
||||
qBitConfig.validate(qInstance, errors);
|
||||
if(!errors.isEmpty())
|
||||
{
|
||||
throw (new QException("CMS QBit configuration validation failed: " + String.join("; ", errors)));
|
||||
}
|
||||
|
||||
CmsTableBuilder.Built tables = new CmsTableBuilder(qBitConfig).build(qInstance);
|
||||
CmsAppBuilder.Built apps = new CmsAppBuilder(qBitConfig).build();
|
||||
MetaDataProducerMultiOutput output = new MetaDataProducerMultiOutput();
|
||||
tables.tables().values().forEach(output::add);
|
||||
tables.possibleValueSources().forEach(output::add);
|
||||
tables.joins().forEach(output::add);
|
||||
tables.widgets().forEach(output::add);
|
||||
apps.widgets().forEach(output::add);
|
||||
apps.apps().forEach(output::add);
|
||||
|
||||
List<String> collisions = collisions(qInstance, output);
|
||||
if(!collisions.isEmpty())
|
||||
{
|
||||
throw (new QException("CMS QBit names collide with names already on the instance (set a different tableNamePrefix and namePrefix, or a namespace): "
|
||||
+ String.join(", ", collisions)));
|
||||
}
|
||||
|
||||
QBitMetaData qBitMetaData = getQBitMetaData();
|
||||
qInstance.addQBit(qBitMetaData);
|
||||
for(QTableMetaData table : output.getEach(QTableMetaData.class))
|
||||
{
|
||||
if(qBitConfig.getTableMetaDataCustomizer() != null)
|
||||
{
|
||||
qBitConfig.getTableMetaDataCustomizer().customizeMetaData(qInstance, table);
|
||||
}
|
||||
table.setSourceQBitName(qBitMetaData.getName());
|
||||
}
|
||||
output.addSelfToInstance(qInstance);
|
||||
|
||||
if(StringUtils.hasContent(qBitConfig.getParentAppName()))
|
||||
{
|
||||
qInstance.getApp(qBitConfig.getParentAppName()).withChild(qInstance.getApp(qBitConfig.getEffectiveAppName()));
|
||||
}
|
||||
|
||||
postProduceActions(output, qInstance);
|
||||
return (new MetaDataProducerMultiOutput());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Check the schema per the startup mode and set the runtime.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void postProduceActions(MetaDataProducerMultiOutput output, QInstance qInstance) throws QException
|
||||
{
|
||||
List<String> problems = qBitConfig.getStartupMode() == CmsStartupMode.SKIP ? List.of() : CmsSchemaCheck.check(qInstance, qBitConfig);
|
||||
if(!problems.isEmpty() && qBitConfig.getStartupMode() == CmsStartupMode.FAIL_FAST)
|
||||
{
|
||||
throw (new QException("The CMS database is not up to date (startupMode FAIL_FAST): " + String.join(" ", problems)));
|
||||
}
|
||||
if(!problems.isEmpty())
|
||||
{
|
||||
LOG.warn("The CMS database is not up to date; starting degraded", logPair("problems", problems));
|
||||
}
|
||||
qBitConfig.setRuntime(new CmsRuntime(problems));
|
||||
|
||||
LOG.info("CMS QBit produced", logPair("version", VERSION), logPair("backendName", qBitConfig.getBackendName()),
|
||||
logPair("tablePrefix", qBitConfig.getTableNamePrefix()), logPair("features", qBitConfig.getFeatures()),
|
||||
logPair("app", qBitConfig.getEffectiveAppName()), logPair("startupMode", qBitConfig.getStartupMode()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A namespace fills in prefixes the config leaves at their defaults.
|
||||
*******************************************************************************/
|
||||
private void applyNamespace()
|
||||
{
|
||||
if(!StringUtils.hasContent(namespace))
|
||||
{
|
||||
return;
|
||||
}
|
||||
String snake = namespace.replaceAll("([a-z0-9])([A-Z])", "$1_$2").toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9_]", "_");
|
||||
if(CmsQBitConfig.DEFAULT_TABLE_NAME_PREFIX.equals(qBitConfig.getTableNamePrefix()))
|
||||
{
|
||||
qBitConfig.setTableNamePrefix(snake.endsWith("_") ? snake : snake + "_");
|
||||
}
|
||||
if(CmsQBitConfig.DEFAULT_NAME_PREFIX.equals(qBitConfig.getNamePrefix()))
|
||||
{
|
||||
StringBuilder camel = new StringBuilder();
|
||||
for(String part : snake.split("_"))
|
||||
{
|
||||
if(!part.isEmpty())
|
||||
{
|
||||
camel.append(camel.isEmpty() ? part : Character.toUpperCase(part.charAt(0)) + part.substring(1));
|
||||
}
|
||||
}
|
||||
qBitConfig.setNamePrefix(camel.toString());
|
||||
}
|
||||
if(CmsQBitConfig.DEFAULT_PERMISSION_BASE.equals(qBitConfig.getPermissionBaseName()))
|
||||
{
|
||||
qBitConfig.setPermissionBaseName(snake.replaceAll("_+$", ""));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Names the qbit would add that the instance already has.
|
||||
*******************************************************************************/
|
||||
static List<String> collisions(QInstance qInstance, MetaDataProducerMultiOutput output)
|
||||
{
|
||||
List<String> rs = new ArrayList<>();
|
||||
output.getEach(QTableMetaData.class).stream().filter(t -> qInstance.getTable(t.getName()) != null).forEach(t -> rs.add("table " + t.getName()));
|
||||
output.getEach(QPossibleValueSource.class).stream().filter(p -> qInstance.getPossibleValueSource(p.getName()) != null).forEach(p -> rs.add("possible-value source " + p.getName()));
|
||||
output.getEach(QJoinMetaData.class).stream().filter(j -> qInstance.getJoin(j.getName()) != null).forEach(j -> rs.add("join " + j.getName()));
|
||||
output.getEach(QWidgetMetaData.class).stream().filter(w -> qInstance.getWidget(w.getName()) != null).forEach(w -> rs.add("widget " + w.getName()));
|
||||
output.getEach(QAppMetaData.class).stream().filter(a -> qInstance.getApp(a.getName()) != null).forEach(a -> rs.add("app " + a.getName()));
|
||||
return (rs);
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.cms;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Runtime state of one CMS install, held by its config: the result of the
|
||||
** startup schema check. Code that runs per request (widget renderers, processes)
|
||||
** finds it by the install's name prefix, so two installs in one instance
|
||||
** (two namespaces) stay apart.
|
||||
*******************************************************************************/
|
||||
public class CmsRuntime
|
||||
{
|
||||
private final List<String> schemaProblems;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor.
|
||||
**
|
||||
** @param schemaProblems what the startup schema check found (empty when fine
|
||||
** or not checked)
|
||||
*******************************************************************************/
|
||||
public CmsRuntime(List<String> schemaProblems)
|
||||
{
|
||||
this.schemaProblems = List.copyOf(schemaProblems);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The config of the CMS install with a name prefix on an instance.
|
||||
**
|
||||
** @param qInstance the instance
|
||||
** @param namePrefix the install's name prefix (CmsQBitConfig.namePrefix)
|
||||
** @return the config, if installed
|
||||
*******************************************************************************/
|
||||
public static Optional<CmsQBitConfig> findConfig(QInstance qInstance, String namePrefix)
|
||||
{
|
||||
if(qInstance == null || qInstance.getQBits() == null)
|
||||
{
|
||||
return (Optional.empty());
|
||||
}
|
||||
for(QBitMetaData qBit : qInstance.getQBits().values())
|
||||
{
|
||||
if(qBit.getConfig() instanceof CmsQBitConfig config && config.getNamePrefix().equals(namePrefix))
|
||||
{
|
||||
return (Optional.of(config));
|
||||
}
|
||||
}
|
||||
return (Optional.empty());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for schemaProblems
|
||||
*******************************************************************************/
|
||||
public List<String> getSchemaProblems()
|
||||
{
|
||||
return (schemaProblems);
|
||||
}
|
||||
}
|
||||
@@ -1,211 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.cms;
|
||||
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.DatabaseMetaData;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qbits.cms.metadata.CmsTableDefinition;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The startup schema check, the "install doctor" (spec section 3.4): the
|
||||
** qbit's changelog has run for every enabled feature (its create changesets
|
||||
** are recorded in DATABASECHANGELOG under the qbit's logical file path) and
|
||||
** every enabled table exists. The qbit never creates or alters anything; it
|
||||
** only reports, and the startup mode decides whether that stops the app.
|
||||
*******************************************************************************/
|
||||
public final class CmsSchemaCheck
|
||||
{
|
||||
/** The baseline version of the changesets that create the tables. */
|
||||
static final String BASELINE = "1.0.0";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Not instantiable.
|
||||
*******************************************************************************/
|
||||
private CmsSchemaCheck()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The problems with the database behind a config (empty when it is fine).
|
||||
**
|
||||
** @param qInstance the instance (for the backend)
|
||||
** @param config the CMS config
|
||||
** @return one message per problem
|
||||
*******************************************************************************/
|
||||
public static List<String> check(QInstance qInstance, CmsQBitConfig config)
|
||||
{
|
||||
List<String> problems = new ArrayList<>();
|
||||
try(Connection connection = ConnectionManager.getConnection((RDBMSBackendMetaData) qInstance.getBackend(config.getBackendName())))
|
||||
{
|
||||
if(config.getSchemaVerification() == CmsSchemaVerification.CATALOG)
|
||||
{
|
||||
return (checkCatalog(qInstance, config, connection));
|
||||
}
|
||||
Set<String> recorded = recordedChangesets(connection);
|
||||
if(recorded == null)
|
||||
{
|
||||
problems.add("The qbit-cms changelog has not been run: there is no DATABASECHANGELOG table. Include "
|
||||
+ CmsQBitConfig.CHANGELOG + " in your Liquibase changelog and run your migrations.");
|
||||
return (problems);
|
||||
}
|
||||
|
||||
for(CmsTableDefinition definition : CmsTables.definitions(config))
|
||||
{
|
||||
String table = config.applyPrefix(definition.logicalName());
|
||||
if(!recorded.contains(BASELINE + "-create-" + definition.logicalName()))
|
||||
{
|
||||
problems.add("Changeset " + BASELINE + "-create-" + definition.logicalName() + " (" + table + ") has not run: run your migrations with "
|
||||
+ "cms.tablePrefix=" + config.getTableNamePrefix() + " and the " + definition.feature() + " feature in cms.features.");
|
||||
}
|
||||
else if(!tableExists(connection, table))
|
||||
{
|
||||
problems.add("Table " + table + " is missing although its changeset is recorded.");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(SQLException e)
|
||||
{
|
||||
problems.add("Could not check the CMS schema: " + e.getMessage());
|
||||
}
|
||||
return (problems);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Verify from the database catalog alone: each table of the installed features
|
||||
** exists and has every column the qbit maps. Never reads Liquibase tables, so it
|
||||
** works for an identity that has only table privileges.
|
||||
*******************************************************************************/
|
||||
static List<String> checkCatalog(QInstance qInstance, CmsQBitConfig config, Connection connection) throws SQLException
|
||||
{
|
||||
List<String> problems = new ArrayList<>();
|
||||
for(CmsTableDefinition definition : CmsTables.definitions(config))
|
||||
{
|
||||
String table = config.applyPrefix(definition.logicalName());
|
||||
if(!tableExists(connection, table))
|
||||
{
|
||||
problems.add("Table " + table + " is missing: run the qbit-cms changelog with cms.tablePrefix=" + config.getTableNamePrefix()
|
||||
+ " and the " + definition.feature() + " feature in cms.features.");
|
||||
continue;
|
||||
}
|
||||
QTableMetaData metaData = qInstance.getTable(table);
|
||||
if(metaData == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Set<String> columns = columnsOf(connection, table);
|
||||
for(QFieldMetaData field : metaData.getFields().values())
|
||||
{
|
||||
String column = field.getBackendName() != null ? field.getBackendName() : field.getName();
|
||||
if(!columns.contains(column.toLowerCase(Locale.ROOT)))
|
||||
{
|
||||
problems.add("Column " + table + "." + column + " is missing: the database is behind this version of qbit-cms.");
|
||||
}
|
||||
}
|
||||
}
|
||||
return (problems);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The lower-cased column names of a table, from the catalog.
|
||||
*******************************************************************************/
|
||||
static Set<String> columnsOf(Connection connection, String table) throws SQLException
|
||||
{
|
||||
Set<String> columns = new HashSet<>();
|
||||
DatabaseMetaData metaData = connection.getMetaData();
|
||||
for(String candidate : List.of(table, table.toUpperCase(Locale.ROOT), table.toLowerCase(Locale.ROOT)))
|
||||
{
|
||||
try(ResultSet resultSet = metaData.getColumns(connection.getCatalog(), null, candidate, null))
|
||||
{
|
||||
while(resultSet.next())
|
||||
{
|
||||
columns.add(resultSet.getString("COLUMN_NAME").toLowerCase(Locale.ROOT));
|
||||
}
|
||||
}
|
||||
}
|
||||
return (columns);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The qbit's recorded changeset ids, or null when Liquibase has never run.
|
||||
*******************************************************************************/
|
||||
static Set<String> recordedChangesets(Connection connection) throws SQLException
|
||||
{
|
||||
if(!tableExists(connection, "databasechangelog"))
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
Set<String> ids = new HashSet<>();
|
||||
try(PreparedStatement statement = connection.prepareStatement("SELECT ID FROM DATABASECHANGELOG WHERE FILENAME = ?"))
|
||||
{
|
||||
statement.setString(1, CmsQBitConfig.CHANGELOG_LOGICAL_FILE_PATH);
|
||||
try(ResultSet resultSet = statement.executeQuery())
|
||||
{
|
||||
while(resultSet.next())
|
||||
{
|
||||
ids.add(resultSet.getString(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
return (ids);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Whether a table (or view) exists, in any case the database folds names to.
|
||||
*******************************************************************************/
|
||||
static boolean tableExists(Connection connection, String name) throws SQLException
|
||||
{
|
||||
DatabaseMetaData metaData = connection.getMetaData();
|
||||
for(String candidate : List.of(name, name.toUpperCase(Locale.ROOT), name.toLowerCase(Locale.ROOT)))
|
||||
{
|
||||
try(ResultSet resultSet = metaData.getTables(connection.getCatalog(), null, candidate, new String[] { "TABLE", "VIEW" }))
|
||||
{
|
||||
if(resultSet.next())
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
}
|
||||
}
|
||||
return (false);
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.cms;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** What the startup schema check reads to decide the database is up to date.
|
||||
*******************************************************************************/
|
||||
public enum CmsSchemaVerification
|
||||
{
|
||||
/**
|
||||
* The Liquibase history (DATABASECHANGELOG) records every changeset and the tables exist
|
||||
* (the original check). Needs read access to the history table, which a DML-only
|
||||
* runtime identity does not have.
|
||||
*/
|
||||
CHANGELOG_HISTORY,
|
||||
|
||||
/**
|
||||
* Only the database catalog: every table of the installed features exists and has every
|
||||
* column the qbit's metadata maps. Needs no Liquibase tables and no privilege beyond
|
||||
* connecting, so it is the check for a runtime identity that owns nothing and may not
|
||||
* read migration history.
|
||||
*/
|
||||
CATALOG
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.cms;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** What the qbit does when its startup schema check finds the database
|
||||
** behind the code (spec section 3.4, the "install doctor").
|
||||
*******************************************************************************/
|
||||
public enum CmsStartupMode
|
||||
{
|
||||
/** Refuse to start, listing what is missing (the production default). */
|
||||
FAIL_FAST,
|
||||
|
||||
/** Start, log the problems and show them on the CMS home. */
|
||||
DEGRADED,
|
||||
|
||||
/** Do not check (unit tests that build meta data without a database). */
|
||||
SKIP
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.cms;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import com.kingsrook.qbits.cms.core.model.CmsDomain;
|
||||
import com.kingsrook.qbits.cms.core.model.CmsMenu;
|
||||
import com.kingsrook.qbits.cms.core.model.CmsMenuItem;
|
||||
import com.kingsrook.qbits.cms.core.model.CmsPage;
|
||||
import com.kingsrook.qbits.cms.core.model.CmsPageSection;
|
||||
import com.kingsrook.qbits.cms.core.model.CmsPageSectionType;
|
||||
import com.kingsrook.qbits.cms.core.model.CmsPageType;
|
||||
import com.kingsrook.qbits.cms.core.model.CmsRedirect;
|
||||
import com.kingsrook.qbits.cms.core.model.CmsRedirectType;
|
||||
import com.kingsrook.qbits.cms.core.model.CmsSite;
|
||||
import com.kingsrook.qbits.cms.core.model.CmsSnippet;
|
||||
import com.kingsrook.qbits.cms.metadata.CmsTableDefinition;
|
||||
import com.kingsrook.qbits.cms.metadata.CmsTableDefinition.Kind;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The CMS tables: logical names (the physical name is the configured prefix
|
||||
** plus the logical name, CmsQBitConfig.applyPrefix) and their definitions.
|
||||
** Java code names CMS tables only through these constants and the config, so
|
||||
** a host can choose any prefix (spec section 2.4).
|
||||
*******************************************************************************/
|
||||
public final class CmsTables
|
||||
{
|
||||
public static final String SITE = CmsSite.TABLE_NAME;
|
||||
public static final String DOMAIN = CmsDomain.TABLE_NAME;
|
||||
public static final String REDIRECT = CmsRedirect.TABLE_NAME;
|
||||
public static final String REDIRECT_TYPE = CmsRedirectType.TABLE_NAME;
|
||||
public static final String PAGE = CmsPage.TABLE_NAME;
|
||||
public static final String PAGE_TYPE = CmsPageType.TABLE_NAME;
|
||||
public static final String PAGE_SECTION = CmsPageSection.TABLE_NAME;
|
||||
public static final String PAGE_SECTION_TYPE = CmsPageSectionType.TABLE_NAME;
|
||||
public static final String SNIPPET = CmsSnippet.TABLE_NAME;
|
||||
public static final String MENU = CmsMenu.TABLE_NAME;
|
||||
public static final String MENU_ITEM = CmsMenuItem.TABLE_NAME;
|
||||
|
||||
private static final List<CmsTableDefinition> DEFINITIONS = List.of(
|
||||
CmsTableDefinition.builder(SITE, CmsSite.class, CmsFeature.CORE, "Site", "language", Kind.MAIN)
|
||||
.recordLabel("name")
|
||||
.section("site", "Site", "language", Tier.T1, "id", "name", "shortName", "description")
|
||||
.child(DOMAIN, "siteId", "Domains", null, "name")
|
||||
.unique("name")
|
||||
.build(),
|
||||
CmsTableDefinition.builder(DOMAIN, CmsDomain.class, CmsFeature.CORE, "Domain", "dns", Kind.CHILD)
|
||||
.recordLabel("fqdn")
|
||||
.section("domain", "Domain", "dns", Tier.T1, "id", "fqdn", "siteId", "isPrimary", "name", "shortName", "description")
|
||||
.unique("shortName").unique("name").unique("fqdn")
|
||||
.build(),
|
||||
CmsTableDefinition.builder(REDIRECT, CmsRedirect.class, CmsFeature.CORE, "Redirect", "open_in_new", Kind.MAIN)
|
||||
.section("redirect", "Redirect", "open_in_new", Tier.T1, "id", "name", "sourcePath", "targetPath", "redirectTypeId", "siteId", "description")
|
||||
.section("matching", "Matching", "tune", Tier.T2, "matchType", "priority", "preserveQueryParams", "isActive", "expiryDate")
|
||||
.section("usage", "Usage", "query_stats", Tier.T2, "hitCount", "lastHitDate")
|
||||
.build(),
|
||||
CmsTableDefinition.builder(REDIRECT_TYPE, CmsRedirectType.class, CmsFeature.CORE, "Redirect Type", "alt_route", Kind.REFERENCE)
|
||||
.section("redirectType", "Redirect Type", "alt_route", Tier.T1, "id", "name", "code", "httpStatusCode", "isActive", "sortOrder", "description")
|
||||
.unique("code")
|
||||
.build(),
|
||||
CmsTableDefinition.builder(PAGE, CmsPage.class, CmsFeature.CORE, "Page", "description", Kind.MAIN)
|
||||
.recordLabel("title")
|
||||
.section("page", "Page", "description", Tier.T1, "id", "title", "name", "slug", "siteId", "pageTypeId", "template", "isPublished", "publishDate")
|
||||
.section("content", "Content", "notes", Tier.T2, "content")
|
||||
.section("structure", "Structure", "account_tree", Tier.T2, "parentPageId", "sortOrder")
|
||||
.section("seo", "SEO", "travel_explore", Tier.T2, "metaDescription", "metaKeywords", "ogImage")
|
||||
.child(PAGE_SECTION, "pageId", "Sections", "sections", "sortOrder")
|
||||
.child(PAGE, "parentPageId", "Child Pages", null, "sortOrder")
|
||||
.unique("slug", "siteId").unique("slug", "parentPageId")
|
||||
.text("content")
|
||||
.build(),
|
||||
CmsTableDefinition.builder(PAGE_TYPE, CmsPageType.class, CmsFeature.CORE, "Page Type", "layers", Kind.REFERENCE)
|
||||
.section("pageType", "Page Type", "layers", Tier.T1, "id", "name", "code", "isActive", "sortOrder", "description")
|
||||
.unique("name").unique("code")
|
||||
.build(),
|
||||
CmsTableDefinition.builder(PAGE_SECTION, CmsPageSection.class, CmsFeature.CORE, "Page Section", "view_agenda", Kind.CHILD)
|
||||
.section("pageSection", "Page Section", "view_agenda", Tier.T1, "id", "name", "pageId", "sectionTypeId", "sortOrder", "cssClassName")
|
||||
.section("content", "Content", "notes", Tier.T2, "content")
|
||||
.text("content")
|
||||
.build(),
|
||||
CmsTableDefinition.builder(PAGE_SECTION_TYPE, CmsPageSectionType.class, CmsFeature.CORE, "Page Section Type", "category", Kind.REFERENCE)
|
||||
.section("pageSectionType", "Page Section Type", "category", Tier.T1, "id", "name", "code", "isActive", "sortOrder", "description")
|
||||
.unique("code").unique("name")
|
||||
.build(),
|
||||
CmsTableDefinition.builder(SNIPPET, CmsSnippet.class, CmsFeature.CORE, "Snippet", "code", Kind.MAIN)
|
||||
.section("snippet", "Snippet", "code", Tier.T1, "id", "name", "shortName", "siteId", "isActive", "sortOrder", "cssClassName")
|
||||
.section("content", "Content", "notes", Tier.T2, "content")
|
||||
.unique("shortName", "siteId")
|
||||
.text("content")
|
||||
.build(),
|
||||
CmsTableDefinition.builder(MENU, CmsMenu.class, CmsFeature.CORE, "Menu", "menu", Kind.MAIN)
|
||||
.section("menu", "Menu", "menu", Tier.T1, "id", "name", "slug", "menuType", "siteId", "isActive", "description")
|
||||
.child(MENU_ITEM, "menuId", "Items", "items", "displayOrder")
|
||||
.unique("slug", "siteId")
|
||||
.build(),
|
||||
CmsTableDefinition.builder(MENU_ITEM, CmsMenuItem.class, CmsFeature.CORE, "Menu Item", "link", Kind.CHILD)
|
||||
.section("menuItem", "Menu Item", "link", Tier.T1, "id", "name", "slug", "menuId", "parentId", "itemType", "displayOrder", "isActive")
|
||||
.section("target", "Link", "open_in_new", Tier.T2, "linkUrl", "routePath", "target")
|
||||
.section("display", "Display", "palette", Tier.T2, "icon", "cssClasses", "description")
|
||||
.child(MENU_ITEM, "parentId", "Child Items", null, "displayOrder")
|
||||
.build());
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Not instantiable.
|
||||
*******************************************************************************/
|
||||
private CmsTables()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Every table definition, in a stable order.
|
||||
*******************************************************************************/
|
||||
public static List<CmsTableDefinition> definitions()
|
||||
{
|
||||
return (DEFINITIONS);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The definitions of the features a config turns on.
|
||||
*******************************************************************************/
|
||||
public static List<CmsTableDefinition> definitions(CmsQBitConfig config)
|
||||
{
|
||||
return (DEFINITIONS.stream().filter(d -> config.hasFeature(d.feature())).toList());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** One definition by logical name.
|
||||
*******************************************************************************/
|
||||
public static Optional<CmsTableDefinition> definition(String logicalName)
|
||||
{
|
||||
return (DEFINITIONS.stream().filter(d -> d.logicalName().equals(logicalName)).findFirst());
|
||||
}
|
||||
}
|
||||
@@ -1,366 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.cms.core.model;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QField;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A host name (fully qualified domain name) of a site; one per site is
|
||||
** primary.
|
||||
**
|
||||
** The physical table name is the configured prefix plus TABLE_NAME (for
|
||||
** example cms_domain); possible-value source names on the fields are
|
||||
** logical and get the same prefix when the table is built.
|
||||
*******************************************************************************/
|
||||
public class CmsDomain extends QRecordEntity
|
||||
{
|
||||
public static final String TABLE_NAME = "domain";
|
||||
|
||||
@QField(isEditable = false, isPrimaryKey = true)
|
||||
private Long id;
|
||||
|
||||
@QField(isRequired = true, maxLength = 40, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String name;
|
||||
|
||||
@QField(isRequired = true, maxLength = 20, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String shortName;
|
||||
|
||||
@QField(maxLength = 255, valueTooLongBehavior = ValueTooLongBehavior.TRUNCATE, gridColumns = 12)
|
||||
private String description;
|
||||
|
||||
@QField(isRequired = true, maxLength = 255, valueTooLongBehavior = ValueTooLongBehavior.ERROR, label = "FQDN")
|
||||
private String fqdn;
|
||||
|
||||
@QField(isRequired = true, defaultValue = "false")
|
||||
private Boolean isPrimary;
|
||||
|
||||
@QField(isRequired = true, possibleValueSourceName = "site", label = "Site")
|
||||
private Long siteId;
|
||||
|
||||
@QField(isEditable = false, dynamicDefaultValueBehavior = DynamicDefaultValueBehavior.CREATE_DATE)
|
||||
private Instant createDate;
|
||||
|
||||
@QField(isEditable = false, dynamicDefaultValueBehavior = DynamicDefaultValueBehavior.MODIFY_DATE)
|
||||
private Instant modifyDate;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Default constructor
|
||||
*******************************************************************************/
|
||||
public CmsDomain()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor that takes a QRecord
|
||||
*******************************************************************************/
|
||||
public CmsDomain(QRecord record) throws QException
|
||||
{
|
||||
populateFromQRecord(record);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for id
|
||||
*******************************************************************************/
|
||||
public Long getId()
|
||||
{
|
||||
return (this.id);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for id
|
||||
*******************************************************************************/
|
||||
public void setId(Long id)
|
||||
{
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for id
|
||||
*******************************************************************************/
|
||||
public CmsDomain withId(Long id)
|
||||
{
|
||||
this.id = id;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for name
|
||||
*******************************************************************************/
|
||||
public String getName()
|
||||
{
|
||||
return (this.name);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for name
|
||||
*******************************************************************************/
|
||||
public void setName(String name)
|
||||
{
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for name
|
||||
*******************************************************************************/
|
||||
public CmsDomain withName(String name)
|
||||
{
|
||||
this.name = name;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for shortName
|
||||
*******************************************************************************/
|
||||
public String getShortName()
|
||||
{
|
||||
return (this.shortName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for shortName
|
||||
*******************************************************************************/
|
||||
public void setShortName(String shortName)
|
||||
{
|
||||
this.shortName = shortName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for shortName
|
||||
*******************************************************************************/
|
||||
public CmsDomain withShortName(String shortName)
|
||||
{
|
||||
this.shortName = shortName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for description
|
||||
*******************************************************************************/
|
||||
public String getDescription()
|
||||
{
|
||||
return (this.description);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for description
|
||||
*******************************************************************************/
|
||||
public void setDescription(String description)
|
||||
{
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for description
|
||||
*******************************************************************************/
|
||||
public CmsDomain withDescription(String description)
|
||||
{
|
||||
this.description = description;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for fqdn
|
||||
*******************************************************************************/
|
||||
public String getFqdn()
|
||||
{
|
||||
return (this.fqdn);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for fqdn
|
||||
*******************************************************************************/
|
||||
public void setFqdn(String fqdn)
|
||||
{
|
||||
this.fqdn = fqdn;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for fqdn
|
||||
*******************************************************************************/
|
||||
public CmsDomain withFqdn(String fqdn)
|
||||
{
|
||||
this.fqdn = fqdn;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for isPrimary
|
||||
*******************************************************************************/
|
||||
public Boolean getIsPrimary()
|
||||
{
|
||||
return (this.isPrimary);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for isPrimary
|
||||
*******************************************************************************/
|
||||
public void setIsPrimary(Boolean isPrimary)
|
||||
{
|
||||
this.isPrimary = isPrimary;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for isPrimary
|
||||
*******************************************************************************/
|
||||
public CmsDomain withIsPrimary(Boolean isPrimary)
|
||||
{
|
||||
this.isPrimary = isPrimary;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for siteId
|
||||
*******************************************************************************/
|
||||
public Long getSiteId()
|
||||
{
|
||||
return (this.siteId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for siteId
|
||||
*******************************************************************************/
|
||||
public void setSiteId(Long siteId)
|
||||
{
|
||||
this.siteId = siteId;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for siteId
|
||||
*******************************************************************************/
|
||||
public CmsDomain withSiteId(Long siteId)
|
||||
{
|
||||
this.siteId = siteId;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for createDate
|
||||
*******************************************************************************/
|
||||
public Instant getCreateDate()
|
||||
{
|
||||
return (this.createDate);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for createDate
|
||||
*******************************************************************************/
|
||||
public void setCreateDate(Instant createDate)
|
||||
{
|
||||
this.createDate = createDate;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for createDate
|
||||
*******************************************************************************/
|
||||
public CmsDomain withCreateDate(Instant createDate)
|
||||
{
|
||||
this.createDate = createDate;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for modifyDate
|
||||
*******************************************************************************/
|
||||
public Instant getModifyDate()
|
||||
{
|
||||
return (this.modifyDate);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for modifyDate
|
||||
*******************************************************************************/
|
||||
public void setModifyDate(Instant modifyDate)
|
||||
{
|
||||
this.modifyDate = modifyDate;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for modifyDate
|
||||
*******************************************************************************/
|
||||
public CmsDomain withModifyDate(Instant modifyDate)
|
||||
{
|
||||
this.modifyDate = modifyDate;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,365 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.cms.core.model;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QField;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A navigation menu of a site (for example the header or footer menu).
|
||||
**
|
||||
** The physical table name is the configured prefix plus TABLE_NAME (for
|
||||
** example cms_menu); possible-value source names on the fields are
|
||||
** logical and get the same prefix when the table is built.
|
||||
*******************************************************************************/
|
||||
public class CmsMenu extends QRecordEntity
|
||||
{
|
||||
public static final String TABLE_NAME = "menu";
|
||||
|
||||
@QField(isEditable = false, isPrimaryKey = true)
|
||||
private Long id;
|
||||
|
||||
@QField(isRequired = true, maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String name;
|
||||
|
||||
@QField(isRequired = true, maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String slug;
|
||||
|
||||
@QField(maxLength = 255, valueTooLongBehavior = ValueTooLongBehavior.ERROR, gridColumns = 12)
|
||||
private String description;
|
||||
|
||||
@QField(isRequired = true, possibleValueSourceName = "site", label = "Site")
|
||||
private Long siteId;
|
||||
|
||||
@QField(isRequired = true, maxLength = 50, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String menuType;
|
||||
|
||||
@QField(isRequired = true, defaultValue = "true")
|
||||
private Boolean isActive;
|
||||
|
||||
@QField(isEditable = false, dynamicDefaultValueBehavior = DynamicDefaultValueBehavior.CREATE_DATE)
|
||||
private Instant createDate;
|
||||
|
||||
@QField(isEditable = false, dynamicDefaultValueBehavior = DynamicDefaultValueBehavior.MODIFY_DATE)
|
||||
private Instant modifyDate;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Default constructor
|
||||
*******************************************************************************/
|
||||
public CmsMenu()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor that takes a QRecord
|
||||
*******************************************************************************/
|
||||
public CmsMenu(QRecord record) throws QException
|
||||
{
|
||||
populateFromQRecord(record);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for id
|
||||
*******************************************************************************/
|
||||
public Long getId()
|
||||
{
|
||||
return (this.id);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for id
|
||||
*******************************************************************************/
|
||||
public void setId(Long id)
|
||||
{
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for id
|
||||
*******************************************************************************/
|
||||
public CmsMenu withId(Long id)
|
||||
{
|
||||
this.id = id;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for name
|
||||
*******************************************************************************/
|
||||
public String getName()
|
||||
{
|
||||
return (this.name);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for name
|
||||
*******************************************************************************/
|
||||
public void setName(String name)
|
||||
{
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for name
|
||||
*******************************************************************************/
|
||||
public CmsMenu withName(String name)
|
||||
{
|
||||
this.name = name;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for slug
|
||||
*******************************************************************************/
|
||||
public String getSlug()
|
||||
{
|
||||
return (this.slug);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for slug
|
||||
*******************************************************************************/
|
||||
public void setSlug(String slug)
|
||||
{
|
||||
this.slug = slug;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for slug
|
||||
*******************************************************************************/
|
||||
public CmsMenu withSlug(String slug)
|
||||
{
|
||||
this.slug = slug;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for description
|
||||
*******************************************************************************/
|
||||
public String getDescription()
|
||||
{
|
||||
return (this.description);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for description
|
||||
*******************************************************************************/
|
||||
public void setDescription(String description)
|
||||
{
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for description
|
||||
*******************************************************************************/
|
||||
public CmsMenu withDescription(String description)
|
||||
{
|
||||
this.description = description;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for siteId
|
||||
*******************************************************************************/
|
||||
public Long getSiteId()
|
||||
{
|
||||
return (this.siteId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for siteId
|
||||
*******************************************************************************/
|
||||
public void setSiteId(Long siteId)
|
||||
{
|
||||
this.siteId = siteId;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for siteId
|
||||
*******************************************************************************/
|
||||
public CmsMenu withSiteId(Long siteId)
|
||||
{
|
||||
this.siteId = siteId;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for menuType
|
||||
*******************************************************************************/
|
||||
public String getMenuType()
|
||||
{
|
||||
return (this.menuType);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for menuType
|
||||
*******************************************************************************/
|
||||
public void setMenuType(String menuType)
|
||||
{
|
||||
this.menuType = menuType;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for menuType
|
||||
*******************************************************************************/
|
||||
public CmsMenu withMenuType(String menuType)
|
||||
{
|
||||
this.menuType = menuType;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for isActive
|
||||
*******************************************************************************/
|
||||
public Boolean getIsActive()
|
||||
{
|
||||
return (this.isActive);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for isActive
|
||||
*******************************************************************************/
|
||||
public void setIsActive(Boolean isActive)
|
||||
{
|
||||
this.isActive = isActive;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for isActive
|
||||
*******************************************************************************/
|
||||
public CmsMenu withIsActive(Boolean isActive)
|
||||
{
|
||||
this.isActive = isActive;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for createDate
|
||||
*******************************************************************************/
|
||||
public Instant getCreateDate()
|
||||
{
|
||||
return (this.createDate);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for createDate
|
||||
*******************************************************************************/
|
||||
public void setCreateDate(Instant createDate)
|
||||
{
|
||||
this.createDate = createDate;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for createDate
|
||||
*******************************************************************************/
|
||||
public CmsMenu withCreateDate(Instant createDate)
|
||||
{
|
||||
this.createDate = createDate;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for modifyDate
|
||||
*******************************************************************************/
|
||||
public Instant getModifyDate()
|
||||
{
|
||||
return (this.modifyDate);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for modifyDate
|
||||
*******************************************************************************/
|
||||
public void setModifyDate(Instant modifyDate)
|
||||
{
|
||||
this.modifyDate = modifyDate;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for modifyDate
|
||||
*******************************************************************************/
|
||||
public CmsMenu withModifyDate(Instant modifyDate)
|
||||
{
|
||||
this.modifyDate = modifyDate;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,604 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.cms.core.model;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QField;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** One entry of a menu: a link or route, ordered, optionally nested under a
|
||||
** parent item.
|
||||
**
|
||||
** The physical table name is the configured prefix plus TABLE_NAME (for
|
||||
** example cms_menu_item); possible-value source names on the fields are
|
||||
** logical and get the same prefix when the table is built.
|
||||
*******************************************************************************/
|
||||
public class CmsMenuItem extends QRecordEntity
|
||||
{
|
||||
public static final String TABLE_NAME = "menu_item";
|
||||
|
||||
@QField(isEditable = false, isPrimaryKey = true)
|
||||
private Long id;
|
||||
|
||||
@QField(isRequired = true, possibleValueSourceName = "menu", label = "Menu")
|
||||
private Long menuId;
|
||||
|
||||
@QField(possibleValueSourceName = "menu_item", label = "Parent Item")
|
||||
private Long parentId;
|
||||
|
||||
@QField(isRequired = true, maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String name;
|
||||
|
||||
@QField(isRequired = true, maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String slug;
|
||||
|
||||
@QField(isRequired = true, maxLength = 50, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String itemType;
|
||||
|
||||
@QField(maxLength = 500, valueTooLongBehavior = ValueTooLongBehavior.ERROR, label = "Link URL", gridColumns = 12)
|
||||
private String linkUrl;
|
||||
|
||||
@QField(maxLength = 200, valueTooLongBehavior = ValueTooLongBehavior.ERROR, gridColumns = 12)
|
||||
private String routePath;
|
||||
|
||||
@QField(maxLength = 20, valueTooLongBehavior = ValueTooLongBehavior.ERROR, defaultValue = "_self")
|
||||
private String target;
|
||||
|
||||
@QField(maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String icon;
|
||||
|
||||
@QField(maxLength = 255, valueTooLongBehavior = ValueTooLongBehavior.ERROR, gridColumns = 12)
|
||||
private String description;
|
||||
|
||||
@QField(isRequired = true, defaultValue = "0")
|
||||
private Integer displayOrder;
|
||||
|
||||
@QField(isRequired = true, defaultValue = "true")
|
||||
private Boolean isActive;
|
||||
|
||||
@QField(maxLength = 200, valueTooLongBehavior = ValueTooLongBehavior.ERROR, label = "CSS Classes")
|
||||
private String cssClasses;
|
||||
|
||||
@QField(isEditable = false, dynamicDefaultValueBehavior = DynamicDefaultValueBehavior.CREATE_DATE)
|
||||
private Instant createDate;
|
||||
|
||||
@QField(isEditable = false, dynamicDefaultValueBehavior = DynamicDefaultValueBehavior.MODIFY_DATE)
|
||||
private Instant modifyDate;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Default constructor
|
||||
*******************************************************************************/
|
||||
public CmsMenuItem()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor that takes a QRecord
|
||||
*******************************************************************************/
|
||||
public CmsMenuItem(QRecord record) throws QException
|
||||
{
|
||||
populateFromQRecord(record);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for id
|
||||
*******************************************************************************/
|
||||
public Long getId()
|
||||
{
|
||||
return (this.id);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for id
|
||||
*******************************************************************************/
|
||||
public void setId(Long id)
|
||||
{
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for id
|
||||
*******************************************************************************/
|
||||
public CmsMenuItem withId(Long id)
|
||||
{
|
||||
this.id = id;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for menuId
|
||||
*******************************************************************************/
|
||||
public Long getMenuId()
|
||||
{
|
||||
return (this.menuId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for menuId
|
||||
*******************************************************************************/
|
||||
public void setMenuId(Long menuId)
|
||||
{
|
||||
this.menuId = menuId;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for menuId
|
||||
*******************************************************************************/
|
||||
public CmsMenuItem withMenuId(Long menuId)
|
||||
{
|
||||
this.menuId = menuId;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for parentId
|
||||
*******************************************************************************/
|
||||
public Long getParentId()
|
||||
{
|
||||
return (this.parentId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for parentId
|
||||
*******************************************************************************/
|
||||
public void setParentId(Long parentId)
|
||||
{
|
||||
this.parentId = parentId;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for parentId
|
||||
*******************************************************************************/
|
||||
public CmsMenuItem withParentId(Long parentId)
|
||||
{
|
||||
this.parentId = parentId;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for name
|
||||
*******************************************************************************/
|
||||
public String getName()
|
||||
{
|
||||
return (this.name);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for name
|
||||
*******************************************************************************/
|
||||
public void setName(String name)
|
||||
{
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for name
|
||||
*******************************************************************************/
|
||||
public CmsMenuItem withName(String name)
|
||||
{
|
||||
this.name = name;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for slug
|
||||
*******************************************************************************/
|
||||
public String getSlug()
|
||||
{
|
||||
return (this.slug);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for slug
|
||||
*******************************************************************************/
|
||||
public void setSlug(String slug)
|
||||
{
|
||||
this.slug = slug;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for slug
|
||||
*******************************************************************************/
|
||||
public CmsMenuItem withSlug(String slug)
|
||||
{
|
||||
this.slug = slug;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for itemType
|
||||
*******************************************************************************/
|
||||
public String getItemType()
|
||||
{
|
||||
return (this.itemType);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for itemType
|
||||
*******************************************************************************/
|
||||
public void setItemType(String itemType)
|
||||
{
|
||||
this.itemType = itemType;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for itemType
|
||||
*******************************************************************************/
|
||||
public CmsMenuItem withItemType(String itemType)
|
||||
{
|
||||
this.itemType = itemType;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for linkUrl
|
||||
*******************************************************************************/
|
||||
public String getLinkUrl()
|
||||
{
|
||||
return (this.linkUrl);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for linkUrl
|
||||
*******************************************************************************/
|
||||
public void setLinkUrl(String linkUrl)
|
||||
{
|
||||
this.linkUrl = linkUrl;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for linkUrl
|
||||
*******************************************************************************/
|
||||
public CmsMenuItem withLinkUrl(String linkUrl)
|
||||
{
|
||||
this.linkUrl = linkUrl;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for routePath
|
||||
*******************************************************************************/
|
||||
public String getRoutePath()
|
||||
{
|
||||
return (this.routePath);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for routePath
|
||||
*******************************************************************************/
|
||||
public void setRoutePath(String routePath)
|
||||
{
|
||||
this.routePath = routePath;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for routePath
|
||||
*******************************************************************************/
|
||||
public CmsMenuItem withRoutePath(String routePath)
|
||||
{
|
||||
this.routePath = routePath;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for target
|
||||
*******************************************************************************/
|
||||
public String getTarget()
|
||||
{
|
||||
return (this.target);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for target
|
||||
*******************************************************************************/
|
||||
public void setTarget(String target)
|
||||
{
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for target
|
||||
*******************************************************************************/
|
||||
public CmsMenuItem withTarget(String target)
|
||||
{
|
||||
this.target = target;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for icon
|
||||
*******************************************************************************/
|
||||
public String getIcon()
|
||||
{
|
||||
return (this.icon);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for icon
|
||||
*******************************************************************************/
|
||||
public void setIcon(String icon)
|
||||
{
|
||||
this.icon = icon;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for icon
|
||||
*******************************************************************************/
|
||||
public CmsMenuItem withIcon(String icon)
|
||||
{
|
||||
this.icon = icon;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for description
|
||||
*******************************************************************************/
|
||||
public String getDescription()
|
||||
{
|
||||
return (this.description);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for description
|
||||
*******************************************************************************/
|
||||
public void setDescription(String description)
|
||||
{
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for description
|
||||
*******************************************************************************/
|
||||
public CmsMenuItem withDescription(String description)
|
||||
{
|
||||
this.description = description;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for displayOrder
|
||||
*******************************************************************************/
|
||||
public Integer getDisplayOrder()
|
||||
{
|
||||
return (this.displayOrder);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for displayOrder
|
||||
*******************************************************************************/
|
||||
public void setDisplayOrder(Integer displayOrder)
|
||||
{
|
||||
this.displayOrder = displayOrder;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for displayOrder
|
||||
*******************************************************************************/
|
||||
public CmsMenuItem withDisplayOrder(Integer displayOrder)
|
||||
{
|
||||
this.displayOrder = displayOrder;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for isActive
|
||||
*******************************************************************************/
|
||||
public Boolean getIsActive()
|
||||
{
|
||||
return (this.isActive);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for isActive
|
||||
*******************************************************************************/
|
||||
public void setIsActive(Boolean isActive)
|
||||
{
|
||||
this.isActive = isActive;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for isActive
|
||||
*******************************************************************************/
|
||||
public CmsMenuItem withIsActive(Boolean isActive)
|
||||
{
|
||||
this.isActive = isActive;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for cssClasses
|
||||
*******************************************************************************/
|
||||
public String getCssClasses()
|
||||
{
|
||||
return (this.cssClasses);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for cssClasses
|
||||
*******************************************************************************/
|
||||
public void setCssClasses(String cssClasses)
|
||||
{
|
||||
this.cssClasses = cssClasses;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for cssClasses
|
||||
*******************************************************************************/
|
||||
public CmsMenuItem withCssClasses(String cssClasses)
|
||||
{
|
||||
this.cssClasses = cssClasses;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for createDate
|
||||
*******************************************************************************/
|
||||
public Instant getCreateDate()
|
||||
{
|
||||
return (this.createDate);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for createDate
|
||||
*******************************************************************************/
|
||||
public void setCreateDate(Instant createDate)
|
||||
{
|
||||
this.createDate = createDate;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for createDate
|
||||
*******************************************************************************/
|
||||
public CmsMenuItem withCreateDate(Instant createDate)
|
||||
{
|
||||
this.createDate = createDate;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for modifyDate
|
||||
*******************************************************************************/
|
||||
public Instant getModifyDate()
|
||||
{
|
||||
return (this.modifyDate);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for modifyDate
|
||||
*******************************************************************************/
|
||||
public void setModifyDate(Instant modifyDate)
|
||||
{
|
||||
this.modifyDate = modifyDate;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for modifyDate
|
||||
*******************************************************************************/
|
||||
public CmsMenuItem withModifyDate(Instant modifyDate)
|
||||
{
|
||||
this.modifyDate = modifyDate;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,638 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.cms.core.model;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QField;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A page of a site, made of ordered page sections; pages nest under a parent
|
||||
** page.
|
||||
**
|
||||
** The physical table name is the configured prefix plus TABLE_NAME (for
|
||||
** example cms_page); possible-value source names on the fields are
|
||||
** logical and get the same prefix when the table is built.
|
||||
*******************************************************************************/
|
||||
public class CmsPage extends QRecordEntity
|
||||
{
|
||||
public static final String TABLE_NAME = "page";
|
||||
|
||||
@QField(isEditable = false, isPrimaryKey = true)
|
||||
private Long id;
|
||||
|
||||
@QField(isRequired = true, maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String name;
|
||||
|
||||
@QField(isRequired = true, maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String slug;
|
||||
|
||||
@QField(isRequired = true, maxLength = 200, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String title;
|
||||
|
||||
@QField(gridColumns = 12)
|
||||
private String content;
|
||||
|
||||
@QField(isRequired = true, possibleValueSourceName = "page_type", label = "Page Type")
|
||||
private Long pageTypeId;
|
||||
|
||||
@QField(maxLength = 50, valueTooLongBehavior = ValueTooLongBehavior.ERROR, defaultValue = "default")
|
||||
private String template;
|
||||
|
||||
@QField(isRequired = true, defaultValue = "false")
|
||||
private Boolean isPublished;
|
||||
|
||||
@QField()
|
||||
private Instant publishDate;
|
||||
|
||||
@QField(isRequired = true, defaultValue = "0")
|
||||
private Integer sortOrder;
|
||||
|
||||
@QField(possibleValueSourceName = "page", label = "Parent Page")
|
||||
private Long parentPageId;
|
||||
|
||||
@QField(isRequired = true, possibleValueSourceName = "site", label = "Site")
|
||||
private Long siteId;
|
||||
|
||||
@QField(maxLength = 500, valueTooLongBehavior = ValueTooLongBehavior.TRUNCATE, gridColumns = 12)
|
||||
private String metaDescription;
|
||||
|
||||
@QField(maxLength = 255, valueTooLongBehavior = ValueTooLongBehavior.TRUNCATE, gridColumns = 12)
|
||||
private String metaKeywords;
|
||||
|
||||
@QField(maxLength = 255, valueTooLongBehavior = ValueTooLongBehavior.TRUNCATE, label = "Open Graph Image", gridColumns = 12)
|
||||
private String ogImage;
|
||||
|
||||
@QField(isEditable = false, dynamicDefaultValueBehavior = DynamicDefaultValueBehavior.CREATE_DATE)
|
||||
private Instant createDate;
|
||||
|
||||
@QField(isEditable = false, dynamicDefaultValueBehavior = DynamicDefaultValueBehavior.MODIFY_DATE)
|
||||
private Instant modifyDate;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Default constructor
|
||||
*******************************************************************************/
|
||||
public CmsPage()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor that takes a QRecord
|
||||
*******************************************************************************/
|
||||
public CmsPage(QRecord record) throws QException
|
||||
{
|
||||
populateFromQRecord(record);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for id
|
||||
*******************************************************************************/
|
||||
public Long getId()
|
||||
{
|
||||
return (this.id);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for id
|
||||
*******************************************************************************/
|
||||
public void setId(Long id)
|
||||
{
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for id
|
||||
*******************************************************************************/
|
||||
public CmsPage withId(Long id)
|
||||
{
|
||||
this.id = id;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for name
|
||||
*******************************************************************************/
|
||||
public String getName()
|
||||
{
|
||||
return (this.name);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for name
|
||||
*******************************************************************************/
|
||||
public void setName(String name)
|
||||
{
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for name
|
||||
*******************************************************************************/
|
||||
public CmsPage withName(String name)
|
||||
{
|
||||
this.name = name;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for slug
|
||||
*******************************************************************************/
|
||||
public String getSlug()
|
||||
{
|
||||
return (this.slug);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for slug
|
||||
*******************************************************************************/
|
||||
public void setSlug(String slug)
|
||||
{
|
||||
this.slug = slug;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for slug
|
||||
*******************************************************************************/
|
||||
public CmsPage withSlug(String slug)
|
||||
{
|
||||
this.slug = slug;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for title
|
||||
*******************************************************************************/
|
||||
public String getTitle()
|
||||
{
|
||||
return (this.title);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for title
|
||||
*******************************************************************************/
|
||||
public void setTitle(String title)
|
||||
{
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for title
|
||||
*******************************************************************************/
|
||||
public CmsPage withTitle(String title)
|
||||
{
|
||||
this.title = title;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for content
|
||||
*******************************************************************************/
|
||||
public String getContent()
|
||||
{
|
||||
return (this.content);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for content
|
||||
*******************************************************************************/
|
||||
public void setContent(String content)
|
||||
{
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for content
|
||||
*******************************************************************************/
|
||||
public CmsPage withContent(String content)
|
||||
{
|
||||
this.content = content;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for pageTypeId
|
||||
*******************************************************************************/
|
||||
public Long getPageTypeId()
|
||||
{
|
||||
return (this.pageTypeId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for pageTypeId
|
||||
*******************************************************************************/
|
||||
public void setPageTypeId(Long pageTypeId)
|
||||
{
|
||||
this.pageTypeId = pageTypeId;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for pageTypeId
|
||||
*******************************************************************************/
|
||||
public CmsPage withPageTypeId(Long pageTypeId)
|
||||
{
|
||||
this.pageTypeId = pageTypeId;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for template
|
||||
*******************************************************************************/
|
||||
public String getTemplate()
|
||||
{
|
||||
return (this.template);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for template
|
||||
*******************************************************************************/
|
||||
public void setTemplate(String template)
|
||||
{
|
||||
this.template = template;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for template
|
||||
*******************************************************************************/
|
||||
public CmsPage withTemplate(String template)
|
||||
{
|
||||
this.template = template;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for isPublished
|
||||
*******************************************************************************/
|
||||
public Boolean getIsPublished()
|
||||
{
|
||||
return (this.isPublished);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for isPublished
|
||||
*******************************************************************************/
|
||||
public void setIsPublished(Boolean isPublished)
|
||||
{
|
||||
this.isPublished = isPublished;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for isPublished
|
||||
*******************************************************************************/
|
||||
public CmsPage withIsPublished(Boolean isPublished)
|
||||
{
|
||||
this.isPublished = isPublished;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for publishDate
|
||||
*******************************************************************************/
|
||||
public Instant getPublishDate()
|
||||
{
|
||||
return (this.publishDate);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for publishDate
|
||||
*******************************************************************************/
|
||||
public void setPublishDate(Instant publishDate)
|
||||
{
|
||||
this.publishDate = publishDate;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for publishDate
|
||||
*******************************************************************************/
|
||||
public CmsPage withPublishDate(Instant publishDate)
|
||||
{
|
||||
this.publishDate = publishDate;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for sortOrder
|
||||
*******************************************************************************/
|
||||
public Integer getSortOrder()
|
||||
{
|
||||
return (this.sortOrder);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for sortOrder
|
||||
*******************************************************************************/
|
||||
public void setSortOrder(Integer sortOrder)
|
||||
{
|
||||
this.sortOrder = sortOrder;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for sortOrder
|
||||
*******************************************************************************/
|
||||
public CmsPage withSortOrder(Integer sortOrder)
|
||||
{
|
||||
this.sortOrder = sortOrder;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for parentPageId
|
||||
*******************************************************************************/
|
||||
public Long getParentPageId()
|
||||
{
|
||||
return (this.parentPageId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for parentPageId
|
||||
*******************************************************************************/
|
||||
public void setParentPageId(Long parentPageId)
|
||||
{
|
||||
this.parentPageId = parentPageId;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for parentPageId
|
||||
*******************************************************************************/
|
||||
public CmsPage withParentPageId(Long parentPageId)
|
||||
{
|
||||
this.parentPageId = parentPageId;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for siteId
|
||||
*******************************************************************************/
|
||||
public Long getSiteId()
|
||||
{
|
||||
return (this.siteId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for siteId
|
||||
*******************************************************************************/
|
||||
public void setSiteId(Long siteId)
|
||||
{
|
||||
this.siteId = siteId;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for siteId
|
||||
*******************************************************************************/
|
||||
public CmsPage withSiteId(Long siteId)
|
||||
{
|
||||
this.siteId = siteId;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for metaDescription
|
||||
*******************************************************************************/
|
||||
public String getMetaDescription()
|
||||
{
|
||||
return (this.metaDescription);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for metaDescription
|
||||
*******************************************************************************/
|
||||
public void setMetaDescription(String metaDescription)
|
||||
{
|
||||
this.metaDescription = metaDescription;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for metaDescription
|
||||
*******************************************************************************/
|
||||
public CmsPage withMetaDescription(String metaDescription)
|
||||
{
|
||||
this.metaDescription = metaDescription;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for metaKeywords
|
||||
*******************************************************************************/
|
||||
public String getMetaKeywords()
|
||||
{
|
||||
return (this.metaKeywords);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for metaKeywords
|
||||
*******************************************************************************/
|
||||
public void setMetaKeywords(String metaKeywords)
|
||||
{
|
||||
this.metaKeywords = metaKeywords;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for metaKeywords
|
||||
*******************************************************************************/
|
||||
public CmsPage withMetaKeywords(String metaKeywords)
|
||||
{
|
||||
this.metaKeywords = metaKeywords;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for ogImage
|
||||
*******************************************************************************/
|
||||
public String getOgImage()
|
||||
{
|
||||
return (this.ogImage);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for ogImage
|
||||
*******************************************************************************/
|
||||
public void setOgImage(String ogImage)
|
||||
{
|
||||
this.ogImage = ogImage;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for ogImage
|
||||
*******************************************************************************/
|
||||
public CmsPage withOgImage(String ogImage)
|
||||
{
|
||||
this.ogImage = ogImage;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for createDate
|
||||
*******************************************************************************/
|
||||
public Instant getCreateDate()
|
||||
{
|
||||
return (this.createDate);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for createDate
|
||||
*******************************************************************************/
|
||||
public void setCreateDate(Instant createDate)
|
||||
{
|
||||
this.createDate = createDate;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for createDate
|
||||
*******************************************************************************/
|
||||
public CmsPage withCreateDate(Instant createDate)
|
||||
{
|
||||
this.createDate = createDate;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for modifyDate
|
||||
*******************************************************************************/
|
||||
public Instant getModifyDate()
|
||||
{
|
||||
return (this.modifyDate);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for modifyDate
|
||||
*******************************************************************************/
|
||||
public void setModifyDate(Instant modifyDate)
|
||||
{
|
||||
this.modifyDate = modifyDate;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for modifyDate
|
||||
*******************************************************************************/
|
||||
public CmsPage withModifyDate(Instant modifyDate)
|
||||
{
|
||||
this.modifyDate = modifyDate;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,366 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.cms.core.model;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QField;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** One ordered section of a page; its content is interpreted by its section
|
||||
** type.
|
||||
**
|
||||
** The physical table name is the configured prefix plus TABLE_NAME (for
|
||||
** example cms_page_section); possible-value source names on the fields are
|
||||
** logical and get the same prefix when the table is built.
|
||||
*******************************************************************************/
|
||||
public class CmsPageSection extends QRecordEntity
|
||||
{
|
||||
public static final String TABLE_NAME = "page_section";
|
||||
|
||||
@QField(isEditable = false, isPrimaryKey = true)
|
||||
private Long id;
|
||||
|
||||
@QField(isRequired = true, maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String name;
|
||||
|
||||
@QField(maxLength = 255, valueTooLongBehavior = ValueTooLongBehavior.ERROR, label = "CSS Class Name")
|
||||
private String cssClassName;
|
||||
|
||||
@QField(isRequired = true, possibleValueSourceName = "page_section_type", label = "Section Type")
|
||||
private Long sectionTypeId;
|
||||
|
||||
@QField(gridColumns = 12)
|
||||
private String content;
|
||||
|
||||
@QField(isRequired = true, defaultValue = "0")
|
||||
private Integer sortOrder;
|
||||
|
||||
@QField(isRequired = true, possibleValueSourceName = "page", label = "Page")
|
||||
private Long pageId;
|
||||
|
||||
@QField(isEditable = false, dynamicDefaultValueBehavior = DynamicDefaultValueBehavior.CREATE_DATE)
|
||||
private Instant createDate;
|
||||
|
||||
@QField(isEditable = false, dynamicDefaultValueBehavior = DynamicDefaultValueBehavior.MODIFY_DATE)
|
||||
private Instant modifyDate;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Default constructor
|
||||
*******************************************************************************/
|
||||
public CmsPageSection()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor that takes a QRecord
|
||||
*******************************************************************************/
|
||||
public CmsPageSection(QRecord record) throws QException
|
||||
{
|
||||
populateFromQRecord(record);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for id
|
||||
*******************************************************************************/
|
||||
public Long getId()
|
||||
{
|
||||
return (this.id);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for id
|
||||
*******************************************************************************/
|
||||
public void setId(Long id)
|
||||
{
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for id
|
||||
*******************************************************************************/
|
||||
public CmsPageSection withId(Long id)
|
||||
{
|
||||
this.id = id;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for name
|
||||
*******************************************************************************/
|
||||
public String getName()
|
||||
{
|
||||
return (this.name);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for name
|
||||
*******************************************************************************/
|
||||
public void setName(String name)
|
||||
{
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for name
|
||||
*******************************************************************************/
|
||||
public CmsPageSection withName(String name)
|
||||
{
|
||||
this.name = name;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for cssClassName
|
||||
*******************************************************************************/
|
||||
public String getCssClassName()
|
||||
{
|
||||
return (this.cssClassName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for cssClassName
|
||||
*******************************************************************************/
|
||||
public void setCssClassName(String cssClassName)
|
||||
{
|
||||
this.cssClassName = cssClassName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for cssClassName
|
||||
*******************************************************************************/
|
||||
public CmsPageSection withCssClassName(String cssClassName)
|
||||
{
|
||||
this.cssClassName = cssClassName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for sectionTypeId
|
||||
*******************************************************************************/
|
||||
public Long getSectionTypeId()
|
||||
{
|
||||
return (this.sectionTypeId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for sectionTypeId
|
||||
*******************************************************************************/
|
||||
public void setSectionTypeId(Long sectionTypeId)
|
||||
{
|
||||
this.sectionTypeId = sectionTypeId;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for sectionTypeId
|
||||
*******************************************************************************/
|
||||
public CmsPageSection withSectionTypeId(Long sectionTypeId)
|
||||
{
|
||||
this.sectionTypeId = sectionTypeId;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for content
|
||||
*******************************************************************************/
|
||||
public String getContent()
|
||||
{
|
||||
return (this.content);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for content
|
||||
*******************************************************************************/
|
||||
public void setContent(String content)
|
||||
{
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for content
|
||||
*******************************************************************************/
|
||||
public CmsPageSection withContent(String content)
|
||||
{
|
||||
this.content = content;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for sortOrder
|
||||
*******************************************************************************/
|
||||
public Integer getSortOrder()
|
||||
{
|
||||
return (this.sortOrder);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for sortOrder
|
||||
*******************************************************************************/
|
||||
public void setSortOrder(Integer sortOrder)
|
||||
{
|
||||
this.sortOrder = sortOrder;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for sortOrder
|
||||
*******************************************************************************/
|
||||
public CmsPageSection withSortOrder(Integer sortOrder)
|
||||
{
|
||||
this.sortOrder = sortOrder;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for pageId
|
||||
*******************************************************************************/
|
||||
public Long getPageId()
|
||||
{
|
||||
return (this.pageId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for pageId
|
||||
*******************************************************************************/
|
||||
public void setPageId(Long pageId)
|
||||
{
|
||||
this.pageId = pageId;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for pageId
|
||||
*******************************************************************************/
|
||||
public CmsPageSection withPageId(Long pageId)
|
||||
{
|
||||
this.pageId = pageId;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for createDate
|
||||
*******************************************************************************/
|
||||
public Instant getCreateDate()
|
||||
{
|
||||
return (this.createDate);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for createDate
|
||||
*******************************************************************************/
|
||||
public void setCreateDate(Instant createDate)
|
||||
{
|
||||
this.createDate = createDate;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for createDate
|
||||
*******************************************************************************/
|
||||
public CmsPageSection withCreateDate(Instant createDate)
|
||||
{
|
||||
this.createDate = createDate;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for modifyDate
|
||||
*******************************************************************************/
|
||||
public Instant getModifyDate()
|
||||
{
|
||||
return (this.modifyDate);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for modifyDate
|
||||
*******************************************************************************/
|
||||
public void setModifyDate(Instant modifyDate)
|
||||
{
|
||||
this.modifyDate = modifyDate;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for modifyDate
|
||||
*******************************************************************************/
|
||||
public CmsPageSection withModifyDate(Instant modifyDate)
|
||||
{
|
||||
this.modifyDate = modifyDate;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,332 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.cms.core.model;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QField;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A kind of page section (reference data); the frontend renders a section by
|
||||
** its type code.
|
||||
**
|
||||
** The physical table name is the configured prefix plus TABLE_NAME (for
|
||||
** example cms_page_section_type); possible-value source names on the fields are
|
||||
** logical and get the same prefix when the table is built.
|
||||
*******************************************************************************/
|
||||
public class CmsPageSectionType extends QRecordEntity
|
||||
{
|
||||
public static final String TABLE_NAME = "page_section_type";
|
||||
|
||||
@QField(isEditable = false, isPrimaryKey = true)
|
||||
private Long id;
|
||||
|
||||
@QField(isRequired = true, maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String name;
|
||||
|
||||
@QField(isRequired = true, maxLength = 50, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String code;
|
||||
|
||||
@QField(maxLength = 255, valueTooLongBehavior = ValueTooLongBehavior.ERROR, gridColumns = 12)
|
||||
private String description;
|
||||
|
||||
@QField(isRequired = true, defaultValue = "true")
|
||||
private Boolean isActive;
|
||||
|
||||
@QField(isRequired = true, defaultValue = "0")
|
||||
private Integer sortOrder;
|
||||
|
||||
@QField(isEditable = false, dynamicDefaultValueBehavior = DynamicDefaultValueBehavior.CREATE_DATE)
|
||||
private Instant createDate;
|
||||
|
||||
@QField(isEditable = false, dynamicDefaultValueBehavior = DynamicDefaultValueBehavior.MODIFY_DATE)
|
||||
private Instant modifyDate;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Default constructor
|
||||
*******************************************************************************/
|
||||
public CmsPageSectionType()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor that takes a QRecord
|
||||
*******************************************************************************/
|
||||
public CmsPageSectionType(QRecord record) throws QException
|
||||
{
|
||||
populateFromQRecord(record);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for id
|
||||
*******************************************************************************/
|
||||
public Long getId()
|
||||
{
|
||||
return (this.id);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for id
|
||||
*******************************************************************************/
|
||||
public void setId(Long id)
|
||||
{
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for id
|
||||
*******************************************************************************/
|
||||
public CmsPageSectionType withId(Long id)
|
||||
{
|
||||
this.id = id;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for name
|
||||
*******************************************************************************/
|
||||
public String getName()
|
||||
{
|
||||
return (this.name);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for name
|
||||
*******************************************************************************/
|
||||
public void setName(String name)
|
||||
{
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for name
|
||||
*******************************************************************************/
|
||||
public CmsPageSectionType withName(String name)
|
||||
{
|
||||
this.name = name;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for code
|
||||
*******************************************************************************/
|
||||
public String getCode()
|
||||
{
|
||||
return (this.code);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for code
|
||||
*******************************************************************************/
|
||||
public void setCode(String code)
|
||||
{
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for code
|
||||
*******************************************************************************/
|
||||
public CmsPageSectionType withCode(String code)
|
||||
{
|
||||
this.code = code;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for description
|
||||
*******************************************************************************/
|
||||
public String getDescription()
|
||||
{
|
||||
return (this.description);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for description
|
||||
*******************************************************************************/
|
||||
public void setDescription(String description)
|
||||
{
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for description
|
||||
*******************************************************************************/
|
||||
public CmsPageSectionType withDescription(String description)
|
||||
{
|
||||
this.description = description;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for isActive
|
||||
*******************************************************************************/
|
||||
public Boolean getIsActive()
|
||||
{
|
||||
return (this.isActive);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for isActive
|
||||
*******************************************************************************/
|
||||
public void setIsActive(Boolean isActive)
|
||||
{
|
||||
this.isActive = isActive;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for isActive
|
||||
*******************************************************************************/
|
||||
public CmsPageSectionType withIsActive(Boolean isActive)
|
||||
{
|
||||
this.isActive = isActive;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for sortOrder
|
||||
*******************************************************************************/
|
||||
public Integer getSortOrder()
|
||||
{
|
||||
return (this.sortOrder);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for sortOrder
|
||||
*******************************************************************************/
|
||||
public void setSortOrder(Integer sortOrder)
|
||||
{
|
||||
this.sortOrder = sortOrder;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for sortOrder
|
||||
*******************************************************************************/
|
||||
public CmsPageSectionType withSortOrder(Integer sortOrder)
|
||||
{
|
||||
this.sortOrder = sortOrder;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for createDate
|
||||
*******************************************************************************/
|
||||
public Instant getCreateDate()
|
||||
{
|
||||
return (this.createDate);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for createDate
|
||||
*******************************************************************************/
|
||||
public void setCreateDate(Instant createDate)
|
||||
{
|
||||
this.createDate = createDate;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for createDate
|
||||
*******************************************************************************/
|
||||
public CmsPageSectionType withCreateDate(Instant createDate)
|
||||
{
|
||||
this.createDate = createDate;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for modifyDate
|
||||
*******************************************************************************/
|
||||
public Instant getModifyDate()
|
||||
{
|
||||
return (this.modifyDate);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for modifyDate
|
||||
*******************************************************************************/
|
||||
public void setModifyDate(Instant modifyDate)
|
||||
{
|
||||
this.modifyDate = modifyDate;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for modifyDate
|
||||
*******************************************************************************/
|
||||
public CmsPageSectionType withModifyDate(Instant modifyDate)
|
||||
{
|
||||
this.modifyDate = modifyDate;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,331 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.cms.core.model;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QField;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A kind of page (reference data).
|
||||
**
|
||||
** The physical table name is the configured prefix plus TABLE_NAME (for
|
||||
** example cms_page_type); possible-value source names on the fields are
|
||||
** logical and get the same prefix when the table is built.
|
||||
*******************************************************************************/
|
||||
public class CmsPageType extends QRecordEntity
|
||||
{
|
||||
public static final String TABLE_NAME = "page_type";
|
||||
|
||||
@QField(isEditable = false, isPrimaryKey = true)
|
||||
private Long id;
|
||||
|
||||
@QField(isRequired = true, maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String name;
|
||||
|
||||
@QField(isRequired = true, maxLength = 50, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String code;
|
||||
|
||||
@QField(maxLength = 255, valueTooLongBehavior = ValueTooLongBehavior.ERROR, gridColumns = 12)
|
||||
private String description;
|
||||
|
||||
@QField(isRequired = true, defaultValue = "true")
|
||||
private Boolean isActive;
|
||||
|
||||
@QField(isRequired = true, defaultValue = "0")
|
||||
private Integer sortOrder;
|
||||
|
||||
@QField(isEditable = false, dynamicDefaultValueBehavior = DynamicDefaultValueBehavior.CREATE_DATE)
|
||||
private Instant createDate;
|
||||
|
||||
@QField(isEditable = false, dynamicDefaultValueBehavior = DynamicDefaultValueBehavior.MODIFY_DATE)
|
||||
private Instant modifyDate;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Default constructor
|
||||
*******************************************************************************/
|
||||
public CmsPageType()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor that takes a QRecord
|
||||
*******************************************************************************/
|
||||
public CmsPageType(QRecord record) throws QException
|
||||
{
|
||||
populateFromQRecord(record);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for id
|
||||
*******************************************************************************/
|
||||
public Long getId()
|
||||
{
|
||||
return (this.id);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for id
|
||||
*******************************************************************************/
|
||||
public void setId(Long id)
|
||||
{
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for id
|
||||
*******************************************************************************/
|
||||
public CmsPageType withId(Long id)
|
||||
{
|
||||
this.id = id;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for name
|
||||
*******************************************************************************/
|
||||
public String getName()
|
||||
{
|
||||
return (this.name);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for name
|
||||
*******************************************************************************/
|
||||
public void setName(String name)
|
||||
{
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for name
|
||||
*******************************************************************************/
|
||||
public CmsPageType withName(String name)
|
||||
{
|
||||
this.name = name;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for code
|
||||
*******************************************************************************/
|
||||
public String getCode()
|
||||
{
|
||||
return (this.code);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for code
|
||||
*******************************************************************************/
|
||||
public void setCode(String code)
|
||||
{
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for code
|
||||
*******************************************************************************/
|
||||
public CmsPageType withCode(String code)
|
||||
{
|
||||
this.code = code;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for description
|
||||
*******************************************************************************/
|
||||
public String getDescription()
|
||||
{
|
||||
return (this.description);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for description
|
||||
*******************************************************************************/
|
||||
public void setDescription(String description)
|
||||
{
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for description
|
||||
*******************************************************************************/
|
||||
public CmsPageType withDescription(String description)
|
||||
{
|
||||
this.description = description;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for isActive
|
||||
*******************************************************************************/
|
||||
public Boolean getIsActive()
|
||||
{
|
||||
return (this.isActive);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for isActive
|
||||
*******************************************************************************/
|
||||
public void setIsActive(Boolean isActive)
|
||||
{
|
||||
this.isActive = isActive;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for isActive
|
||||
*******************************************************************************/
|
||||
public CmsPageType withIsActive(Boolean isActive)
|
||||
{
|
||||
this.isActive = isActive;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for sortOrder
|
||||
*******************************************************************************/
|
||||
public Integer getSortOrder()
|
||||
{
|
||||
return (this.sortOrder);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for sortOrder
|
||||
*******************************************************************************/
|
||||
public void setSortOrder(Integer sortOrder)
|
||||
{
|
||||
this.sortOrder = sortOrder;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for sortOrder
|
||||
*******************************************************************************/
|
||||
public CmsPageType withSortOrder(Integer sortOrder)
|
||||
{
|
||||
this.sortOrder = sortOrder;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for createDate
|
||||
*******************************************************************************/
|
||||
public Instant getCreateDate()
|
||||
{
|
||||
return (this.createDate);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for createDate
|
||||
*******************************************************************************/
|
||||
public void setCreateDate(Instant createDate)
|
||||
{
|
||||
this.createDate = createDate;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for createDate
|
||||
*******************************************************************************/
|
||||
public CmsPageType withCreateDate(Instant createDate)
|
||||
{
|
||||
this.createDate = createDate;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for modifyDate
|
||||
*******************************************************************************/
|
||||
public Instant getModifyDate()
|
||||
{
|
||||
return (this.modifyDate);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for modifyDate
|
||||
*******************************************************************************/
|
||||
public void setModifyDate(Instant modifyDate)
|
||||
{
|
||||
this.modifyDate = modifyDate;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for modifyDate
|
||||
*******************************************************************************/
|
||||
public CmsPageType withModifyDate(Instant modifyDate)
|
||||
{
|
||||
this.modifyDate = modifyDate;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,604 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.cms.core.model;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QField;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A public URL redirect: requests matching the source path go to the target
|
||||
** path.
|
||||
**
|
||||
** The physical table name is the configured prefix plus TABLE_NAME (for
|
||||
** example cms_redirect); possible-value source names on the fields are
|
||||
** logical and get the same prefix when the table is built.
|
||||
*******************************************************************************/
|
||||
public class CmsRedirect extends QRecordEntity
|
||||
{
|
||||
public static final String TABLE_NAME = "redirect";
|
||||
|
||||
@QField(isEditable = false, isPrimaryKey = true)
|
||||
private Long id;
|
||||
|
||||
@QField(isRequired = true, maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String name;
|
||||
|
||||
@QField(maxLength = 500, valueTooLongBehavior = ValueTooLongBehavior.TRUNCATE)
|
||||
private String description;
|
||||
|
||||
@QField(isRequired = true, maxLength = 500, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String sourcePath;
|
||||
|
||||
@QField(isRequired = true, maxLength = 500, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String targetPath;
|
||||
|
||||
@QField(isRequired = true, possibleValueSourceName = "redirect_type", label = "Redirect Type")
|
||||
private Long redirectTypeId;
|
||||
|
||||
@QField(isRequired = true, maxLength = 20, valueTooLongBehavior = ValueTooLongBehavior.ERROR, defaultValue = "EXACT")
|
||||
private String matchType;
|
||||
|
||||
@QField(isRequired = true, defaultValue = "true")
|
||||
private Boolean isActive;
|
||||
|
||||
@QField(isRequired = true, defaultValue = "0")
|
||||
private Integer priority;
|
||||
|
||||
@QField(isRequired = true, defaultValue = "true")
|
||||
private Boolean preserveQueryParams;
|
||||
|
||||
@QField(isEditable = false, defaultValue = "0")
|
||||
private Long hitCount;
|
||||
|
||||
@QField(isEditable = false)
|
||||
private Instant lastHitDate;
|
||||
|
||||
@QField()
|
||||
private Instant expiryDate;
|
||||
|
||||
@QField(isRequired = true, possibleValueSourceName = "site", label = "Site")
|
||||
private Long siteId;
|
||||
|
||||
@QField(isEditable = false, dynamicDefaultValueBehavior = DynamicDefaultValueBehavior.CREATE_DATE)
|
||||
private Instant createDate;
|
||||
|
||||
@QField(isEditable = false, dynamicDefaultValueBehavior = DynamicDefaultValueBehavior.MODIFY_DATE)
|
||||
private Instant modifyDate;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Default constructor
|
||||
*******************************************************************************/
|
||||
public CmsRedirect()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor that takes a QRecord
|
||||
*******************************************************************************/
|
||||
public CmsRedirect(QRecord record) throws QException
|
||||
{
|
||||
populateFromQRecord(record);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for id
|
||||
*******************************************************************************/
|
||||
public Long getId()
|
||||
{
|
||||
return (this.id);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for id
|
||||
*******************************************************************************/
|
||||
public void setId(Long id)
|
||||
{
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for id
|
||||
*******************************************************************************/
|
||||
public CmsRedirect withId(Long id)
|
||||
{
|
||||
this.id = id;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for name
|
||||
*******************************************************************************/
|
||||
public String getName()
|
||||
{
|
||||
return (this.name);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for name
|
||||
*******************************************************************************/
|
||||
public void setName(String name)
|
||||
{
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for name
|
||||
*******************************************************************************/
|
||||
public CmsRedirect withName(String name)
|
||||
{
|
||||
this.name = name;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for description
|
||||
*******************************************************************************/
|
||||
public String getDescription()
|
||||
{
|
||||
return (this.description);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for description
|
||||
*******************************************************************************/
|
||||
public void setDescription(String description)
|
||||
{
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for description
|
||||
*******************************************************************************/
|
||||
public CmsRedirect withDescription(String description)
|
||||
{
|
||||
this.description = description;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for sourcePath
|
||||
*******************************************************************************/
|
||||
public String getSourcePath()
|
||||
{
|
||||
return (this.sourcePath);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for sourcePath
|
||||
*******************************************************************************/
|
||||
public void setSourcePath(String sourcePath)
|
||||
{
|
||||
this.sourcePath = sourcePath;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for sourcePath
|
||||
*******************************************************************************/
|
||||
public CmsRedirect withSourcePath(String sourcePath)
|
||||
{
|
||||
this.sourcePath = sourcePath;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for targetPath
|
||||
*******************************************************************************/
|
||||
public String getTargetPath()
|
||||
{
|
||||
return (this.targetPath);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for targetPath
|
||||
*******************************************************************************/
|
||||
public void setTargetPath(String targetPath)
|
||||
{
|
||||
this.targetPath = targetPath;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for targetPath
|
||||
*******************************************************************************/
|
||||
public CmsRedirect withTargetPath(String targetPath)
|
||||
{
|
||||
this.targetPath = targetPath;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for redirectTypeId
|
||||
*******************************************************************************/
|
||||
public Long getRedirectTypeId()
|
||||
{
|
||||
return (this.redirectTypeId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for redirectTypeId
|
||||
*******************************************************************************/
|
||||
public void setRedirectTypeId(Long redirectTypeId)
|
||||
{
|
||||
this.redirectTypeId = redirectTypeId;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for redirectTypeId
|
||||
*******************************************************************************/
|
||||
public CmsRedirect withRedirectTypeId(Long redirectTypeId)
|
||||
{
|
||||
this.redirectTypeId = redirectTypeId;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for matchType
|
||||
*******************************************************************************/
|
||||
public String getMatchType()
|
||||
{
|
||||
return (this.matchType);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for matchType
|
||||
*******************************************************************************/
|
||||
public void setMatchType(String matchType)
|
||||
{
|
||||
this.matchType = matchType;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for matchType
|
||||
*******************************************************************************/
|
||||
public CmsRedirect withMatchType(String matchType)
|
||||
{
|
||||
this.matchType = matchType;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for isActive
|
||||
*******************************************************************************/
|
||||
public Boolean getIsActive()
|
||||
{
|
||||
return (this.isActive);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for isActive
|
||||
*******************************************************************************/
|
||||
public void setIsActive(Boolean isActive)
|
||||
{
|
||||
this.isActive = isActive;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for isActive
|
||||
*******************************************************************************/
|
||||
public CmsRedirect withIsActive(Boolean isActive)
|
||||
{
|
||||
this.isActive = isActive;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for priority
|
||||
*******************************************************************************/
|
||||
public Integer getPriority()
|
||||
{
|
||||
return (this.priority);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for priority
|
||||
*******************************************************************************/
|
||||
public void setPriority(Integer priority)
|
||||
{
|
||||
this.priority = priority;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for priority
|
||||
*******************************************************************************/
|
||||
public CmsRedirect withPriority(Integer priority)
|
||||
{
|
||||
this.priority = priority;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for preserveQueryParams
|
||||
*******************************************************************************/
|
||||
public Boolean getPreserveQueryParams()
|
||||
{
|
||||
return (this.preserveQueryParams);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for preserveQueryParams
|
||||
*******************************************************************************/
|
||||
public void setPreserveQueryParams(Boolean preserveQueryParams)
|
||||
{
|
||||
this.preserveQueryParams = preserveQueryParams;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for preserveQueryParams
|
||||
*******************************************************************************/
|
||||
public CmsRedirect withPreserveQueryParams(Boolean preserveQueryParams)
|
||||
{
|
||||
this.preserveQueryParams = preserveQueryParams;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for hitCount
|
||||
*******************************************************************************/
|
||||
public Long getHitCount()
|
||||
{
|
||||
return (this.hitCount);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for hitCount
|
||||
*******************************************************************************/
|
||||
public void setHitCount(Long hitCount)
|
||||
{
|
||||
this.hitCount = hitCount;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for hitCount
|
||||
*******************************************************************************/
|
||||
public CmsRedirect withHitCount(Long hitCount)
|
||||
{
|
||||
this.hitCount = hitCount;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for lastHitDate
|
||||
*******************************************************************************/
|
||||
public Instant getLastHitDate()
|
||||
{
|
||||
return (this.lastHitDate);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for lastHitDate
|
||||
*******************************************************************************/
|
||||
public void setLastHitDate(Instant lastHitDate)
|
||||
{
|
||||
this.lastHitDate = lastHitDate;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for lastHitDate
|
||||
*******************************************************************************/
|
||||
public CmsRedirect withLastHitDate(Instant lastHitDate)
|
||||
{
|
||||
this.lastHitDate = lastHitDate;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for expiryDate
|
||||
*******************************************************************************/
|
||||
public Instant getExpiryDate()
|
||||
{
|
||||
return (this.expiryDate);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for expiryDate
|
||||
*******************************************************************************/
|
||||
public void setExpiryDate(Instant expiryDate)
|
||||
{
|
||||
this.expiryDate = expiryDate;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for expiryDate
|
||||
*******************************************************************************/
|
||||
public CmsRedirect withExpiryDate(Instant expiryDate)
|
||||
{
|
||||
this.expiryDate = expiryDate;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for siteId
|
||||
*******************************************************************************/
|
||||
public Long getSiteId()
|
||||
{
|
||||
return (this.siteId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for siteId
|
||||
*******************************************************************************/
|
||||
public void setSiteId(Long siteId)
|
||||
{
|
||||
this.siteId = siteId;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for siteId
|
||||
*******************************************************************************/
|
||||
public CmsRedirect withSiteId(Long siteId)
|
||||
{
|
||||
this.siteId = siteId;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for createDate
|
||||
*******************************************************************************/
|
||||
public Instant getCreateDate()
|
||||
{
|
||||
return (this.createDate);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for createDate
|
||||
*******************************************************************************/
|
||||
public void setCreateDate(Instant createDate)
|
||||
{
|
||||
this.createDate = createDate;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for createDate
|
||||
*******************************************************************************/
|
||||
public CmsRedirect withCreateDate(Instant createDate)
|
||||
{
|
||||
this.createDate = createDate;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for modifyDate
|
||||
*******************************************************************************/
|
||||
public Instant getModifyDate()
|
||||
{
|
||||
return (this.modifyDate);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for modifyDate
|
||||
*******************************************************************************/
|
||||
public void setModifyDate(Instant modifyDate)
|
||||
{
|
||||
this.modifyDate = modifyDate;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for modifyDate
|
||||
*******************************************************************************/
|
||||
public CmsRedirect withModifyDate(Instant modifyDate)
|
||||
{
|
||||
this.modifyDate = modifyDate;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.cms.core.model;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PossibleValueEnum;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** How a redirect's source path matches a request path. Its possible-value
|
||||
** source is named after the redirect table (cms_redirect_match_type).
|
||||
*******************************************************************************/
|
||||
public enum CmsRedirectMatchType implements PossibleValueEnum<String>
|
||||
{
|
||||
EXACT("Exact path"),
|
||||
PREFIX("Path prefix"),
|
||||
REGEX("Regular expression");
|
||||
|
||||
/** The logical possible-value source name (the prefix is added). */
|
||||
public static final String NAME = "redirect_match_type";
|
||||
|
||||
private final String label;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor.
|
||||
*******************************************************************************/
|
||||
CmsRedirectMatchType(String label)
|
||||
{
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The stored value.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public String getPossibleValueId()
|
||||
{
|
||||
return (name());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The label.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public String getPossibleValueLabel()
|
||||
{
|
||||
return (label);
|
||||
}
|
||||
}
|
||||
@@ -1,366 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.cms.core.model;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QField;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A kind of redirect and its HTTP status (reference data, for example 301 and
|
||||
** 302).
|
||||
**
|
||||
** The physical table name is the configured prefix plus TABLE_NAME (for
|
||||
** example cms_redirect_type); possible-value source names on the fields are
|
||||
** logical and get the same prefix when the table is built.
|
||||
*******************************************************************************/
|
||||
public class CmsRedirectType extends QRecordEntity
|
||||
{
|
||||
public static final String TABLE_NAME = "redirect_type";
|
||||
|
||||
@QField(isEditable = false, isPrimaryKey = true)
|
||||
private Long id;
|
||||
|
||||
@QField(isRequired = true, maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String name;
|
||||
|
||||
@QField(isRequired = true, maxLength = 50, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String code;
|
||||
|
||||
@QField(maxLength = 500, valueTooLongBehavior = ValueTooLongBehavior.TRUNCATE)
|
||||
private String description;
|
||||
|
||||
@QField(isRequired = true, label = "HTTP Status Code")
|
||||
private Integer httpStatusCode;
|
||||
|
||||
@QField(isRequired = true, defaultValue = "true")
|
||||
private Boolean isActive;
|
||||
|
||||
@QField(isRequired = true, defaultValue = "0")
|
||||
private Integer sortOrder;
|
||||
|
||||
@QField(isEditable = false, dynamicDefaultValueBehavior = DynamicDefaultValueBehavior.CREATE_DATE)
|
||||
private Instant createDate;
|
||||
|
||||
@QField(isEditable = false, dynamicDefaultValueBehavior = DynamicDefaultValueBehavior.MODIFY_DATE)
|
||||
private Instant modifyDate;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Default constructor
|
||||
*******************************************************************************/
|
||||
public CmsRedirectType()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor that takes a QRecord
|
||||
*******************************************************************************/
|
||||
public CmsRedirectType(QRecord record) throws QException
|
||||
{
|
||||
populateFromQRecord(record);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for id
|
||||
*******************************************************************************/
|
||||
public Long getId()
|
||||
{
|
||||
return (this.id);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for id
|
||||
*******************************************************************************/
|
||||
public void setId(Long id)
|
||||
{
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for id
|
||||
*******************************************************************************/
|
||||
public CmsRedirectType withId(Long id)
|
||||
{
|
||||
this.id = id;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for name
|
||||
*******************************************************************************/
|
||||
public String getName()
|
||||
{
|
||||
return (this.name);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for name
|
||||
*******************************************************************************/
|
||||
public void setName(String name)
|
||||
{
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for name
|
||||
*******************************************************************************/
|
||||
public CmsRedirectType withName(String name)
|
||||
{
|
||||
this.name = name;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for code
|
||||
*******************************************************************************/
|
||||
public String getCode()
|
||||
{
|
||||
return (this.code);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for code
|
||||
*******************************************************************************/
|
||||
public void setCode(String code)
|
||||
{
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for code
|
||||
*******************************************************************************/
|
||||
public CmsRedirectType withCode(String code)
|
||||
{
|
||||
this.code = code;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for description
|
||||
*******************************************************************************/
|
||||
public String getDescription()
|
||||
{
|
||||
return (this.description);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for description
|
||||
*******************************************************************************/
|
||||
public void setDescription(String description)
|
||||
{
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for description
|
||||
*******************************************************************************/
|
||||
public CmsRedirectType withDescription(String description)
|
||||
{
|
||||
this.description = description;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for httpStatusCode
|
||||
*******************************************************************************/
|
||||
public Integer getHttpStatusCode()
|
||||
{
|
||||
return (this.httpStatusCode);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for httpStatusCode
|
||||
*******************************************************************************/
|
||||
public void setHttpStatusCode(Integer httpStatusCode)
|
||||
{
|
||||
this.httpStatusCode = httpStatusCode;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for httpStatusCode
|
||||
*******************************************************************************/
|
||||
public CmsRedirectType withHttpStatusCode(Integer httpStatusCode)
|
||||
{
|
||||
this.httpStatusCode = httpStatusCode;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for isActive
|
||||
*******************************************************************************/
|
||||
public Boolean getIsActive()
|
||||
{
|
||||
return (this.isActive);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for isActive
|
||||
*******************************************************************************/
|
||||
public void setIsActive(Boolean isActive)
|
||||
{
|
||||
this.isActive = isActive;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for isActive
|
||||
*******************************************************************************/
|
||||
public CmsRedirectType withIsActive(Boolean isActive)
|
||||
{
|
||||
this.isActive = isActive;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for sortOrder
|
||||
*******************************************************************************/
|
||||
public Integer getSortOrder()
|
||||
{
|
||||
return (this.sortOrder);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for sortOrder
|
||||
*******************************************************************************/
|
||||
public void setSortOrder(Integer sortOrder)
|
||||
{
|
||||
this.sortOrder = sortOrder;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for sortOrder
|
||||
*******************************************************************************/
|
||||
public CmsRedirectType withSortOrder(Integer sortOrder)
|
||||
{
|
||||
this.sortOrder = sortOrder;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for createDate
|
||||
*******************************************************************************/
|
||||
public Instant getCreateDate()
|
||||
{
|
||||
return (this.createDate);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for createDate
|
||||
*******************************************************************************/
|
||||
public void setCreateDate(Instant createDate)
|
||||
{
|
||||
this.createDate = createDate;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for createDate
|
||||
*******************************************************************************/
|
||||
public CmsRedirectType withCreateDate(Instant createDate)
|
||||
{
|
||||
this.createDate = createDate;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for modifyDate
|
||||
*******************************************************************************/
|
||||
public Instant getModifyDate()
|
||||
{
|
||||
return (this.modifyDate);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for modifyDate
|
||||
*******************************************************************************/
|
||||
public void setModifyDate(Instant modifyDate)
|
||||
{
|
||||
this.modifyDate = modifyDate;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for modifyDate
|
||||
*******************************************************************************/
|
||||
public CmsRedirectType withModifyDate(Instant modifyDate)
|
||||
{
|
||||
this.modifyDate = modifyDate;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,264 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.cms.core.model;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QField;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A website served by the CMS. Every page, menu, redirect and snippet belongs
|
||||
** to one site.
|
||||
**
|
||||
** The physical table name is the configured prefix plus TABLE_NAME (for
|
||||
** example cms_site); possible-value source names on the fields are
|
||||
** logical and get the same prefix when the table is built.
|
||||
*******************************************************************************/
|
||||
public class CmsSite extends QRecordEntity
|
||||
{
|
||||
public static final String TABLE_NAME = "site";
|
||||
|
||||
@QField(isEditable = false, isPrimaryKey = true)
|
||||
private Long id;
|
||||
|
||||
@QField(isRequired = true, maxLength = 40, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String name;
|
||||
|
||||
@QField(isRequired = true, maxLength = 20, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String shortName;
|
||||
|
||||
@QField(maxLength = 255, valueTooLongBehavior = ValueTooLongBehavior.TRUNCATE, gridColumns = 12)
|
||||
private String description;
|
||||
|
||||
@QField(isEditable = false, dynamicDefaultValueBehavior = DynamicDefaultValueBehavior.CREATE_DATE)
|
||||
private Instant createDate;
|
||||
|
||||
@QField(isEditable = false, dynamicDefaultValueBehavior = DynamicDefaultValueBehavior.MODIFY_DATE)
|
||||
private Instant modifyDate;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Default constructor
|
||||
*******************************************************************************/
|
||||
public CmsSite()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor that takes a QRecord
|
||||
*******************************************************************************/
|
||||
public CmsSite(QRecord record) throws QException
|
||||
{
|
||||
populateFromQRecord(record);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for id
|
||||
*******************************************************************************/
|
||||
public Long getId()
|
||||
{
|
||||
return (this.id);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for id
|
||||
*******************************************************************************/
|
||||
public void setId(Long id)
|
||||
{
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for id
|
||||
*******************************************************************************/
|
||||
public CmsSite withId(Long id)
|
||||
{
|
||||
this.id = id;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for name
|
||||
*******************************************************************************/
|
||||
public String getName()
|
||||
{
|
||||
return (this.name);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for name
|
||||
*******************************************************************************/
|
||||
public void setName(String name)
|
||||
{
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for name
|
||||
*******************************************************************************/
|
||||
public CmsSite withName(String name)
|
||||
{
|
||||
this.name = name;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for shortName
|
||||
*******************************************************************************/
|
||||
public String getShortName()
|
||||
{
|
||||
return (this.shortName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for shortName
|
||||
*******************************************************************************/
|
||||
public void setShortName(String shortName)
|
||||
{
|
||||
this.shortName = shortName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for shortName
|
||||
*******************************************************************************/
|
||||
public CmsSite withShortName(String shortName)
|
||||
{
|
||||
this.shortName = shortName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for description
|
||||
*******************************************************************************/
|
||||
public String getDescription()
|
||||
{
|
||||
return (this.description);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for description
|
||||
*******************************************************************************/
|
||||
public void setDescription(String description)
|
||||
{
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for description
|
||||
*******************************************************************************/
|
||||
public CmsSite withDescription(String description)
|
||||
{
|
||||
this.description = description;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for createDate
|
||||
*******************************************************************************/
|
||||
public Instant getCreateDate()
|
||||
{
|
||||
return (this.createDate);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for createDate
|
||||
*******************************************************************************/
|
||||
public void setCreateDate(Instant createDate)
|
||||
{
|
||||
this.createDate = createDate;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for createDate
|
||||
*******************************************************************************/
|
||||
public CmsSite withCreateDate(Instant createDate)
|
||||
{
|
||||
this.createDate = createDate;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for modifyDate
|
||||
*******************************************************************************/
|
||||
public Instant getModifyDate()
|
||||
{
|
||||
return (this.modifyDate);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for modifyDate
|
||||
*******************************************************************************/
|
||||
public void setModifyDate(Instant modifyDate)
|
||||
{
|
||||
this.modifyDate = modifyDate;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for modifyDate
|
||||
*******************************************************************************/
|
||||
public CmsSite withModifyDate(Instant modifyDate)
|
||||
{
|
||||
this.modifyDate = modifyDate;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,399 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.cms.core.model;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QField;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A reusable block of content of a site, referenced by its short name.
|
||||
**
|
||||
** The physical table name is the configured prefix plus TABLE_NAME (for
|
||||
** example cms_snippet); possible-value source names on the fields are
|
||||
** logical and get the same prefix when the table is built.
|
||||
*******************************************************************************/
|
||||
public class CmsSnippet extends QRecordEntity
|
||||
{
|
||||
public static final String TABLE_NAME = "snippet";
|
||||
|
||||
@QField(isEditable = false, isPrimaryKey = true)
|
||||
private Long id;
|
||||
|
||||
@QField(isRequired = true, maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String name;
|
||||
|
||||
@QField(isRequired = true, maxLength = 50, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String shortName;
|
||||
|
||||
@QField(gridColumns = 12)
|
||||
private String content;
|
||||
|
||||
@QField(maxLength = 255, valueTooLongBehavior = ValueTooLongBehavior.ERROR, label = "CSS Class Name")
|
||||
private String cssClassName;
|
||||
|
||||
@QField(isRequired = true, defaultValue = "true")
|
||||
private Boolean isActive;
|
||||
|
||||
@QField(isRequired = true, defaultValue = "0")
|
||||
private Integer sortOrder;
|
||||
|
||||
@QField(isRequired = true, possibleValueSourceName = "site", label = "Site")
|
||||
private Long siteId;
|
||||
|
||||
@QField(isEditable = false, dynamicDefaultValueBehavior = DynamicDefaultValueBehavior.CREATE_DATE)
|
||||
private Instant createDate;
|
||||
|
||||
@QField(isEditable = false, dynamicDefaultValueBehavior = DynamicDefaultValueBehavior.MODIFY_DATE)
|
||||
private Instant modifyDate;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Default constructor
|
||||
*******************************************************************************/
|
||||
public CmsSnippet()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor that takes a QRecord
|
||||
*******************************************************************************/
|
||||
public CmsSnippet(QRecord record) throws QException
|
||||
{
|
||||
populateFromQRecord(record);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for id
|
||||
*******************************************************************************/
|
||||
public Long getId()
|
||||
{
|
||||
return (this.id);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for id
|
||||
*******************************************************************************/
|
||||
public void setId(Long id)
|
||||
{
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for id
|
||||
*******************************************************************************/
|
||||
public CmsSnippet withId(Long id)
|
||||
{
|
||||
this.id = id;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for name
|
||||
*******************************************************************************/
|
||||
public String getName()
|
||||
{
|
||||
return (this.name);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for name
|
||||
*******************************************************************************/
|
||||
public void setName(String name)
|
||||
{
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for name
|
||||
*******************************************************************************/
|
||||
public CmsSnippet withName(String name)
|
||||
{
|
||||
this.name = name;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for shortName
|
||||
*******************************************************************************/
|
||||
public String getShortName()
|
||||
{
|
||||
return (this.shortName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for shortName
|
||||
*******************************************************************************/
|
||||
public void setShortName(String shortName)
|
||||
{
|
||||
this.shortName = shortName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for shortName
|
||||
*******************************************************************************/
|
||||
public CmsSnippet withShortName(String shortName)
|
||||
{
|
||||
this.shortName = shortName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for content
|
||||
*******************************************************************************/
|
||||
public String getContent()
|
||||
{
|
||||
return (this.content);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for content
|
||||
*******************************************************************************/
|
||||
public void setContent(String content)
|
||||
{
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for content
|
||||
*******************************************************************************/
|
||||
public CmsSnippet withContent(String content)
|
||||
{
|
||||
this.content = content;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for cssClassName
|
||||
*******************************************************************************/
|
||||
public String getCssClassName()
|
||||
{
|
||||
return (this.cssClassName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for cssClassName
|
||||
*******************************************************************************/
|
||||
public void setCssClassName(String cssClassName)
|
||||
{
|
||||
this.cssClassName = cssClassName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for cssClassName
|
||||
*******************************************************************************/
|
||||
public CmsSnippet withCssClassName(String cssClassName)
|
||||
{
|
||||
this.cssClassName = cssClassName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for isActive
|
||||
*******************************************************************************/
|
||||
public Boolean getIsActive()
|
||||
{
|
||||
return (this.isActive);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for isActive
|
||||
*******************************************************************************/
|
||||
public void setIsActive(Boolean isActive)
|
||||
{
|
||||
this.isActive = isActive;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for isActive
|
||||
*******************************************************************************/
|
||||
public CmsSnippet withIsActive(Boolean isActive)
|
||||
{
|
||||
this.isActive = isActive;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for sortOrder
|
||||
*******************************************************************************/
|
||||
public Integer getSortOrder()
|
||||
{
|
||||
return (this.sortOrder);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for sortOrder
|
||||
*******************************************************************************/
|
||||
public void setSortOrder(Integer sortOrder)
|
||||
{
|
||||
this.sortOrder = sortOrder;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for sortOrder
|
||||
*******************************************************************************/
|
||||
public CmsSnippet withSortOrder(Integer sortOrder)
|
||||
{
|
||||
this.sortOrder = sortOrder;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for siteId
|
||||
*******************************************************************************/
|
||||
public Long getSiteId()
|
||||
{
|
||||
return (this.siteId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for siteId
|
||||
*******************************************************************************/
|
||||
public void setSiteId(Long siteId)
|
||||
{
|
||||
this.siteId = siteId;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for siteId
|
||||
*******************************************************************************/
|
||||
public CmsSnippet withSiteId(Long siteId)
|
||||
{
|
||||
this.siteId = siteId;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for createDate
|
||||
*******************************************************************************/
|
||||
public Instant getCreateDate()
|
||||
{
|
||||
return (this.createDate);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for createDate
|
||||
*******************************************************************************/
|
||||
public void setCreateDate(Instant createDate)
|
||||
{
|
||||
this.createDate = createDate;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for createDate
|
||||
*******************************************************************************/
|
||||
public CmsSnippet withCreateDate(Instant createDate)
|
||||
{
|
||||
this.createDate = createDate;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for modifyDate
|
||||
*******************************************************************************/
|
||||
public Instant getModifyDate()
|
||||
{
|
||||
return (this.modifyDate);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for modifyDate
|
||||
*******************************************************************************/
|
||||
public void setModifyDate(Instant modifyDate)
|
||||
{
|
||||
this.modifyDate = modifyDate;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for modifyDate
|
||||
*******************************************************************************/
|
||||
public CmsSnippet withModifyDate(Instant modifyDate)
|
||||
{
|
||||
this.modifyDate = modifyDate;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,343 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.cms.home;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import com.kingsrook.qbits.cms.CmsQBitConfig;
|
||||
import com.kingsrook.qbits.cms.CmsRuntime;
|
||||
import com.kingsrook.qbits.cms.CmsTables;
|
||||
import com.kingsrook.qbits.cms.metadata.CmsAreas;
|
||||
import com.kingsrook.qbits.cms.metadata.CmsTableDefinition;
|
||||
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer;
|
||||
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionEnforcement;
|
||||
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.permissions.TablePermissionSubType;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.RawHTML;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The CMS homes. One renderer serves the top app's home and every area's
|
||||
** home; the widget's default values say which (namePrefix and area). Each
|
||||
** home owns its data and shows only real data read as the current user, so
|
||||
** a table the user may not read is left out instead of shown as zero:
|
||||
**
|
||||
** - the top home: one card per area with its main tables' counts (and
|
||||
** unpublished pages), linking to the area; no per-record lists, so nothing
|
||||
** duplicates an area home;
|
||||
** - an area home: what needs attention (unpublished or inactive rows), the
|
||||
** create actions the user may run, and the area's recently changed
|
||||
** records. Next's app page lists the tables and counts below it.
|
||||
**
|
||||
** Reads always run with PermissionEnforcement.ENFORCE: a widget renders
|
||||
** inside a trusted scope, and these counts must still be the user's own.
|
||||
*******************************************************************************/
|
||||
public class CmsHomeRenderer extends AbstractWidgetRenderer
|
||||
{
|
||||
public static final String KEY_NAME_PREFIX = "namePrefix";
|
||||
public static final String KEY_AREA = "area";
|
||||
|
||||
static final int RECENT_ROWS = 8;
|
||||
|
||||
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("MMM d, yyyy", Locale.US).withZone(ZoneOffset.UTC);
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Render the home named by the widget's default values.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
|
||||
{
|
||||
QWidgetMetaDataInterface widget = input.getWidgetMetaData();
|
||||
Map<String, Serializable> defaults = widget.getDefaultValues();
|
||||
String prefix = defaults == null ? null : (String) defaults.get(KEY_NAME_PREFIX);
|
||||
String areaKey = defaults == null ? null : (String) defaults.get(KEY_AREA);
|
||||
Optional<CmsQBitConfig> config = CmsRuntime.findConfig(QContext.getQInstance(), prefix);
|
||||
if(config.isEmpty())
|
||||
{
|
||||
throw (new QException("No CMS install with name prefix " + prefix));
|
||||
}
|
||||
|
||||
StringBuilder html = new StringBuilder();
|
||||
CmsRuntime runtime = config.get().getRuntime();
|
||||
if(runtime != null && !runtime.getSchemaProblems().isEmpty())
|
||||
{
|
||||
html.append(CmsHtml.warning("The CMS database is not up to date, so some screens may fail: "
|
||||
+ CmsHtml.escape(String.join(" ", runtime.getSchemaProblems()))));
|
||||
}
|
||||
|
||||
Optional<CmsAreas.Area> area = areaKey == null ? Optional.empty() : CmsAreas.area(areaKey);
|
||||
html.append(area.isPresent() ? renderArea(config.get(), area.get()) : renderTop(config.get()));
|
||||
return (new RenderWidgetOutput(new RawHTML(widget.getLabel(), html.toString())));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The top home: a card per area.
|
||||
*******************************************************************************/
|
||||
String renderTop(CmsQBitConfig config) throws QException
|
||||
{
|
||||
List<CmsHtml.Tile> tiles = new ArrayList<>();
|
||||
for(CmsAreas.Area area : CmsAreas.areas(config))
|
||||
{
|
||||
List<String> notes = new ArrayList<>();
|
||||
long total = 0;
|
||||
boolean any = false;
|
||||
for(String logical : area.mainTables())
|
||||
{
|
||||
Long count = count(config.applyPrefix(logical), null);
|
||||
if(count != null)
|
||||
{
|
||||
any = true;
|
||||
total += count;
|
||||
notes.add(CmsHtml.link(tablePath(config, logical), plural(count, label(logical))) + draftNote(config, logical));
|
||||
}
|
||||
}
|
||||
if(any)
|
||||
{
|
||||
tiles.add(new CmsHtml.Tile(area.label(), String.valueOf(total), "/app/" + area.appName(config), notes));
|
||||
}
|
||||
}
|
||||
if(tiles.isEmpty())
|
||||
{
|
||||
return (CmsHtml.empty("You do not have access to any CMS area."));
|
||||
}
|
||||
return (CmsHtml.tiles(tiles));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** An area home. Next's app page already lists the area's tables with their
|
||||
** record counts, so this home shows only what that list cannot: what needs
|
||||
** attention, the create actions the user may run, and recent changes.
|
||||
*******************************************************************************/
|
||||
String renderArea(CmsQBitConfig config, CmsAreas.Area area) throws QException
|
||||
{
|
||||
StringBuilder html = new StringBuilder();
|
||||
List<CmsHtml.Tile> attention = new ArrayList<>();
|
||||
List<String> actions = new ArrayList<>();
|
||||
List<QRecord> recent = new ArrayList<>();
|
||||
boolean readable = false;
|
||||
for(String logical : area.mainTables())
|
||||
{
|
||||
String table = config.applyPrefix(logical);
|
||||
if(count(table, null) == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
readable = true;
|
||||
if(hasField(table, "isPublished"))
|
||||
{
|
||||
addAttention(attention, config, logical, "isPublished", "Unpublished ");
|
||||
}
|
||||
if(hasField(table, "isActive"))
|
||||
{
|
||||
addAttention(attention, config, logical, "isActive", "Inactive ");
|
||||
}
|
||||
if(PermissionsHelper.hasTablePermission(new InsertInput(table), table, TablePermissionSubType.INSERT))
|
||||
{
|
||||
actions.add(CmsHtml.link(tablePath(config, logical) + "/create", "New " + label(logical).toLowerCase(Locale.ROOT)));
|
||||
}
|
||||
recent.addAll(recent(table));
|
||||
}
|
||||
if(!readable)
|
||||
{
|
||||
return (CmsHtml.empty("You do not have access to " + CmsHtml.escape(area.label()) + "."));
|
||||
}
|
||||
|
||||
html.append(CmsHtml.heading("Needs attention"));
|
||||
html.append(attention.isEmpty() ? CmsHtml.empty("Nothing: everything here is published and active.") : CmsHtml.tiles(attention));
|
||||
if(!actions.isEmpty())
|
||||
{
|
||||
html.append("<div style=\"display:flex;flex-wrap:wrap;gap:6px 18px;margin-top:12px\">");
|
||||
actions.forEach(a -> html.append("<span>").append(a).append("</span>"));
|
||||
html.append("</div>");
|
||||
}
|
||||
|
||||
html.append(CmsHtml.heading("Recently changed"));
|
||||
recent.sort(Comparator.comparing((QRecord r) -> r.getValueInstant("modifyDate"), Comparator.nullsLast(Comparator.reverseOrder())));
|
||||
if(recent.isEmpty())
|
||||
{
|
||||
html.append(CmsHtml.empty("Nothing yet. Records you create appear here."));
|
||||
}
|
||||
else
|
||||
{
|
||||
List<CmsHtml.Row> rows = new ArrayList<>();
|
||||
for(QRecord record : recent.subList(0, Math.min(RECENT_ROWS, recent.size())))
|
||||
{
|
||||
String logical = record.getTableName().substring(config.getTableNamePrefix().length());
|
||||
String title = record.getRecordLabel() != null ? record.getRecordLabel() : label(logical) + " " + record.getValue("id");
|
||||
Instant when = record.getValueInstant("modifyDate");
|
||||
rows.add(new CmsHtml.Row(title, tablePath(config, logical) + "/" + record.getValue("id"), label(logical) + (when == null ? "" : " · " + DATE.format(when))));
|
||||
}
|
||||
html.append(CmsHtml.rows(rows));
|
||||
}
|
||||
return (html.toString());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A "needs attention" tile when some rows of a table have a flag off.
|
||||
*******************************************************************************/
|
||||
private void addAttention(List<CmsHtml.Tile> tiles, CmsQBitConfig config, String logical, String flagField, String labelPrefix) throws QException
|
||||
{
|
||||
Long count = count(config.applyPrefix(logical), new QQueryFilter(new QFilterCriteria(flagField, QCriteriaOperator.EQUALS, false)));
|
||||
if(count != null && count > 0)
|
||||
{
|
||||
tiles.add(new CmsHtml.Tile(labelPrefix + pluralLabel(logical).toLowerCase(Locale.ROOT), String.valueOf(count), tablePath(config, logical), List.of()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** ", N drafts" for a table with an isPublished field (empty otherwise).
|
||||
*******************************************************************************/
|
||||
private String draftNote(CmsQBitConfig config, String logical) throws QException
|
||||
{
|
||||
String table = config.applyPrefix(logical);
|
||||
if(!hasField(table, "isPublished"))
|
||||
{
|
||||
return ("");
|
||||
}
|
||||
Long drafts = count(table, new QQueryFilter(new QFilterCriteria("isPublished", QCriteriaOperator.EQUALS, false)));
|
||||
return (drafts == null || drafts == 0 ? "" : ", " + CmsHtml.muted(drafts + " unpublished"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The most recently changed records of a table, read as the current user.
|
||||
*******************************************************************************/
|
||||
private List<QRecord> recent(String table) throws QException
|
||||
{
|
||||
try
|
||||
{
|
||||
QueryInput queryInput = new QueryInput(table)
|
||||
.withFilter(new QQueryFilter().withOrderBy(new QFilterOrderBy("modifyDate", false)).withLimit(RECENT_ROWS))
|
||||
.withShouldGenerateDisplayValues(true);
|
||||
queryInput.setPermissionEnforcement(PermissionEnforcement.ENFORCE);
|
||||
return (new QueryAction().execute(queryInput).getRecords());
|
||||
}
|
||||
catch(QPermissionDeniedException e)
|
||||
{
|
||||
return (List.of());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A count read as the current user, or null when the user may not read it.
|
||||
*******************************************************************************/
|
||||
private Long count(String table, QQueryFilter filter) throws QException
|
||||
{
|
||||
try
|
||||
{
|
||||
CountInput countInput = new CountInput(table).withFilter(filter);
|
||||
countInput.setPermissionEnforcement(PermissionEnforcement.ENFORCE);
|
||||
Integer count = new CountAction().execute(countInput).getCount();
|
||||
return (count == null ? 0L : count.longValue());
|
||||
}
|
||||
catch(QPermissionDeniedException e)
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static boolean hasField(String table, String field)
|
||||
{
|
||||
QTableMetaData tableMetaData = QContext.getQInstance().getTable(table);
|
||||
return (tableMetaData != null && tableMetaData.getFields().containsKey(field));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The admin path of a table (Next routes tables at /app/name).
|
||||
*******************************************************************************/
|
||||
static String tablePath(CmsQBitConfig config, String logical)
|
||||
{
|
||||
return ("/app/" + config.applyPrefix(logical));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static String label(String logical)
|
||||
{
|
||||
return (CmsTables.definition(logical).map(CmsTableDefinition::label).orElse(logical));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static String pluralLabel(String logical)
|
||||
{
|
||||
return (CmsTableDefinition.pluralize(label(logical)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** "1 page" / "3 pages".
|
||||
*******************************************************************************/
|
||||
static String plural(long count, String label)
|
||||
{
|
||||
String lower = label.toLowerCase(Locale.ROOT);
|
||||
return (count + " " + (count == 1 ? lower : CmsTableDefinition.pluralize(lower)));
|
||||
}
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.cms.home;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Small HTML building blocks for the CMS homes, readable at every width and
|
||||
** in light and dark mode: text inherits the surrounding color, borders and
|
||||
** fills are translucent, grids wrap to one column on a phone, long words
|
||||
** break instead of scrolling sideways.
|
||||
*******************************************************************************/
|
||||
public final class CmsHtml
|
||||
{
|
||||
static final String MUTED = "opacity:.72";
|
||||
static final String WRAP = "overflow-wrap:anywhere;min-width:0";
|
||||
static final String CARD = "box-sizing:border-box;min-width:0;padding:14px 16px;border-radius:10px;border:1px solid rgba(127,127,127,.28);background:rgba(127,127,127,.06)";
|
||||
|
||||
/*******************************************************************************
|
||||
** A tile: label, big number, optional notes; the number links to href.
|
||||
**
|
||||
** @param label the label
|
||||
** @param value the value text
|
||||
** @param href the link (may be null)
|
||||
** @param notes secondary lines (already HTML)
|
||||
*******************************************************************************/
|
||||
public record Tile(String label, String value, String href, List<String> notes)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A list row: a linked title and a muted detail.
|
||||
**
|
||||
** @param title the title
|
||||
** @param href the link (may be null)
|
||||
** @param detail the detail
|
||||
*******************************************************************************/
|
||||
public record Row(String title, String href, String detail)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Not instantiable.
|
||||
*******************************************************************************/
|
||||
private CmsHtml()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** HTML-escape text.
|
||||
*******************************************************************************/
|
||||
public static String escape(String text)
|
||||
{
|
||||
if(text == null)
|
||||
{
|
||||
return ("");
|
||||
}
|
||||
StringBuilder rs = new StringBuilder(text.length());
|
||||
for(char c : text.toCharArray())
|
||||
{
|
||||
switch(c)
|
||||
{
|
||||
case '<' -> rs.append("<");
|
||||
case '>' -> rs.append(">");
|
||||
case '&' -> rs.append("&");
|
||||
case '"' -> rs.append(""");
|
||||
case '\'' -> rs.append("'");
|
||||
default -> rs.append(c);
|
||||
}
|
||||
}
|
||||
return (rs.toString());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A link that inherits the text color.
|
||||
*******************************************************************************/
|
||||
public static String link(String href, String text)
|
||||
{
|
||||
return ("<a href=\"" + escape(href) + "\" style=\"color:inherit;text-decoration:underline;text-underline-offset:2px\">" + escape(text) + "</a>");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Muted text (already HTML).
|
||||
*******************************************************************************/
|
||||
public static String muted(String html)
|
||||
{
|
||||
return ("<span style=\"" + MUTED + ";font-size:13px\">" + html + "</span>");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A heading inside a widget.
|
||||
*******************************************************************************/
|
||||
public static String heading(String text)
|
||||
{
|
||||
return ("<div style=\"margin:18px 0 8px;font-size:13px;font-weight:600;text-transform:uppercase;letter-spacing:.04em;" + MUTED + "\">" + escape(text) + "</div>");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A responsive grid of tiles (one column on a phone).
|
||||
*******************************************************************************/
|
||||
public static String tiles(List<Tile> tiles)
|
||||
{
|
||||
StringBuilder html = new StringBuilder("<div style=\"display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,13rem),1fr));gap:12px\">");
|
||||
for(Tile tile : tiles)
|
||||
{
|
||||
html.append("<div style=\"").append(CARD).append("\">");
|
||||
html.append("<div style=\"font-size:13px;").append(MUTED).append(';').append(WRAP).append("\">").append(escape(tile.label())).append("</div>");
|
||||
String value = tile.href() == null ? escape(tile.value())
|
||||
: "<a href=\"" + escape(tile.href()) + "\" style=\"color:inherit;text-decoration:none\">" + escape(tile.value()) + "</a>";
|
||||
html.append("<div style=\"font-size:26px;font-weight:700;line-height:1.3;").append(WRAP).append("\">").append(value).append("</div>");
|
||||
for(String note : tile.notes())
|
||||
{
|
||||
html.append("<div style=\"font-size:13px;").append(WRAP).append("\">").append(note).append("</div>");
|
||||
}
|
||||
html.append("</div>");
|
||||
}
|
||||
return (html.append("</div>").toString());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A plain list of rows, each wrapping as needed.
|
||||
*******************************************************************************/
|
||||
public static String rows(List<Row> rows)
|
||||
{
|
||||
StringBuilder html = new StringBuilder("<ul style=\"list-style:none;margin:0;padding:0\">");
|
||||
for(Row row : rows)
|
||||
{
|
||||
html.append("<li style=\"display:flex;flex-wrap:wrap;align-items:baseline;column-gap:10px;row-gap:2px;padding:7px 0;border-bottom:1px solid rgba(127,127,127,.18)\">");
|
||||
html.append("<span style=\"flex:1 1 14rem;").append(WRAP).append("\">").append(row.href() == null ? escape(row.title()) : link(row.href(), row.title())).append("</span>");
|
||||
html.append("<span style=\"flex:0 1 auto;font-size:13px;").append(MUTED).append(';').append(WRAP).append("\">").append(escape(row.detail())).append("</span>");
|
||||
html.append("</li>");
|
||||
}
|
||||
return (html.append("</ul>").toString());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** An honest empty state.
|
||||
*******************************************************************************/
|
||||
public static String empty(String messageHtml)
|
||||
{
|
||||
return ("<div style=\"padding:14px 2px;font-size:14px;" + MUTED + ';' + WRAP + "\">" + messageHtml + "</div>");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A warning banner (amber left border, readable in both modes).
|
||||
*******************************************************************************/
|
||||
public static String warning(String messageHtml)
|
||||
{
|
||||
return ("<div style=\"margin:0 0 12px;padding:8px 12px;border-left:3px solid #d97706;border-radius:4px;background:rgba(217,119,6,.10);font-size:14px;"
|
||||
+ WRAP + "\">" + messageHtml + "</div>");
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.cms.metadata;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.cms.CmsQBitConfig;
|
||||
import com.kingsrook.qbits.cms.home.CmsHomeRenderer;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.WidgetType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppSection;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Builds the CMS app tree: one top app with its home, and one sub-app per
|
||||
** area with its own home. The nav of an area lists its main tables and a
|
||||
** Settings section of reference tables; processes are never in the nav.
|
||||
** The qbit never sets the instance's home app (the host owns its landing
|
||||
** page).
|
||||
*******************************************************************************/
|
||||
public class CmsAppBuilder
|
||||
{
|
||||
private final CmsQBitConfig config;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor.
|
||||
*******************************************************************************/
|
||||
public CmsAppBuilder(CmsQBitConfig config)
|
||||
{
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The result: the apps (top first) and their home widgets.
|
||||
*******************************************************************************/
|
||||
public record Built(List<QAppMetaData> apps, List<QWidgetMetaData> widgets)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Build the apps and homes.
|
||||
*******************************************************************************/
|
||||
public Built build()
|
||||
{
|
||||
List<QAppMetaData> apps = new ArrayList<>();
|
||||
List<QWidgetMetaData> widgets = new ArrayList<>();
|
||||
|
||||
String topHome = CmsAreas.topHomeWidgetName(config);
|
||||
QAppMetaData top = new QAppMetaData()
|
||||
.withName(config.getEffectiveAppName())
|
||||
.withLabel(config.getAppLabel())
|
||||
.withIcon(new QIcon("web"))
|
||||
.withWidgets(List.of(topHome));
|
||||
apps.add(top);
|
||||
widgets.add(homeWidget(topHome, config.getAppLabel(), null));
|
||||
|
||||
for(CmsAreas.Area area : CmsAreas.areas(config))
|
||||
{
|
||||
QAppMetaData sub = new QAppMetaData()
|
||||
.withName(area.appName(config))
|
||||
.withLabel(area.label())
|
||||
.withIcon(new QIcon(area.icon()))
|
||||
.withWidgets(List.of(area.homeWidgetName(config)))
|
||||
.withSection(new QAppSection().withName(area.appName(config)).withLabel(area.label()).withIcon(new QIcon(area.icon()))
|
||||
.withTables(area.mainTables().stream().map(config::applyPrefix).toList()));
|
||||
if(!area.settingsTables().isEmpty())
|
||||
{
|
||||
sub.withSection(new QAppSection().withName(area.appName(config) + "Settings").withLabel("Settings").withIcon(new QIcon("settings"))
|
||||
.withTables(area.settingsTables().stream().map(config::applyPrefix).toList()));
|
||||
}
|
||||
top.withChild(sub);
|
||||
apps.add(sub);
|
||||
widgets.add(homeWidget(area.homeWidgetName(config), area.label(), area.key()));
|
||||
}
|
||||
return (new Built(apps, widgets));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A home widget (full width; one renderer, told which home by its defaults).
|
||||
*******************************************************************************/
|
||||
private QWidgetMetaData homeWidget(String name, String label, String areaKey)
|
||||
{
|
||||
QWidgetMetaData widget = new QWidgetMetaData()
|
||||
.withName(name)
|
||||
.withLabel(label)
|
||||
.withType(WidgetType.HTML.getType())
|
||||
.withCodeReference(new QCodeReference(CmsHomeRenderer.class))
|
||||
.withGridColumns(12)
|
||||
.withIsCard(true)
|
||||
.withDefaultValue(CmsHomeRenderer.KEY_NAME_PREFIX, config.getNamePrefix());
|
||||
if(areaKey != null)
|
||||
{
|
||||
widget.withDefaultValue(CmsHomeRenderer.KEY_AREA, areaKey);
|
||||
}
|
||||
return (widget);
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.cms.metadata;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import com.kingsrook.qbits.cms.CmsFeature;
|
||||
import com.kingsrook.qbits.cms.CmsQBitConfig;
|
||||
import com.kingsrook.qbits.cms.CmsTables;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The CMS areas: one sub-app each, with its own home. An area lists only its
|
||||
** main tables in the nav, plus a Settings section for reference data; child
|
||||
** tables are edited on their parents' records (owner rule).
|
||||
*******************************************************************************/
|
||||
public final class CmsAreas
|
||||
{
|
||||
/*******************************************************************************
|
||||
** One area.
|
||||
**
|
||||
** @param key the name suffix (sub-app cmsSites, home cmsSitesHome)
|
||||
** @param label the label
|
||||
** @param icon a Material icon name
|
||||
** @param feature the feature that owns the area
|
||||
** @param mainTables logical names of the tables in the nav
|
||||
** @param settingsTables logical names of the reference tables
|
||||
*******************************************************************************/
|
||||
public record Area(String key, String label, String icon, CmsFeature feature, List<String> mainTables, List<String> settingsTables)
|
||||
{
|
||||
/*******************************************************************************
|
||||
** The sub-app name.
|
||||
*******************************************************************************/
|
||||
public String appName(CmsQBitConfig config)
|
||||
{
|
||||
return (config.name(key));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The home widget name.
|
||||
*******************************************************************************/
|
||||
public String homeWidgetName(CmsQBitConfig config)
|
||||
{
|
||||
return (config.name(key + "Home"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static final List<Area> AREAS = List.of(
|
||||
new Area("Sites", "Sites", "language", CmsFeature.CORE, List.of(CmsTables.SITE, CmsTables.REDIRECT), List.of(CmsTables.REDIRECT_TYPE)),
|
||||
new Area("Pages", "Pages", "description", CmsFeature.CORE, List.of(CmsTables.PAGE, CmsTables.SNIPPET), List.of(CmsTables.PAGE_TYPE, CmsTables.PAGE_SECTION_TYPE)),
|
||||
new Area("Navigation", "Navigation", "menu", CmsFeature.CORE, List.of(CmsTables.MENU), List.of()));
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Not instantiable.
|
||||
*******************************************************************************/
|
||||
private CmsAreas()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The areas of the config's features.
|
||||
*******************************************************************************/
|
||||
public static List<Area> areas(CmsQBitConfig config)
|
||||
{
|
||||
return (AREAS.stream().filter(a -> config.hasFeature(a.feature())).toList());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** One area by key.
|
||||
*******************************************************************************/
|
||||
public static Optional<Area> area(String key)
|
||||
{
|
||||
return (AREAS.stream().filter(a -> a.key().equals(key)).findFirst());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The top app's home widget name.
|
||||
*******************************************************************************/
|
||||
public static String topHomeWidgetName(CmsQBitConfig config)
|
||||
{
|
||||
return (config.name("Home"));
|
||||
}
|
||||
}
|
||||
@@ -1,256 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.cms.metadata;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import com.kingsrook.qbits.cms.CmsQBitConfig;
|
||||
import com.kingsrook.qbits.cms.CmsTables;
|
||||
import com.kingsrook.qbits.cms.core.model.CmsRedirectMatchType;
|
||||
import com.kingsrook.qbits.cms.security.CmsOpenReadsPermissionChecker;
|
||||
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.ChildRecordListRenderer;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSTableBackendDetails;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Builds the CMS tables of a config: one standard QQQ table per definition
|
||||
** (fields from the entity, prefixed names, snake_case columns), a
|
||||
** possible-value source per table, joins and child-record lists for child
|
||||
** tables, associations, permission rules and field audit.
|
||||
**
|
||||
** Every table is a normal, visible QQQ table (owner rule): reference tables
|
||||
** and child tables are left out of the nav, not hidden.
|
||||
*******************************************************************************/
|
||||
public class CmsTableBuilder
|
||||
{
|
||||
/** The QQQ audit table; field audit is applied only when the host has it. */
|
||||
static final String AUDIT_TABLE = "audit";
|
||||
|
||||
private final CmsQBitConfig config;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor.
|
||||
*******************************************************************************/
|
||||
public CmsTableBuilder(CmsQBitConfig config)
|
||||
{
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The result of a build.
|
||||
**
|
||||
** @param tables the tables, by physical name
|
||||
** @param possibleValueSources the possible-value sources
|
||||
** @param joins the joins
|
||||
** @param widgets the child-record list widgets
|
||||
*******************************************************************************/
|
||||
public record Built(Map<String, QTableMetaData> tables, List<QPossibleValueSource> possibleValueSources, List<QJoinMetaData> joins,
|
||||
List<QWidgetMetaData> widgets)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Build every table of the config's features.
|
||||
*******************************************************************************/
|
||||
public Built build(QInstance qInstance) throws QException
|
||||
{
|
||||
List<CmsTableDefinition> definitions = CmsTables.definitions(config);
|
||||
Set<String> logicals = definitions.stream().map(CmsTableDefinition::logicalName).collect(Collectors.toSet());
|
||||
Map<String, QTableMetaData> tables = new LinkedHashMap<>();
|
||||
List<QPossibleValueSource> pvs = new ArrayList<>();
|
||||
List<QJoinMetaData> joins = new ArrayList<>();
|
||||
List<QWidgetMetaData> widgets = new ArrayList<>();
|
||||
boolean audit = qInstance.getTable(AUDIT_TABLE) != null;
|
||||
|
||||
for(CmsTableDefinition definition : definitions)
|
||||
{
|
||||
tables.put(config.applyPrefix(definition.logicalName()), buildTable(definition, logicals, audit));
|
||||
pvs.add(QPossibleValueSource.newForTable(config.applyPrefix(definition.logicalName())));
|
||||
}
|
||||
if(logicals.contains(CmsTables.REDIRECT))
|
||||
{
|
||||
pvs.add(QPossibleValueSource.newForEnum(config.applyPrefix(CmsRedirectMatchType.NAME), CmsRedirectMatchType.values()));
|
||||
}
|
||||
|
||||
for(CmsTableDefinition definition : definitions)
|
||||
{
|
||||
QTableMetaData parent = tables.get(config.applyPrefix(definition.logicalName()));
|
||||
for(CmsTableDefinition.Child child : definition.children())
|
||||
{
|
||||
if(logicals.contains(child.childLogicalName()))
|
||||
{
|
||||
addChild(parent, definition, child, joins, widgets);
|
||||
}
|
||||
}
|
||||
}
|
||||
return (new Built(tables, pvs, joins, widgets));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** One table.
|
||||
*******************************************************************************/
|
||||
QTableMetaData buildTable(CmsTableDefinition definition, Set<String> logicals, boolean audit) throws QException
|
||||
{
|
||||
String name = config.applyPrefix(definition.logicalName());
|
||||
QTableMetaData table = new QTableMetaData()
|
||||
.withName(name)
|
||||
.withLabel(definition.pluralLabel())
|
||||
.withIcon(new QIcon(definition.icon()))
|
||||
.withBackendName(config.getBackendName())
|
||||
.withBackendDetails(new RDBMSTableBackendDetails().withTableName(name))
|
||||
.withPrimaryKeyField("id")
|
||||
.withFieldsFromEntity(definition.entityClass())
|
||||
.withRecordLabelFormat(String.join(" ", definition.recordLabelFields().stream().map(f -> "%s").toList()))
|
||||
.withRecordLabelFields(definition.recordLabelFields())
|
||||
.withPermissionRules(permissionRules());
|
||||
|
||||
for(QFieldMetaData field : table.getFields().values())
|
||||
{
|
||||
String pvsName = field.getPossibleValueSourceName();
|
||||
if(pvsName != null && logicals.contains(pvsName))
|
||||
{
|
||||
field.setPossibleValueSourceName(config.applyPrefix(pvsName));
|
||||
}
|
||||
else if(pvsName != null)
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////
|
||||
// a reference to a table of a feature that is off: no lookup //
|
||||
//////////////////////////////////////////////////////////////////
|
||||
field.setPossibleValueSourceName(null);
|
||||
}
|
||||
if(definition.textFields().contains(field.getName()))
|
||||
{
|
||||
field.setType(QFieldType.TEXT);
|
||||
}
|
||||
}
|
||||
if(CmsTables.REDIRECT.equals(definition.logicalName()))
|
||||
{
|
||||
table.getField("matchType").setPossibleValueSourceName(config.applyPrefix(CmsRedirectMatchType.NAME));
|
||||
}
|
||||
QInstanceEnricher.setInferredFieldBackendNames(table);
|
||||
|
||||
for(List<String> uniqueKey : definition.uniqueKeys())
|
||||
{
|
||||
table.withUniqueKey(new UniqueKey(uniqueKey));
|
||||
}
|
||||
for(CmsTableDefinition.Section section : definition.sections())
|
||||
{
|
||||
table.addSection(new QFieldSection(section.name(), section.label(), new QIcon(section.icon()), section.tier(), section.fields()));
|
||||
}
|
||||
table.addSection(new QFieldSection("dates", "Dates", new QIcon("calendar_month"), Tier.T3, List.of("createDate", "modifyDate")));
|
||||
|
||||
if(audit)
|
||||
{
|
||||
table.withAuditRules(new QAuditRules().withAuditLevel(AuditLevel.FIELD));
|
||||
}
|
||||
return (table);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Permission rules for every CMS table: the table name plus .read, .insert,
|
||||
** .edit and .delete; with openReads, reads need nothing.
|
||||
*******************************************************************************/
|
||||
QPermissionRules permissionRules()
|
||||
{
|
||||
QPermissionRules rules = new QPermissionRules().withLevel(PermissionLevel.READ_INSERT_EDIT_DELETE_PERMISSIONS);
|
||||
if(Boolean.TRUE.equals(config.getOpenReads()))
|
||||
{
|
||||
rules.withCustomPermissionChecker(new QCodeReference(CmsOpenReadsPermissionChecker.class));
|
||||
}
|
||||
return (rules);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A child table: a join, a child-record list on the parent's screen, an
|
||||
** exposed join (reports and filters) and, when named, an association.
|
||||
*******************************************************************************/
|
||||
private void addChild(QTableMetaData parent, CmsTableDefinition parentDefinition, CmsTableDefinition.Child child, List<QJoinMetaData> joins,
|
||||
List<QWidgetMetaData> widgets)
|
||||
{
|
||||
String childTable = config.applyPrefix(child.childLogicalName());
|
||||
String joinName = parent.getName() + "_" + childTable + "_" + child.joinField();
|
||||
|
||||
QJoinMetaData join = new QJoinMetaData()
|
||||
.withName(joinName)
|
||||
.withLeftTable(parent.getName())
|
||||
.withRightTable(childTable)
|
||||
.withType(JoinType.ONE_TO_MANY)
|
||||
.withJoinOn(new JoinOn("id", child.joinField()))
|
||||
.withOrderBy(new QFilterOrderBy(child.orderBy()));
|
||||
joins.add(join);
|
||||
|
||||
QWidgetMetaData widget = ChildRecordListRenderer.widgetMetaDataBuilder(join)
|
||||
.withName(joinName)
|
||||
.withLabel(child.label())
|
||||
.withMaxRows(50)
|
||||
.withCanAddChildRecord(true)
|
||||
.withDefaultValuesForNewChildRecordsFromParentFields(Map.of(child.joinField(), "id"))
|
||||
.getWidgetMetaData();
|
||||
widgets.add(widget);
|
||||
|
||||
String icon = CmsTables.definition(child.childLogicalName()).map(CmsTableDefinition::icon).orElse(parentDefinition.icon());
|
||||
parent.addSection(new QFieldSection(joinName, child.label(), new QIcon(icon), Tier.T2, List.of()).withWidgetName(joinName));
|
||||
if(!childTable.equals(parent.getName()))
|
||||
{
|
||||
parent.withExposedJoin(new ExposedJoin().withLabel(child.label()).withJoinPath(List.of(joinName)).withJoinTable(childTable));
|
||||
}
|
||||
if(child.associationName() != null)
|
||||
{
|
||||
parent.withAssociation(new Association().withName(child.associationName()).withAssociatedTableName(childTable).withJoinName(joinName));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.cms.metadata;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qbits.cms.CmsFeature;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Everything the builder needs to know about one CMS table beyond its
|
||||
** entity: label, icon, record label, screen sections, child tables, unique
|
||||
** keys and which fields hold long text.
|
||||
**
|
||||
** @param logicalName the unprefixed table name, e.g. "page"
|
||||
** @param entityClass the entity whose fields the table has
|
||||
** @param feature the feature that owns the table
|
||||
** @param label the singular label ("Page"); the table label is the plural
|
||||
** @param icon a Material icon name
|
||||
** @param kind main table, child table or reference data
|
||||
** @param recordLabelFields fields of the record label (joined by spaces)
|
||||
** @param sections screen sections (the system dates section is added)
|
||||
** @param children child tables shown as lists on this table's records
|
||||
** @param uniqueKeys unique keys (field names)
|
||||
** @param textFields fields stored as TEXT (long, unbounded content)
|
||||
*******************************************************************************/
|
||||
public record CmsTableDefinition(String logicalName, Class<? extends QRecordEntity> entityClass, CmsFeature feature, String label, String icon,
|
||||
Kind kind, List<String> recordLabelFields, List<Section> sections, List<Child> children,
|
||||
List<List<String>> uniqueKeys, Set<String> textFields)
|
||||
{
|
||||
/*******************************************************************************
|
||||
** How a table appears in the admin.
|
||||
*******************************************************************************/
|
||||
public enum Kind
|
||||
{
|
||||
/** In the nav of its area; has its own home counts. */
|
||||
MAIN,
|
||||
|
||||
/** Not in the nav: edited as a child list on its parent's record. */
|
||||
CHILD,
|
||||
|
||||
/** Reference data in the area's Settings section; admin-editable only. */
|
||||
REFERENCE
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A screen section.
|
||||
**
|
||||
** @param name the section name
|
||||
** @param label the section label
|
||||
** @param icon a Material icon name
|
||||
** @param tier T1 (primary) or T2
|
||||
** @param fields the fields, in order
|
||||
*******************************************************************************/
|
||||
public record Section(String name, String label, String icon, Tier tier, List<String> fields)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A child table shown as a list on this table's records.
|
||||
**
|
||||
** @param childLogicalName the child's logical table name
|
||||
** @param joinField the child's field holding this table's id
|
||||
** @param label the list label
|
||||
** @param associationName the association name (API and nested writes), or null
|
||||
** @param orderBy the child field to order by
|
||||
*******************************************************************************/
|
||||
public record Child(String childLogicalName, String joinField, String label, String associationName, String orderBy)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The plural label (the QQQ table label, as in the nav): "Pages".
|
||||
*******************************************************************************/
|
||||
public String pluralLabel()
|
||||
{
|
||||
return (pluralize(label));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** English plural of a label's last word ("Page Type" to "Page Types",
|
||||
** "Category" to "Categories").
|
||||
*******************************************************************************/
|
||||
public static String pluralize(String word)
|
||||
{
|
||||
String lower = word.toLowerCase(Locale.ROOT);
|
||||
return (lower.endsWith("y") && !lower.endsWith("ay") && !lower.endsWith("ey") ? word.substring(0, word.length() - 1) + "ies" : word + "s");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Start a definition.
|
||||
*******************************************************************************/
|
||||
public static Builder builder(String logicalName, Class<? extends QRecordEntity> entityClass, CmsFeature feature, String label, String icon, Kind kind)
|
||||
{
|
||||
return (new Builder(logicalName, entityClass, feature, label, icon, kind));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent builder for a definition.
|
||||
*******************************************************************************/
|
||||
public static final class Builder
|
||||
{
|
||||
private final String logicalName;
|
||||
private final Class<? extends QRecordEntity> entityClass;
|
||||
private final CmsFeature feature;
|
||||
private final String label;
|
||||
private final String icon;
|
||||
private final Kind kind;
|
||||
private List<String> recordLabelFields = List.of("name");
|
||||
private final List<Section> sections = new ArrayList<>();
|
||||
private final List<Child> children = new ArrayList<>();
|
||||
private final List<List<String>> uniqueKeys = new ArrayList<>();
|
||||
private Set<String> textFields = Set.of();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor.
|
||||
*******************************************************************************/
|
||||
private Builder(String logicalName, Class<? extends QRecordEntity> entityClass, CmsFeature feature, String label, String icon, Kind kind)
|
||||
{
|
||||
this.logicalName = logicalName;
|
||||
this.entityClass = entityClass;
|
||||
this.feature = feature;
|
||||
this.label = label;
|
||||
this.icon = icon;
|
||||
this.kind = kind;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The record label fields.
|
||||
*******************************************************************************/
|
||||
public Builder recordLabel(String... fields)
|
||||
{
|
||||
this.recordLabelFields = List.of(fields);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A screen section.
|
||||
*******************************************************************************/
|
||||
public Builder section(String name, String label, String icon, Tier tier, String... fields)
|
||||
{
|
||||
this.sections.add(new Section(name, label, icon, tier, List.of(fields)));
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A child list.
|
||||
*******************************************************************************/
|
||||
public Builder child(String childLogicalName, String joinField, String label, String associationName, String orderBy)
|
||||
{
|
||||
this.children.add(new Child(childLogicalName, joinField, label, associationName, orderBy));
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A unique key.
|
||||
*******************************************************************************/
|
||||
public Builder unique(String... fields)
|
||||
{
|
||||
this.uniqueKeys.add(List.of(fields));
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fields stored as TEXT.
|
||||
*******************************************************************************/
|
||||
public Builder text(String... fields)
|
||||
{
|
||||
this.textFields = Set.of(fields);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The definition.
|
||||
*******************************************************************************/
|
||||
public CmsTableDefinition build()
|
||||
{
|
||||
return (new CmsTableDefinition(logicalName, entityClass, feature, label, icon, kind, recordLabelFields, List.copyOf(sections), List.copyOf(children),
|
||||
List.copyOf(uniqueKeys), textFields));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.cms.security;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.actions.permissions.CustomPermissionChecker;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.replace.ReplaceInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithPermissionRules;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Table permissions when CmsQBitConfig.openReads is on: reading needs no
|
||||
** permission; an insert, update, delete or replace needs the table's
|
||||
** .insert, .edit or .delete permission (the same names as the standard
|
||||
** READ_INSERT_EDIT_DELETE rules, so roles grant the same names either way).
|
||||
**
|
||||
** Only write inputs count as writes. Other permission questions (table meta
|
||||
** data, which pass a plain table input) are answered as reads.
|
||||
*******************************************************************************/
|
||||
public class CmsOpenReadsPermissionChecker implements CustomPermissionChecker
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
** Throw if the current session may not perform the write the input describes.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void checkPermissionsThrowing(AbstractActionInput actionInput, MetaDataWithPermissionRules metaDataWithPermissionRules) throws QPermissionDeniedException
|
||||
{
|
||||
QSession session = QContext.getQSession();
|
||||
for(String suffix : requiredWriteSuffixes(actionInput))
|
||||
{
|
||||
if(session == null || !session.hasPermission(metaDataWithPermissionRules.getName() + "." + suffix))
|
||||
{
|
||||
throw (new QPermissionDeniedException("Permission denied."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The write permission suffixes an input requires (empty for reads).
|
||||
*******************************************************************************/
|
||||
static List<String> requiredWriteSuffixes(AbstractActionInput actionInput)
|
||||
{
|
||||
List<String> rs = new ArrayList<>();
|
||||
if(actionInput instanceof InsertInput)
|
||||
{
|
||||
rs.add("insert");
|
||||
}
|
||||
else if(actionInput instanceof UpdateInput)
|
||||
{
|
||||
rs.add("edit");
|
||||
}
|
||||
else if(actionInput instanceof DeleteInput)
|
||||
{
|
||||
rs.add("delete");
|
||||
}
|
||||
else if(actionInput instanceof ReplaceInput replaceInput)
|
||||
{
|
||||
rs.add("insert");
|
||||
rs.add("edit");
|
||||
if(Boolean.TRUE.equals(replaceInput.getPerformDeletes()))
|
||||
{
|
||||
rs.add("delete");
|
||||
}
|
||||
}
|
||||
return (rs);
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.cms.security;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The CMS role bundles (spec section 6.2). A host maps its identity
|
||||
** provider's groups to roles in CmsQBitConfig.roleMapping, and
|
||||
** CmsSecurity.applySessionPermissions turns a user's groups into the
|
||||
** permissions of their roles.
|
||||
*******************************************************************************/
|
||||
public enum CmsRole
|
||||
{
|
||||
/** Everything, including reference data (types) and the admin permission. */
|
||||
ADMIN,
|
||||
|
||||
/** Read and write content; read reference data. No publishing. */
|
||||
EDITOR,
|
||||
|
||||
/** EDITOR plus publishing. */
|
||||
PUBLISHER,
|
||||
|
||||
/** Read content, create and edit drafts; no deletes, no publishing. */
|
||||
DRAFTER,
|
||||
|
||||
/** Read everything. */
|
||||
VIEWER
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.cms.security;
|
||||
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import com.kingsrook.qbits.cms.CmsQBitConfig;
|
||||
import com.kingsrook.qbits.cms.CmsTables;
|
||||
import com.kingsrook.qbits.cms.metadata.CmsTableDefinition;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Turns identity-provider groups into CMS permissions (spec section 6.2).
|
||||
** The host calls applySessionPermissions from its authentication customizer,
|
||||
** the same integration point other qbits use; the qbit never talks to the
|
||||
** identity provider itself.
|
||||
**
|
||||
** Permission names: each table's .read, .insert, .edit and .delete
|
||||
** (cms_page.edit), plus permissionBaseName.admin and .publish.
|
||||
*******************************************************************************/
|
||||
public final class CmsSecurity
|
||||
{
|
||||
public static final String ADMIN = "admin";
|
||||
public static final String PUBLISH = "publish";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Not instantiable.
|
||||
*******************************************************************************/
|
||||
private CmsSecurity()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The roles a user's groups map to.
|
||||
**
|
||||
** @param config the CMS config (its roleMapping)
|
||||
** @param groups the user's identity-provider groups
|
||||
** @return the roles
|
||||
*******************************************************************************/
|
||||
public static Set<CmsRole> rolesFor(CmsQBitConfig config, Collection<String> groups)
|
||||
{
|
||||
Set<CmsRole> roles = EnumSet.noneOf(CmsRole.class);
|
||||
if(groups != null && config.getRoleMapping() != null)
|
||||
{
|
||||
for(String group : groups)
|
||||
{
|
||||
CmsRole role = config.getRoleMapping().get(group);
|
||||
if(role != null)
|
||||
{
|
||||
roles.add(role);
|
||||
}
|
||||
}
|
||||
}
|
||||
return (roles);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The permission names of a set of roles.
|
||||
**
|
||||
** @param config the CMS config
|
||||
** @param roles the roles
|
||||
** @return the permission names, sorted
|
||||
*******************************************************************************/
|
||||
public static Set<String> permissionsFor(CmsQBitConfig config, Set<CmsRole> roles)
|
||||
{
|
||||
Set<String> rs = new TreeSet<>();
|
||||
for(CmsTableDefinition definition : CmsTables.definitions(config))
|
||||
{
|
||||
String table = config.applyPrefix(definition.logicalName());
|
||||
boolean reference = definition.kind() == CmsTableDefinition.Kind.REFERENCE;
|
||||
for(CmsRole role : roles)
|
||||
{
|
||||
rs.add(table + ".read");
|
||||
switch(role)
|
||||
{
|
||||
case ADMIN -> addWrites(rs, table, true);
|
||||
case EDITOR, PUBLISHER ->
|
||||
{
|
||||
if(!reference)
|
||||
{
|
||||
addWrites(rs, table, true);
|
||||
}
|
||||
}
|
||||
case DRAFTER ->
|
||||
{
|
||||
if(!reference)
|
||||
{
|
||||
addWrites(rs, table, false);
|
||||
}
|
||||
}
|
||||
default ->
|
||||
{
|
||||
//////////////////////////
|
||||
// VIEWER: read only //
|
||||
//////////////////////////
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if(roles.contains(CmsRole.ADMIN))
|
||||
{
|
||||
rs.add(config.permission(ADMIN));
|
||||
rs.add(config.permission(PUBLISH));
|
||||
}
|
||||
if(roles.contains(CmsRole.PUBLISHER))
|
||||
{
|
||||
rs.add(config.permission(PUBLISH));
|
||||
}
|
||||
return (rs);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Add the permissions of a user's groups to a session.
|
||||
**
|
||||
** @param session the session
|
||||
** @param groups the user's identity-provider groups
|
||||
** @param config the CMS config
|
||||
*******************************************************************************/
|
||||
public static void applySessionPermissions(QSession session, Collection<String> groups, CmsQBitConfig config)
|
||||
{
|
||||
Set<String> permissions = new HashSet<>(session.getPermissions() == null ? Set.of() : session.getPermissions());
|
||||
permissions.addAll(permissionsFor(config, rolesFor(config, groups)));
|
||||
session.setPermissions(permissions);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static void addWrites(Set<String> rs, String table, boolean delete)
|
||||
{
|
||||
rs.add(table + ".insert");
|
||||
rs.add(table + ".edit");
|
||||
if(delete)
|
||||
{
|
||||
rs.add(table + ".delete");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.cms;
|
||||
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qbits.cms.security.CmsRole;
|
||||
import com.kingsrook.qbits.cms.security.CmsSecurity;
|
||||
import com.kingsrook.qqq.backend.core.actions.dashboard.RenderWidgetAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Child-record lists on parent records render and serialize (the record
|
||||
** screens' Sections, Child Pages, Domains and Items lists).
|
||||
*******************************************************************************/
|
||||
class CmsChildListTest
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@AfterEach
|
||||
void afterEach()
|
||||
{
|
||||
QContext.clear();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Every child list renders to JSON for a real parent record.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testChildListsSerialize() throws Exception
|
||||
{
|
||||
RDBMSBackendMetaData backend = CmsTestSupport.h2Backend();
|
||||
CmsTestSupport.migrate(backend, Map.of());
|
||||
QInstance qInstance = CmsTestSupport.blankInstance(backend);
|
||||
CmsQBitConfig config = new CmsQBitConfig().withBackendName(CmsTestSupport.BACKEND);
|
||||
new CmsQBitProducer().withQBitConfig(config).produce(qInstance);
|
||||
CmsTestSupport.session(qInstance, CmsSecurity.permissionsFor(config, Set.of(CmsRole.ADMIN)));
|
||||
|
||||
Integer siteId = new InsertAction().execute(new InsertInput("cms_site").withRecord(new QRecord().withValue("name", "S").withValue("shortName", "s")))
|
||||
.getRecords().get(0).getValueInteger("id");
|
||||
new InsertAction().execute(new InsertInput("cms_page").withRecord(new QRecord().withValue("name", "P").withValue("slug", "p").withValue("title", "P")
|
||||
.withValue("pageTypeId", 1).withValue("siteId", siteId)
|
||||
.withAssociatedRecord("sections", new QRecord().withValue("name", "A").withValue("sectionTypeId", 1))));
|
||||
new InsertAction().execute(new InsertInput("cms_menu").withRecord(new QRecord().withValue("name", "M").withValue("slug", "m").withValue("menuType", "HEADER")
|
||||
.withValue("siteId", siteId).withAssociatedRecord("items", new QRecord().withValue("name", "I").withValue("slug", "i").withValue("itemType", "LINK"))));
|
||||
|
||||
for(String widget : List.of("cms_page_cms_page_section_pageId", "cms_page_cms_page_parentPageId", "cms_site_cms_domain_siteId",
|
||||
"cms_menu_cms_menu_item_menuId", "cms_menu_item_cms_menu_item_parentId"))
|
||||
{
|
||||
RenderWidgetOutput output = new RenderWidgetAction().execute(new RenderWidgetInput().withWidgetMetaData(qInstance.getWidget(widget))
|
||||
.withUrlParams(new HashMap<>(Map.of("id", "1"))));
|
||||
assertThat(JsonUtils.toJson(output.getWidgetData())).as(widget).isNotEmpty();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.cms;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.time.Instant;
|
||||
import com.kingsrook.qbits.cms.metadata.CmsTableDefinition;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Every CMS entity round-trips through QRecord with its getters, setters and
|
||||
** fluent setters, and every definition's sections name real fields.
|
||||
*******************************************************************************/
|
||||
class CmsEntitiesTest
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
** Round trip each entity.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testEntitiesRoundTrip() throws Exception
|
||||
{
|
||||
for(CmsTableDefinition definition : CmsTables.definitions())
|
||||
{
|
||||
Class<? extends QRecordEntity> entityClass = definition.entityClass();
|
||||
QRecordEntity entity = entityClass.getConstructor().newInstance();
|
||||
QRecord record = new QRecord();
|
||||
for(Field field : entityClass.getDeclaredFields())
|
||||
{
|
||||
if(Modifier.isStatic(field.getModifiers()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String name = field.getName();
|
||||
String cap = Character.toUpperCase(name.charAt(0)) + name.substring(1);
|
||||
Serializable value = sample(field.getType());
|
||||
Method setter = entityClass.getMethod("set" + cap, field.getType());
|
||||
Method getter = entityClass.getMethod("get" + cap);
|
||||
Method wither = entityClass.getMethod("with" + cap, field.getType());
|
||||
setter.invoke(entity, value);
|
||||
assertThat(getter.invoke(entity)).as(entityClass.getSimpleName() + "." + name).isEqualTo(value);
|
||||
assertThat(wither.invoke(entity, value)).isSameAs(entity);
|
||||
record.setValue(name, value);
|
||||
}
|
||||
QRecordEntity copy = entityClass.getConstructor(QRecord.class).newInstance(record);
|
||||
assertThat(copy.toQRecord().getValues()).as(entityClass.getSimpleName()).isEqualTo(entity.toQRecord().getValues());
|
||||
|
||||
for(CmsTableDefinition.Section section : definition.sections())
|
||||
{
|
||||
for(String fieldName : section.fields())
|
||||
{
|
||||
assertThat(entityClass.getDeclaredField(fieldName)).as(definition.logicalName() + " section field " + fieldName).isNotNull();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A sample value of a field type.
|
||||
*******************************************************************************/
|
||||
private static Serializable sample(Class<?> type)
|
||||
{
|
||||
if(type == Long.class)
|
||||
{
|
||||
return (7L);
|
||||
}
|
||||
if(type == Integer.class)
|
||||
{
|
||||
return (3);
|
||||
}
|
||||
if(type == Boolean.class)
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
if(type == Instant.class)
|
||||
{
|
||||
return (Instant.parse("2026-10-12T00:00:00Z"));
|
||||
}
|
||||
return ("value");
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.cms;
|
||||
|
||||
|
||||
import java.util.EnumSet;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** CmsFeature flags and their dependencies.
|
||||
*******************************************************************************/
|
||||
class CmsFeatureTest
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
** The default set is what this version implements; dependencies hold.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testFeatures()
|
||||
{
|
||||
assertThat(CmsFeature.allAvailable()).containsExactly(CmsFeature.CORE);
|
||||
assertThat(CmsFeature.validate(CmsFeature.allAvailable())).isEmpty();
|
||||
assertThat(CmsFeature.validate(null)).containsExactly("features must include CORE");
|
||||
assertThat(CmsFeature.BLOG.getRequires()).containsExactly(CmsFeature.MEDIA);
|
||||
assertThat(CmsFeature.CORE.isAvailable()).isTrue();
|
||||
|
||||
assertThat(CmsFeature.validate(EnumSet.of(CmsFeature.CORE, CmsFeature.BLOG)))
|
||||
.contains("feature BLOG requires MEDIA", "feature BLOG is not available in this version of qbit-cms");
|
||||
assertThat(CmsFeature.validate(EnumSet.of(CmsFeature.CORE, CmsFeature.PUBLISHING)))
|
||||
.contains("feature PUBLISHING requires BLOG or PROJECTS");
|
||||
assertThat(CmsFeature.validate(EnumSet.of(CmsFeature.CORE, CmsFeature.MEDIA, CmsFeature.BLOG, CmsFeature.STREAM, CmsFeature.PUBLISHING)))
|
||||
.noneMatch(e -> e.contains("requires"));
|
||||
}
|
||||
}
|
||||
@@ -1,262 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.cms;
|
||||
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.Statement;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qbits.cms.security.CmsRole;
|
||||
import com.kingsrook.qbits.cms.security.CmsSecurity;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Installing the qbit into a blank QQQ instance on H2: the host runs the
|
||||
** changelog, produces the qbit (FAIL_FAST schema check), and the CMS tables
|
||||
** work as ordinary QQQ tables.
|
||||
*******************************************************************************/
|
||||
class CmsInstallTest
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@AfterEach
|
||||
void afterEach()
|
||||
{
|
||||
QContext.clear();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fresh install: schema check passes, generic reference data is seeded,
|
||||
** and content round-trips, including children through associations.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testFreshInstall() throws Exception
|
||||
{
|
||||
RDBMSBackendMetaData backend = CmsTestSupport.h2Backend();
|
||||
CmsTestSupport.migrate(backend, Map.of());
|
||||
|
||||
QInstance qInstance = CmsTestSupport.blankInstance(backend);
|
||||
CmsQBitConfig config = new CmsQBitConfig().withBackendName(CmsTestSupport.BACKEND);
|
||||
new CmsQBitProducer().withQBitConfig(config).produce(qInstance);
|
||||
new QInstanceValidator().validate(qInstance);
|
||||
assertThat(config.getRuntime().getSchemaProblems()).isEmpty();
|
||||
|
||||
CmsTestSupport.session(qInstance, CmsSecurity.permissionsFor(config, Set.of(CmsRole.ADMIN)));
|
||||
assertThat(count("cms_redirect_type")).isEqualTo(4);
|
||||
assertThat(count("cms_page_type")).isEqualTo(4);
|
||||
assertThat(count("cms_page_section_type")).isEqualTo(9);
|
||||
|
||||
Integer siteId = insert("cms_site", new QRecord().withValue("name", "Example").withValue("shortName", "example"));
|
||||
insert("cms_domain", new QRecord().withValue("name", "www").withValue("shortName", "www").withValue("fqdn", "www.example.com")
|
||||
.withValue("isPrimary", true).withValue("siteId", siteId));
|
||||
Integer pageTypeId = idOf("cms_page_type", "code", "STATIC");
|
||||
Integer sectionTypeId = idOf("cms_page_section_type", "code", "MARKDOWN");
|
||||
|
||||
QRecord page = new QRecord().withValue("name", "About").withValue("slug", "about").withValue("title", "About us")
|
||||
.withValue("pageTypeId", pageTypeId).withValue("siteId", siteId).withValue("content", "x".repeat(5000))
|
||||
.withAssociatedRecord("sections", new QRecord().withValue("name", "Intro").withValue("sectionTypeId", sectionTypeId).withValue("sortOrder", 1)
|
||||
.withValue("content", "{\"content\": \"# Hello\"}"))
|
||||
.withAssociatedRecord("sections", new QRecord().withValue("name", "Body").withValue("sectionTypeId", sectionTypeId).withValue("sortOrder", 2));
|
||||
Integer pageId = insert("cms_page", page);
|
||||
|
||||
QRecord stored = new GetAction().execute(new GetInput("cms_page").withPrimaryKey(pageId).withIncludeAssociations(true)).getRecord();
|
||||
assertThat(stored.getValueString("content")).hasSize(5000);
|
||||
assertThat(stored.getValueBoolean("isPublished")).isFalse();
|
||||
assertThat(stored.getValue("createDate")).isNotNull();
|
||||
assertThat(stored.getAssociatedRecords().get("sections")).hasSize(2);
|
||||
|
||||
Integer menuId = insert("cms_menu", new QRecord().withValue("name", "Header").withValue("slug", "header").withValue("menuType", "HEADER").withValue("siteId", siteId)
|
||||
.withAssociatedRecord("items", new QRecord().withValue("name", "About").withValue("slug", "about").withValue("itemType", "PAGE").withValue("routePath", "/about")));
|
||||
assertThat(count("cms_menu_item")).isEqualTo(1);
|
||||
assertThat(menuId).isNotNull();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A host can choose any prefix; the changelog property and the config agree.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testCustomPrefix() throws Exception
|
||||
{
|
||||
RDBMSBackendMetaData backend = CmsTestSupport.h2Backend();
|
||||
CmsTestSupport.migrate(backend, Map.of("cms.tablePrefix", "acme_cms_"));
|
||||
|
||||
QInstance qInstance = CmsTestSupport.blankInstance(backend);
|
||||
CmsQBitConfig config = new CmsQBitConfig().withBackendName(CmsTestSupport.BACKEND).withTableNamePrefix("acme_cms_");
|
||||
new CmsQBitProducer().withQBitConfig(config).produce(qInstance);
|
||||
assertThat(qInstance.getTable("acme_cms_page")).isNotNull();
|
||||
assertThat(qInstance.getTable("cms_page")).isNull();
|
||||
|
||||
CmsTestSupport.session(qInstance, CmsSecurity.permissionsFor(config, Set.of(CmsRole.ADMIN)));
|
||||
assertThat(count("acme_cms_redirect_type")).isEqualTo(4);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Running the changelog again changes nothing (the guard runs every time
|
||||
** and passes for tables this changelog created).
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testRerunIsNoop() throws Exception
|
||||
{
|
||||
RDBMSBackendMetaData backend = CmsTestSupport.h2Backend();
|
||||
CmsTestSupport.migrate(backend, Map.of());
|
||||
CmsTestSupport.migrate(backend, Map.of());
|
||||
|
||||
QInstance qInstance = CmsTestSupport.blankInstance(backend);
|
||||
CmsQBitConfig config = new CmsQBitConfig().withBackendName(CmsTestSupport.BACKEND);
|
||||
new CmsQBitProducer().withQBitConfig(config).produce(qInstance);
|
||||
CmsTestSupport.session(qInstance, CmsSecurity.permissionsFor(config, Set.of(CmsRole.ADMIN)));
|
||||
assertThat(count("cms_redirect_type")).isEqualTo(4);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** An existing table with a qbit name halts the install; nothing is
|
||||
** overwritten.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testExistingTableHalts() throws Exception
|
||||
{
|
||||
RDBMSBackendMetaData backend = CmsTestSupport.h2Backend();
|
||||
try(Connection connection = ConnectionManager.getConnection(backend); Statement statement = connection.createStatement())
|
||||
{
|
||||
statement.execute("CREATE TABLE cms_page (id INT PRIMARY KEY, title VARCHAR(10))");
|
||||
statement.execute("INSERT INTO cms_page VALUES (1, 'theirs')");
|
||||
}
|
||||
assertThatThrownBy(() -> CmsTestSupport.migrate(backend, Map.of())).hasStackTraceContaining("was not created by qbit-cms");
|
||||
|
||||
try(Connection connection = ConnectionManager.getConnection(backend); Statement statement = connection.createStatement())
|
||||
{
|
||||
var resultSet = statement.executeQuery("SELECT title FROM cms_page WHERE id = 1");
|
||||
assertThat(resultSet.next()).isTrue();
|
||||
assertThat(resultSet.getString(1)).isEqualTo("theirs");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Reference data is optional.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testSeedOff() throws Exception
|
||||
{
|
||||
RDBMSBackendMetaData backend = CmsTestSupport.h2Backend();
|
||||
CmsTestSupport.migrate(backend, Map.of("cms.seedReferenceData", "false"));
|
||||
|
||||
QInstance qInstance = CmsTestSupport.blankInstance(backend);
|
||||
CmsQBitConfig config = new CmsQBitConfig().withBackendName(CmsTestSupport.BACKEND);
|
||||
new CmsQBitProducer().withQBitConfig(config).produce(qInstance);
|
||||
CmsTestSupport.session(qInstance, CmsSecurity.permissionsFor(config, Set.of(CmsRole.ADMIN)));
|
||||
assertThat(count("cms_redirect_type")).isZero();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Without the changelog, FAIL_FAST refuses to start and names the problem;
|
||||
** DEGRADED starts and records it.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testSchemaCheck() throws Exception
|
||||
{
|
||||
RDBMSBackendMetaData backend = CmsTestSupport.h2Backend();
|
||||
|
||||
CmsQBitConfig failFast = new CmsQBitConfig().withBackendName(CmsTestSupport.BACKEND);
|
||||
assertThatThrownBy(() -> new CmsQBitProducer().withQBitConfig(failFast).produce(CmsTestSupport.blankInstance(backend)))
|
||||
.isInstanceOf(QException.class)
|
||||
.hasMessageContaining("not up to date")
|
||||
.hasMessageContaining("DATABASECHANGELOG");
|
||||
|
||||
CmsQBitConfig degraded = new CmsQBitConfig().withBackendName(CmsTestSupport.BACKEND).withStartupMode(CmsStartupMode.DEGRADED);
|
||||
new CmsQBitProducer().withQBitConfig(degraded).produce(CmsTestSupport.blankInstance(backend));
|
||||
assertThat(degraded.getRuntime().getSchemaProblems()).isNotEmpty();
|
||||
|
||||
////////////////////////////////////////////////////////////////
|
||||
// changelog run without CORE (cms.features): tables missing //
|
||||
////////////////////////////////////////////////////////////////
|
||||
CmsTestSupport.migrate(backend, Map.of("cms.features", "MEDIA"));
|
||||
CmsQBitConfig partial = new CmsQBitConfig().withBackendName(CmsTestSupport.BACKEND).withStartupMode(CmsStartupMode.DEGRADED);
|
||||
new CmsQBitProducer().withQBitConfig(partial).produce(CmsTestSupport.blankInstance(backend));
|
||||
assertThat(partial.getRuntime().getSchemaProblems()).anyMatch(p -> p.contains("1.0.0-create-page") && p.contains("CORE"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static Integer insert(String table, QRecord record) throws QException
|
||||
{
|
||||
List<QRecord> records = new InsertAction().execute(new InsertInput(table).withRecord(record)).getRecords();
|
||||
assertThat(records.get(0).getErrors()).as(table).isEmpty();
|
||||
return (records.get(0).getValueInteger("id"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static int count(String table) throws QException
|
||||
{
|
||||
return (new CountAction().execute(new CountInput(table)).getCount());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static Integer idOf(String table, String field, String value) throws QException
|
||||
{
|
||||
List<QRecord> records = new QueryAction().execute(new QueryInput(table).withFilter(new QQueryFilter(new QFilterCriteria(field, QCriteriaOperator.EQUALS, value)))).getRecords();
|
||||
return (records.get(0).getValueInteger("id"));
|
||||
}
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.cms;
|
||||
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.Statement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qbits.cms.security.CmsRole;
|
||||
import com.kingsrook.qbits.cms.security.CmsSecurity;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.module.postgres.model.metadata.PostgreSQLBackendMetaData;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.testcontainers.containers.PostgreSQLContainer;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The install on PostgreSQL (the production database): the changelog creates
|
||||
** the kof22-shaped schema (identity ids, the same column types and
|
||||
** nullability), the schema check passes, and QQQ reads and writes it.
|
||||
** Skipped when Docker is not available.
|
||||
*******************************************************************************/
|
||||
@Testcontainers(disabledWithoutDocker = true)
|
||||
class CmsPostgresInstallTest
|
||||
{
|
||||
@Container
|
||||
private static final PostgreSQLContainer<?> POSTGRES = new PostgreSQLContainer<>("postgres:17-alpine");
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@AfterEach
|
||||
void afterEach()
|
||||
{
|
||||
QContext.clear();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fresh install on PostgreSQL with the default prefix.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testInstallOnPostgres() throws Exception
|
||||
{
|
||||
ConnectionManager.resetConnectionProviders();
|
||||
PostgreSQLBackendMetaData backend = new PostgreSQLBackendMetaData()
|
||||
.withName(CmsTestSupport.BACKEND)
|
||||
.withHostName(POSTGRES.getHost())
|
||||
.withPort(POSTGRES.getFirstMappedPort())
|
||||
.withDatabaseName(POSTGRES.getDatabaseName())
|
||||
.withUsername(POSTGRES.getUsername())
|
||||
.withPassword(POSTGRES.getPassword());
|
||||
CmsTestSupport.migrate(backend, Map.of());
|
||||
|
||||
QInstance qInstance = CmsTestSupport.blankInstance(backend);
|
||||
CmsQBitConfig config = new CmsQBitConfig().withBackendName(CmsTestSupport.BACKEND);
|
||||
new CmsQBitProducer().withQBitConfig(config).produce(qInstance);
|
||||
assertThat(config.getRuntime().getSchemaProblems()).isEmpty();
|
||||
|
||||
try(Connection connection = ConnectionManager.getConnection(backend); Statement statement = connection.createStatement())
|
||||
{
|
||||
assertThat(columns(statement, "cms_page")).contains(
|
||||
"id bigint NO", "slug character varying(100) NO", "title character varying(200) NO", "content text YES", "is_published boolean NO",
|
||||
"publish_date timestamp without time zone YES", "parent_page_id bigint YES", "create_date timestamp without time zone NO");
|
||||
try(ResultSet resultSet = statement.executeQuery("SELECT pg_get_serial_sequence('cms_page', 'id'), "
|
||||
+ "(SELECT COUNT(*) FROM information_schema.table_constraints WHERE table_name = 'cms_page' AND constraint_type = 'FOREIGN KEY')"))
|
||||
{
|
||||
assertThat(resultSet.next()).isTrue();
|
||||
assertThat(resultSet.getString(1)).isEqualTo("public.cms_page_id_seq");
|
||||
assertThat(resultSet.getInt(2)).isEqualTo(3);
|
||||
}
|
||||
}
|
||||
|
||||
CmsTestSupport.session(qInstance, CmsSecurity.permissionsFor(config, Set.of(CmsRole.ADMIN)));
|
||||
Integer siteId = new InsertAction().execute(new InsertInput("cms_site").withRecord(new QRecord().withValue("name", "Example").withValue("shortName", "ex")))
|
||||
.getRecords().get(0).getValueInteger("id");
|
||||
Integer pageId = new InsertAction().execute(new InsertInput("cms_page").withRecord(new QRecord().withValue("name", "Home").withValue("slug", "home")
|
||||
.withValue("title", "Home").withValue("pageTypeId", 1).withValue("siteId", siteId)
|
||||
.withAssociatedRecord("sections", new QRecord().withValue("name", "Hero").withValue("sectionTypeId", 2).withValue("content", "{}")))).getRecords().get(0).getValueInteger("id");
|
||||
QRecord page = new GetAction().execute(new GetInput("cms_page").withPrimaryKey(pageId).withIncludeAssociations(true)).getRecord();
|
||||
assertThat(page.getAssociatedRecords().get("sections")).hasSize(1);
|
||||
assertThat(page.getValue("createDate")).isNotNull();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** "name type nullable" for each column of a table.
|
||||
*******************************************************************************/
|
||||
private static List<String> columns(Statement statement, String table) throws Exception
|
||||
{
|
||||
List<String> rs = new ArrayList<>();
|
||||
try(ResultSet resultSet = statement.executeQuery("SELECT column_name, data_type, character_maximum_length, is_nullable FROM information_schema.columns "
|
||||
+ "WHERE table_name = '" + table + "' ORDER BY ordinal_position"))
|
||||
{
|
||||
while(resultSet.next())
|
||||
{
|
||||
Object length = resultSet.getObject(3);
|
||||
rs.add(resultSet.getString(1) + " " + resultSet.getString(2) + (length == null ? "" : "(" + length + ")") + " " + resultSet.getString(4));
|
||||
}
|
||||
}
|
||||
return (rs);
|
||||
}
|
||||
}
|
||||
@@ -1,258 +0,0 @@
|
||||
/*
|
||||
* Copyright 2025 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.cms;
|
||||
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qbits.cms.security.CmsRole;
|
||||
import com.kingsrook.qbits.cms.security.CmsSecurity;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.module.postgres.model.metadata.PostgreSQLBackendMetaData;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.testcontainers.containers.PostgreSQLContainer;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The install as production runs it on PostgreSQL 17: a schema OWNER runs the
|
||||
** changelog (Liquibase 4.33.0) with a prefix that cannot collide with a host's own
|
||||
** CMS tables, and the application runs as a DML-only role that owns nothing, cannot
|
||||
** read migration history and cannot run DDL. The startup check must work for that
|
||||
** role (catalog verification), and the QQQ permission ceilings must hold on top of
|
||||
** the database grants.
|
||||
*******************************************************************************/
|
||||
@Testcontainers(disabledWithoutDocker = true)
|
||||
class CmsPostgresRuntimeIdentityTest
|
||||
{
|
||||
private static final String OWNER = "qcms_owner";
|
||||
private static final String RUNTIME = "qcms_runtime";
|
||||
private static final String PREFIX = "qcms_";
|
||||
|
||||
@Container
|
||||
private static final PostgreSQLContainer<?> POSTGRES = new PostgreSQLContainer<>("postgres:17-alpine");
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Owner, runtime role, owner-run migration (twice), DML-only grants.
|
||||
*******************************************************************************/
|
||||
@BeforeAll
|
||||
static void provision() throws Exception
|
||||
{
|
||||
try(Connection admin = connect(POSTGRES.getUsername(), POSTGRES.getPassword(), "postgres"); Statement statement = admin.createStatement())
|
||||
{
|
||||
statement.execute("CREATE ROLE " + OWNER + " LOGIN PASSWORD 'owner-synthetic'");
|
||||
statement.execute("CREATE ROLE " + RUNTIME + " LOGIN PASSWORD 'runtime-synthetic'");
|
||||
statement.execute("CREATE DATABASE cmsdb OWNER " + OWNER);
|
||||
statement.execute("REVOKE ALL ON DATABASE cmsdb FROM PUBLIC");
|
||||
statement.execute("GRANT CONNECT ON DATABASE cmsdb TO " + RUNTIME);
|
||||
}
|
||||
try(Connection owner = connect(OWNER, "owner-synthetic", "cmsdb"); Statement statement = owner.createStatement())
|
||||
{
|
||||
statement.execute("REVOKE ALL ON SCHEMA public FROM PUBLIC");
|
||||
statement.execute("GRANT USAGE ON SCHEMA public TO " + RUNTIME);
|
||||
}
|
||||
|
||||
ConnectionManager.resetConnectionProviders();
|
||||
PostgreSQLBackendMetaData ownerBackend = backend(OWNER, "owner-synthetic");
|
||||
CmsTestSupport.migrate(ownerBackend, Map.of("cms.tablePrefix", PREFIX));
|
||||
CmsTestSupport.migrate(ownerBackend, Map.of("cms.tablePrefix", PREFIX));
|
||||
|
||||
try(Connection owner = connect(OWNER, "owner-synthetic", "cmsdb"); Statement statement = owner.createStatement())
|
||||
{
|
||||
statement.execute("DO $$ DECLARE t text; BEGIN FOR t IN SELECT tablename FROM pg_tables WHERE schemaname = 'public' AND tablename LIKE '" + PREFIX
|
||||
+ "%' LOOP EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON %I TO " + RUNTIME + "', t); END LOOP; END $$");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Clear the context.
|
||||
*******************************************************************************/
|
||||
@AfterEach
|
||||
void afterEach()
|
||||
{
|
||||
QContext.clear();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The original history check cannot run as the DML-only role and must fail
|
||||
** closed; the catalog check starts it.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testStartupCheckAsRuntimeRole() throws Exception
|
||||
{
|
||||
CmsQBitConfig history = config().withSchemaVerification(CmsSchemaVerification.CHANGELOG_HISTORY);
|
||||
assertThatThrownBy(() -> new CmsQBitProducer().withQBitConfig(history).produce(runtimeInstance()))
|
||||
.isInstanceOf(QException.class).hasMessageContaining("not up to date");
|
||||
|
||||
CmsQBitConfig catalog = config().withSchemaVerification(CmsSchemaVerification.CATALOG);
|
||||
new CmsQBitProducer().withQBitConfig(catalog).produce(runtimeInstance());
|
||||
assertThat(catalog.getRuntime().getSchemaProblems()).isEmpty();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The catalog check names a missing table and a missing column, and refuses to
|
||||
** start in FAIL_FAST mode.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testCatalogCheckFindsDrift() throws Exception
|
||||
{
|
||||
CmsQBitConfig unmigrated = new CmsQBitConfig().withBackendName(CmsTestSupport.BACKEND).withTableNamePrefix("zcms_").withNamePrefix("zcms")
|
||||
.withPermissionBaseName("zcms").withSchemaVerification(CmsSchemaVerification.CATALOG);
|
||||
assertThatThrownBy(() -> new CmsQBitProducer().withQBitConfig(unmigrated).produce(runtimeInstance()))
|
||||
.isInstanceOf(QException.class).hasMessageContaining("zcms_page is missing");
|
||||
|
||||
try(Connection owner = connect(OWNER, "owner-synthetic", "cmsdb"); Statement statement = owner.createStatement())
|
||||
{
|
||||
statement.execute("ALTER TABLE " + PREFIX + "page RENAME COLUMN title TO title_old");
|
||||
try
|
||||
{
|
||||
CmsQBitConfig drifted = config().withSchemaVerification(CmsSchemaVerification.CATALOG);
|
||||
assertThatThrownBy(() -> new CmsQBitProducer().withQBitConfig(drifted).produce(runtimeInstance()))
|
||||
.isInstanceOf(QException.class).hasMessageContaining(PREFIX + "page.title is missing");
|
||||
}
|
||||
finally
|
||||
{
|
||||
statement.execute("ALTER TABLE " + PREFIX + "page RENAME COLUMN title_old TO title");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The runtime role can do DML and nothing else: no DDL, no migration history.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testRuntimeRoleIsDmlOnly() throws Exception
|
||||
{
|
||||
try(Connection runtime = connect(RUNTIME, "runtime-synthetic", "cmsdb"); Statement statement = runtime.createStatement())
|
||||
{
|
||||
assertThatThrownBy(() -> statement.execute("CREATE TABLE intruder (id int)")).isInstanceOf(SQLException.class).hasMessageContaining("permission denied");
|
||||
assertThatThrownBy(() -> statement.execute("ALTER TABLE " + PREFIX + "site ADD COLUMN x int")).isInstanceOf(SQLException.class);
|
||||
assertThatThrownBy(() -> statement.execute("DROP TABLE " + PREFIX + "site")).isInstanceOf(SQLException.class);
|
||||
assertThatThrownBy(() -> statement.execute("TRUNCATE " + PREFIX + "site")).isInstanceOf(SQLException.class);
|
||||
assertThatThrownBy(() -> statement.executeQuery("SELECT * FROM databasechangelog")).isInstanceOf(SQLException.class).hasMessageContaining("permission denied");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Role ceilings over the DML grants, through QQQ's own actions on the native PG
|
||||
** backend as the runtime role: an admin writes, a viewer reads only, an editor
|
||||
** cannot change reference data, and a user with no CMS permission is refused.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testRoleCeilingsAsRuntimeRole() throws Exception
|
||||
{
|
||||
CmsQBitConfig config = config().withSchemaVerification(CmsSchemaVerification.CATALOG);
|
||||
QInstance instance = runtimeInstance();
|
||||
instance.setEnforcePermissionsInCoreActions(true);
|
||||
new CmsQBitProducer().withQBitConfig(config).produce(instance);
|
||||
|
||||
CmsTestSupport.session(instance, CmsSecurity.permissionsFor(config, Set.of(CmsRole.ADMIN)));
|
||||
Integer siteId = new InsertAction().execute(new InsertInput(PREFIX + "site").withRecord(new QRecord().withValue("name", "Example").withValue("shortName", "ex")))
|
||||
.getRecords().get(0).getValueInteger("id");
|
||||
assertThat(siteId).isNotNull();
|
||||
|
||||
CmsTestSupport.session(instance, CmsSecurity.permissionsFor(config, Set.of(CmsRole.VIEWER)));
|
||||
assertThat(new QueryAction().execute(new QueryInput(PREFIX + "site")).getRecords()).isNotEmpty();
|
||||
assertThatThrownBy(() -> new InsertAction().execute(new InsertInput(PREFIX + "site").withRecord(new QRecord().withValue("name", "V").withValue("shortName", "v"))))
|
||||
.isInstanceOf(QPermissionDeniedException.class);
|
||||
|
||||
CmsTestSupport.session(instance, CmsSecurity.permissionsFor(config, Set.of(CmsRole.EDITOR)));
|
||||
assertThatThrownBy(() -> new InsertAction().execute(new InsertInput(PREFIX + "page_type").withRecord(new QRecord().withValue("name", "X"))))
|
||||
.isInstanceOf(QPermissionDeniedException.class);
|
||||
|
||||
CmsTestSupport.session(instance, Set.of());
|
||||
assertThatThrownBy(() -> new QueryAction().execute(new QueryInput(PREFIX + "site"))).isInstanceOf(QPermissionDeniedException.class);
|
||||
assertThatThrownBy(() -> new QueryAction().execute(new QueryInput(PREFIX + "page"))).isInstanceOf(QPermissionDeniedException.class);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The configuration under test: a prefix of its own, never the default.
|
||||
*******************************************************************************/
|
||||
private static CmsQBitConfig config()
|
||||
{
|
||||
return (new CmsQBitConfig().withBackendName(CmsTestSupport.BACKEND).withTableNamePrefix(PREFIX).withNamePrefix("qcms").withPermissionBaseName("qcms"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** An application instance on the runtime role.
|
||||
*******************************************************************************/
|
||||
private static QInstance runtimeInstance()
|
||||
{
|
||||
ConnectionManager.resetConnectionProviders();
|
||||
return (CmsTestSupport.blankInstance(backend(RUNTIME, "runtime-synthetic")));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A native PostgreSQL backend as a named role.
|
||||
*******************************************************************************/
|
||||
private static PostgreSQLBackendMetaData backend(String user, String password)
|
||||
{
|
||||
return (new PostgreSQLBackendMetaData()
|
||||
.withName(CmsTestSupport.BACKEND)
|
||||
.withHostName(POSTGRES.getHost())
|
||||
.withPort(POSTGRES.getFirstMappedPort())
|
||||
.withDatabaseName("cmsdb")
|
||||
.withUsername(user)
|
||||
.withPassword(password));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A JDBC connection as a named role.
|
||||
*******************************************************************************/
|
||||
private static Connection connect(String user, String password, String database) throws SQLException
|
||||
{
|
||||
return (DriverManager.getConnection("jdbc:postgresql://" + POSTGRES.getHost() + ":" + POSTGRES.getFirstMappedPort() + "/" + database, user, password));
|
||||
}
|
||||
}
|
||||
@@ -17,218 +17,66 @@
|
||||
package com.kingsrook.qbits.cms;
|
||||
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppSection;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSTableBackendDetails;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitConfigValidationException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** CmsQBitProducer: what it produces, validation, namespaces and collisions
|
||||
** (no database: startupMode SKIP).
|
||||
** Unit test for CmsQBitProducer
|
||||
*******************************************************************************/
|
||||
class CmsQBitProducerTest
|
||||
{
|
||||
private static final List<String> CORE_TABLES = List.of("cms_site", "cms_domain", "cms_redirect", "cms_redirect_type", "cms_page", "cms_page_type",
|
||||
"cms_page_section", "cms_page_section_type", "cms_snippet", "cms_menu", "cms_menu_item");
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The CORE feature: eleven standard tables named per the owner rule, a
|
||||
** possible-value source each, child lists, apps with homes, and a valid
|
||||
** instance.
|
||||
** A configured qbit registers itself in the instance.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testProduceCore() throws Exception
|
||||
void testProduce() throws Exception
|
||||
{
|
||||
QInstance qInstance = CmsTestSupport.blankInstance(CmsTestSupport.h2Backend());
|
||||
CmsQBitConfig config = config();
|
||||
CmsQBitProducer producer = new CmsQBitProducer().withQBitConfig(config);
|
||||
QInstance qInstance = new QInstance();
|
||||
CmsQBitConfig config = new CmsQBitConfig().withBackendName("rdbms");
|
||||
CmsQBitProducer producer = new CmsQBitProducer().withQBitConfig(config).withNamespace("cms");
|
||||
|
||||
assertThat(producer.isEnabled()).isTrue();
|
||||
assertThat(producer.getQBitConfig()).isSameAs(config);
|
||||
assertThat(producer.produce(qInstance).getEach(QTableMetaData.class)).isEmpty();
|
||||
new QInstanceValidator().validate(qInstance);
|
||||
assertThat(producer.getNamespace()).isEqualTo("cms");
|
||||
assertThat(config.getDefaultBackendNameForTables()).isEqualTo("rdbms");
|
||||
|
||||
producer.produce(qInstance);
|
||||
assertThat(qInstance.getQBits()).hasSize(1);
|
||||
assertThat(qInstance.getQBits().values().iterator().next().getArtifactId()).isEqualTo("qbit-cms");
|
||||
for(String name : CORE_TABLES)
|
||||
{
|
||||
QTableMetaData table = qInstance.getTable(name);
|
||||
assertThat(table).as(name).isNotNull();
|
||||
assertThat(name).matches("^cms_[a-z][a-z_]*$");
|
||||
assertThat(((RDBMSTableBackendDetails) table.getBackendDetails()).getTableName()).isEqualTo(name);
|
||||
assertThat(table.getBackendName()).isEqualTo(CmsTestSupport.BACKEND);
|
||||
assertThat(table.getPermissionRules().getLevel()).isEqualTo(PermissionLevel.READ_INSERT_EDIT_DELETE_PERMISSIONS);
|
||||
assertThat(table.getIsHidden()).as(name + " is visible").isFalse();
|
||||
assertThat(table.getField("createDate").getBackendName()).isEqualTo("create_date");
|
||||
assertThat(qInstance.getPossibleValueSource(name)).as(name + " PVS").isNotNull();
|
||||
}
|
||||
assertThat(qInstance.getTable("cms_page").getField("pageTypeId").getBackendName()).isEqualTo("page_type_id");
|
||||
assertThat(qInstance.getTable("cms_page").getField("siteId").getPossibleValueSourceName()).isEqualTo("cms_site");
|
||||
assertThat(qInstance.getTable("cms_redirect").getField("matchType").getPossibleValueSourceName()).isEqualTo("cms_redirect_match_type");
|
||||
assertThat(qInstance.getTable("cms_page").getAssociations()).extracting("name").contains("sections");
|
||||
assertThat(qInstance.getTable("cms_menu").getAssociations()).extracting("name").contains("items");
|
||||
|
||||
QAppMetaData top = qInstance.getApp("cms");
|
||||
assertThat(top.getLabel()).isEqualTo("CMS");
|
||||
assertThat(qInstance.getTable("cms_page_section_type").getLabel()).isEqualTo("Page Section Types");
|
||||
assertThat(top.getWidgets()).containsExactly("cmsHome");
|
||||
assertThat(top.getChildren()).extracting("name").containsExactly("cmsSites", "cmsPages", "cmsNavigation");
|
||||
assertThat(qInstance.getApp("cmsPages").getWidgets()).containsExactly("cmsPagesHome");
|
||||
assertThat(qInstance.getWidget("cmsPagesHome")).isNotNull();
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// the nav lists main and reference tables only, never a child //
|
||||
// table, and no processes //
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
List<String> navTables = qInstance.getApps().values().stream().filter(a -> a.getSections() != null)
|
||||
.flatMap(a -> a.getSections().stream()).flatMap(s -> s.getTables() == null ? java.util.stream.Stream.empty() : s.getTables().stream()).toList();
|
||||
assertThat(navTables).containsExactlyInAnyOrder("cms_site", "cms_redirect", "cms_redirect_type", "cms_page", "cms_snippet", "cms_page_type",
|
||||
"cms_page_section_type", "cms_menu");
|
||||
for(QAppMetaData app : qInstance.getApps().values())
|
||||
{
|
||||
for(QAppSection section : app.getSections() == null ? List.<QAppSection>of() : app.getSections())
|
||||
{
|
||||
assertThat(section.getProcesses()).as(app.getName()).isNullOrEmpty();
|
||||
}
|
||||
}
|
||||
assertThat(qInstance.getTable("cms_page").getSections()).extracting("name").contains("cms_page_cms_page_section_pageId");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Validation lists every problem.
|
||||
** Without a config the producer is disabled; without a backend it fails.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testValidation()
|
||||
void testDisabledAndInvalid()
|
||||
{
|
||||
assertThat(new CmsQBitProducer().isEnabled()).isFalse();
|
||||
assertThatThrownBy(() -> new CmsQBitProducer().produce(new QInstance())).hasMessageContaining("no config");
|
||||
|
||||
QInstance qInstance = CmsTestSupport.blankInstance(CmsTestSupport.h2Backend());
|
||||
qInstance.addBackend(new QBackendMetaData().withName("memory").withBackendType(MemoryBackendModule.class));
|
||||
assertThatThrownBy(() -> produce(qInstance, new CmsQBitConfig())).hasMessageContaining("backendName is required");
|
||||
assertThatThrownBy(() -> produce(qInstance, config().withBackendName("nope"))).hasMessageContaining("not a backend");
|
||||
assertThatThrownBy(() -> produce(qInstance, config().withBackendName("memory"))).hasMessageContaining("must be an RDBMS backend");
|
||||
assertThatThrownBy(() -> produce(qInstance, config().withTableNamePrefix("CMS"))).hasMessageContaining("tableNamePrefix");
|
||||
assertThatThrownBy(() -> produce(qInstance, config().withNamePrefix("9x"))).hasMessageContaining("namePrefix");
|
||||
assertThatThrownBy(() -> produce(qInstance, config().withPermissionBaseName(""))).hasMessageContaining("permissionBaseName");
|
||||
assertThatThrownBy(() -> produce(qInstance, config().withParentAppName("content"))).hasMessageContaining("parentAppName content");
|
||||
assertThatThrownBy(() -> produce(qInstance, config().withFeatures(EnumSet.of(CmsFeature.CORE, CmsFeature.MEDIA)))).hasMessageContaining("MEDIA is not available");
|
||||
assertThatThrownBy(() -> produce(qInstance, config().withFeatures(Set.of()))).hasMessageContaining("must include CORE");
|
||||
assertThatThrownBy(() -> produce(qInstance, config().withRoleMapping(null))).hasMessageContaining("roleMapping");
|
||||
assertThatThrownBy(() -> produce(qInstance, config().withStartupMode(null))).hasMessageContaining("startupMode");
|
||||
assertThat(qInstance.getQBits()).isNullOrEmpty();
|
||||
CmsQBitConfig config = new CmsQBitConfig();
|
||||
config.setBackendName(null);
|
||||
assertThat(config.getBackendName()).isNull();
|
||||
assertThatThrownBy(() -> new CmsQBitProducer().withQBitConfig(config).produce(new QInstance()))
|
||||
.isInstanceOf(QBitConfigValidationException.class);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A second install needs a namespace, which becomes its prefixes.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testNamespaceAndCollisions() throws Exception
|
||||
{
|
||||
QInstance qInstance = CmsTestSupport.blankInstance(CmsTestSupport.h2Backend());
|
||||
produce(qInstance, config());
|
||||
|
||||
assertThatThrownBy(() -> produce(qInstance, config()))
|
||||
.isInstanceOf(QException.class)
|
||||
.hasMessageContaining("collide")
|
||||
.hasMessageContaining("table cms_site");
|
||||
|
||||
CmsQBitConfig second = config();
|
||||
new CmsQBitProducer().withQBitConfig(second).withNamespace("siteTwo").produce(qInstance);
|
||||
assertThat(second.getTableNamePrefix()).isEqualTo("site_two_");
|
||||
assertThat(second.getNamePrefix()).isEqualTo("siteTwo");
|
||||
assertThat(second.getPermissionBaseName()).isEqualTo("site_two");
|
||||
assertThat(qInstance.getTable("site_two_page")).isNotNull();
|
||||
assertThat(qInstance.getApp("siteTwo")).isNotNull();
|
||||
assertThat(qInstance.getApp("siteTwoPages")).isNotNull();
|
||||
assertThat(qInstance.getQBits()).hasSize(2);
|
||||
assertThat(CmsRuntime.findConfig(qInstance, "siteTwo")).containsSame(second);
|
||||
new QInstanceValidator().validate(qInstance);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The host can nest the CMS under one of its apps.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testParentApp() throws Exception
|
||||
{
|
||||
QInstance qInstance = CmsTestSupport.blankInstance(CmsTestSupport.h2Backend());
|
||||
qInstance.addApp(new QAppMetaData().withName("content").withLabel("Content"));
|
||||
produce(qInstance, config().withParentAppName("content").withAppName("cmsApp").withAppLabel("Website"));
|
||||
assertThat(qInstance.getApp("content").getChildren()).extracting("name").containsExactly("cmsApp");
|
||||
assertThat(qInstance.getApp("cmsApp").getLabel()).isEqualTo("Website");
|
||||
new QInstanceValidator().validate(qInstance);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** With openReads, tables keep their rule names and get the open-reads
|
||||
** checker.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testOpenReadsRules() throws Exception
|
||||
{
|
||||
QInstance qInstance = CmsTestSupport.blankInstance(CmsTestSupport.h2Backend());
|
||||
produce(qInstance, config().withOpenReads(true));
|
||||
assertThat(qInstance.getTable("cms_page").getPermissionRules().getCustomPermissionChecker()).isNotNull();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Naming constants follow the owner rule.
|
||||
** Names follow the owner rule: lowercase snake_case with the full qbit name.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testNaming()
|
||||
{
|
||||
CmsQBitConfig config = config();
|
||||
assertThat(CmsQBitConfig.DEFAULT_TABLE_NAME_PREFIX).isEqualTo("cms_").matches("^[a-z][a-z0-9_]*_$");
|
||||
assertThat(config.applyPrefix("page")).isEqualTo("cms_page");
|
||||
assertThat(config.name("Pages")).isEqualTo("cmsPages");
|
||||
assertThat(config.permission("admin")).isEqualTo("cms.admin");
|
||||
assertThat(config.getEffectiveAppName()).isEqualTo("cms");
|
||||
assertThat(config.toString()).contains("tableNamePrefix=cms_");
|
||||
assertThat(config.getDefaultBackendNameForTables()).isEqualTo(CmsTestSupport.BACKEND);
|
||||
assertThat(CmsQBitConfig.TABLE_NAME_PREFIX).isEqualTo("cms_").matches("^[a-z][a-z0-9_]*_$");
|
||||
assertThat(CmsQBitConfig.PERMISSION_BASE_NAME).isEqualTo("cms");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static CmsQBitConfig config()
|
||||
{
|
||||
return (new CmsQBitConfig().withBackendName(CmsTestSupport.BACKEND).withStartupMode(CmsStartupMode.SKIP));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static void produce(QInstance qInstance, CmsQBitConfig config) throws QException
|
||||
{
|
||||
new CmsQBitProducer().withQBitConfig(config).produce(qInstance);
|
||||
}
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.cms;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qbits.cms.security.CmsRole;
|
||||
import com.kingsrook.qbits.cms.security.CmsSecurity;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Roles, permissions and their enforcement (spec section 6) with QQQ core
|
||||
** permission enforcement on, as QQQ 4.2 will default.
|
||||
*******************************************************************************/
|
||||
class CmsSecurityTest
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@AfterEach
|
||||
void afterEach()
|
||||
{
|
||||
QContext.clear();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Role bundles.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testRolePermissions()
|
||||
{
|
||||
CmsQBitConfig config = new CmsQBitConfig().withBackendName(CmsTestSupport.BACKEND)
|
||||
.withRole("cms-admins", CmsRole.ADMIN).withRole("writers", CmsRole.EDITOR).withRole("readers", CmsRole.VIEWER);
|
||||
|
||||
assertThat(CmsSecurity.rolesFor(config, List.of("writers", "unknown"))).containsExactly(CmsRole.EDITOR);
|
||||
assertThat(CmsSecurity.rolesFor(config, null)).isEmpty();
|
||||
|
||||
Set<String> admin = CmsSecurity.permissionsFor(config, Set.of(CmsRole.ADMIN));
|
||||
assertThat(admin).contains("cms_page.read", "cms_page.delete", "cms_page_type.edit", "cms.admin", "cms.publish");
|
||||
|
||||
Set<String> editor = CmsSecurity.permissionsFor(config, Set.of(CmsRole.EDITOR));
|
||||
assertThat(editor).contains("cms_page.insert", "cms_page.edit", "cms_page.delete", "cms_page_type.read")
|
||||
.doesNotContain("cms_page_type.edit", "cms.publish", "cms.admin");
|
||||
|
||||
assertThat(CmsSecurity.permissionsFor(config, Set.of(CmsRole.PUBLISHER))).contains("cms.publish").doesNotContain("cms.admin");
|
||||
|
||||
Set<String> drafter = CmsSecurity.permissionsFor(config, Set.of(CmsRole.DRAFTER));
|
||||
assertThat(drafter).contains("cms_page.insert", "cms_page.edit").doesNotContain("cms_page.delete", "cms.publish");
|
||||
|
||||
Set<String> viewer = CmsSecurity.permissionsFor(config, Set.of(CmsRole.VIEWER));
|
||||
assertThat(viewer).allMatch(p -> p.endsWith(".read")).contains("cms_menu_item.read");
|
||||
|
||||
QSession session = new QSession().withPermission("host.thing");
|
||||
CmsSecurity.applySessionPermissions(session, List.of("readers"), config);
|
||||
assertThat(session.getPermissions()).contains("host.thing", "cms_page.read").doesNotContain("cms_page.insert");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Enforced: a viewer reads but cannot write; an editor writes content but
|
||||
** not reference data; a session without CMS permissions cannot read.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testEnforcement() throws Exception
|
||||
{
|
||||
CmsQBitConfig config = new CmsQBitConfig().withBackendName(CmsTestSupport.BACKEND);
|
||||
QInstance qInstance = install(config);
|
||||
|
||||
CmsTestSupport.session(qInstance, CmsSecurity.permissionsFor(config, Set.of(CmsRole.VIEWER)));
|
||||
assertThat(new QueryAction().execute(new QueryInput("cms_redirect_type")).getRecords()).hasSize(4);
|
||||
assertThatThrownBy(() -> insertSite("viewer")).isInstanceOf(QPermissionDeniedException.class);
|
||||
|
||||
CmsTestSupport.session(qInstance, CmsSecurity.permissionsFor(config, Set.of(CmsRole.EDITOR)));
|
||||
Integer siteId = insertSite("editor");
|
||||
assertThat(siteId).isNotNull();
|
||||
assertThatThrownBy(() -> new UpdateAction().execute(new UpdateInput("cms_page_type").withRecord(new QRecord().withValue("id", 1).withValue("name", "X"))))
|
||||
.isInstanceOf(QPermissionDeniedException.class);
|
||||
|
||||
CmsTestSupport.session(qInstance, CmsSecurity.permissionsFor(config, Set.of(CmsRole.DRAFTER)));
|
||||
assertThatThrownBy(() -> new DeleteAction().execute(new DeleteInput("cms_site").withPrimaryKey(siteId))).isInstanceOf(QPermissionDeniedException.class);
|
||||
|
||||
CmsTestSupport.session(qInstance, Set.of());
|
||||
assertThatThrownBy(() -> new QueryAction().execute(new QueryInput("cms_page"))).isInstanceOf(QPermissionDeniedException.class);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** openReads: anyone reads, writes still need the table permission.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testOpenReads() throws Exception
|
||||
{
|
||||
CmsQBitConfig config = new CmsQBitConfig().withBackendName(CmsTestSupport.BACKEND).withOpenReads(true);
|
||||
QInstance qInstance = install(config);
|
||||
|
||||
CmsTestSupport.session(qInstance, Set.of());
|
||||
assertThat(new QueryAction().execute(new QueryInput("cms_redirect_type")).getRecords()).hasSize(4);
|
||||
assertThatThrownBy(() -> insertSite("anonymous")).isInstanceOf(QPermissionDeniedException.class);
|
||||
|
||||
CmsTestSupport.session(qInstance, Set.of("cms_site.insert"));
|
||||
assertThat(insertSite("writer")).isNotNull();
|
||||
assertThatThrownBy(() -> new DeleteAction().execute(new DeleteInput("cms_site").withPrimaryKey(1))).isInstanceOf(QPermissionDeniedException.class);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A migrated, enforced install.
|
||||
*******************************************************************************/
|
||||
private static QInstance install(CmsQBitConfig config) throws Exception
|
||||
{
|
||||
RDBMSBackendMetaData backend = CmsTestSupport.h2Backend();
|
||||
CmsTestSupport.migrate(backend, Map.of());
|
||||
QInstance qInstance = CmsTestSupport.blankInstance(backend);
|
||||
qInstance.setEnforcePermissionsInCoreActions(true);
|
||||
new CmsQBitProducer().withQBitConfig(config).produce(qInstance);
|
||||
return (qInstance);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static Integer insertSite(String name) throws Exception
|
||||
{
|
||||
return (new InsertAction().execute(new InsertInput("cms_site").withRecord(new QRecord().withValue("name", name).withValue("shortName", name)))
|
||||
.getRecords().get(0).getValueInteger("id"));
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.cms;
|
||||
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData;
|
||||
import liquibase.Contexts;
|
||||
import liquibase.LabelExpression;
|
||||
import liquibase.Liquibase;
|
||||
import liquibase.database.Database;
|
||||
import liquibase.database.DatabaseFactory;
|
||||
import liquibase.database.jvm.JdbcConnection;
|
||||
import liquibase.resource.ClassLoaderResourceAccessor;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Test fixtures: a blank QQQ instance on a fresh in-memory H2 database (the
|
||||
** "any running QQQ system" a host starts from), the qbit's changelog run the
|
||||
** way a host runs it, and sessions with chosen permissions.
|
||||
*******************************************************************************/
|
||||
public final class CmsTestSupport
|
||||
{
|
||||
public static final String BACKEND = "rdbms";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Not instantiable.
|
||||
*******************************************************************************/
|
||||
private CmsTestSupport()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A fresh H2 backend (its own database). QQQ caches connection providers
|
||||
** by backend name, so the cache is reset first.
|
||||
*******************************************************************************/
|
||||
public static RDBMSBackendMetaData h2Backend()
|
||||
{
|
||||
ConnectionManager.resetConnectionProviders();
|
||||
return (new RDBMSBackendMetaData()
|
||||
.withName(BACKEND)
|
||||
.withVendor("h2")
|
||||
.withHostName("mem")
|
||||
.withDatabaseName("cms_" + UUID.randomUUID().toString().replace("-", ""))
|
||||
.withUsername("sa"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A blank instance with one RDBMS backend and mock authentication.
|
||||
*******************************************************************************/
|
||||
public static QInstance blankInstance(RDBMSBackendMetaData backend)
|
||||
{
|
||||
QInstance qInstance = new QInstance();
|
||||
qInstance.addBackend(backend);
|
||||
qInstance.withInstanceDefaultAuthentication(new QAuthenticationMetaData().withName("mock").withType(QAuthenticationType.MOCK));
|
||||
return (qInstance);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Run the qbit's changelog against a backend, as a host's Liquibase would,
|
||||
** with the given changelog properties.
|
||||
*******************************************************************************/
|
||||
public static void migrate(RDBMSBackendMetaData backend, Map<String, String> properties) throws Exception
|
||||
{
|
||||
Connection connection = ConnectionManager.getConnection(backend);
|
||||
Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnection(connection));
|
||||
try(Liquibase liquibase = new Liquibase(CmsQBitConfig.CHANGELOG, new ClassLoaderResourceAccessor(), database))
|
||||
{
|
||||
properties.forEach(liquibase::setChangeLogParameter);
|
||||
liquibase.update(new Contexts(), new LabelExpression());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Start a context on an instance with a session holding these permissions.
|
||||
*******************************************************************************/
|
||||
public static QSession session(QInstance qInstance, Collection<String> permissions) throws Exception
|
||||
{
|
||||
QSession session = new QSession().withPermissions(permissions);
|
||||
QContext.init(qInstance, session);
|
||||
return (session);
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.cms.demo;
|
||||
|
||||
|
||||
import com.kingsrook.qbits.cms.CmsQBitConfig;
|
||||
import com.kingsrook.qbits.cms.CmsQBitProducer;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.instances.AbstractQQQApplication;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.branding.QBrandingMetaData;
|
||||
import com.kingsrook.qqq.backend.module.postgres.model.metadata.PostgreSQLBackendMetaData;
|
||||
import com.kingsrook.qqq.middleware.javalin.QApplicationJavalinServer;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A local demo host for looking at the CMS in the Next UI (screenshots): a
|
||||
** blank QQQ app with one PostgreSQL backend and the qbit, nothing else.
|
||||
** Not part of the build; run it from the IDE or with exec:java against a
|
||||
** database the qbit's changelog has run on:
|
||||
**
|
||||
** CMS_DEMO_DB_PORT (default 55433), CMS_DEMO_DB_NAME (cmsdemo),
|
||||
** CMS_DEMO_DB_USER (postgres), CMS_DEMO_DB_PASSWORD, CMS_DEMO_PORT (8000).
|
||||
**
|
||||
** It signs everyone in anonymously, so it runs the CMS with openReads.
|
||||
*******************************************************************************/
|
||||
public class CmsDemoServer extends AbstractQQQApplication
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
** Start the demo.
|
||||
*******************************************************************************/
|
||||
public static void main(String[] args) throws Exception
|
||||
{
|
||||
new QApplicationJavalinServer(new CmsDemoServer())
|
||||
.withServeFrontendNext(true)
|
||||
.withServeFrontendMaterialDashboard(false)
|
||||
.withPort(Integer.parseInt(env("CMS_DEMO_PORT", "8000")))
|
||||
.start();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The demo instance.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public QInstance defineQInstance() throws QException
|
||||
{
|
||||
QInstance qInstance = new QInstance();
|
||||
qInstance.addBackend(new PostgreSQLBackendMetaData()
|
||||
.withName("database")
|
||||
.withHostName("localhost")
|
||||
.withPort(Integer.parseInt(env("CMS_DEMO_DB_PORT", "55433")))
|
||||
.withDatabaseName(env("CMS_DEMO_DB_NAME", "cmsdemo"))
|
||||
.withUsername(env("CMS_DEMO_DB_USER", "postgres"))
|
||||
.withPassword(env("CMS_DEMO_DB_PASSWORD", "")));
|
||||
qInstance.withInstanceDefaultAuthentication(new QAuthenticationMetaData().withName("anonymous").withType(QAuthenticationType.FULLY_ANONYMOUS));
|
||||
qInstance.setBranding(new QBrandingMetaData().withAppName("QBit CMS demo").withCompanyName("QRun-IO"));
|
||||
|
||||
new CmsQBitProducer()
|
||||
.withQBitConfig(new CmsQBitConfig().withBackendName("database").withOpenReads(true))
|
||||
.produce(qInstance);
|
||||
return (qInstance);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static String env(String name, String defaultValue)
|
||||
{
|
||||
String value = System.getenv(name);
|
||||
return (value == null || value.isEmpty() ? defaultValue : value);
|
||||
}
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.cms.home;
|
||||
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qbits.cms.CmsQBitConfig;
|
||||
import com.kingsrook.qbits.cms.CmsQBitProducer;
|
||||
import com.kingsrook.qbits.cms.CmsStartupMode;
|
||||
import com.kingsrook.qbits.cms.CmsTestSupport;
|
||||
import com.kingsrook.qbits.cms.metadata.CmsTableDefinition;
|
||||
import com.kingsrook.qbits.cms.security.CmsRole;
|
||||
import com.kingsrook.qbits.cms.security.CmsSecurity;
|
||||
import com.kingsrook.qqq.backend.core.actions.dashboard.RenderWidgetAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.RawHTML;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The CMS homes render real data read as the user, honest empty states, and
|
||||
** leave out what the user may not read.
|
||||
*******************************************************************************/
|
||||
class CmsHomeRendererTest
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@AfterEach
|
||||
void afterEach()
|
||||
{
|
||||
QContext.clear();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Empty install: area homes say so honestly; settings show seeded types.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testEmptyInstall() throws Exception
|
||||
{
|
||||
CmsQBitConfig config = new CmsQBitConfig().withBackendName(CmsTestSupport.BACKEND);
|
||||
QInstance qInstance = install(config);
|
||||
CmsTestSupport.session(qInstance, CmsSecurity.permissionsFor(config, Set.of(CmsRole.ADMIN)));
|
||||
|
||||
String top = render(qInstance, "cmsHome");
|
||||
assertThat(top).contains("Sites", "Pages", "Navigation", "/app/cmsPages", "0 pages");
|
||||
|
||||
String sites = render(qInstance, "cmsSitesHome");
|
||||
assertThat(sites).contains("Nothing yet", "Needs attention", "everything here is published and active", "New site", "New redirect");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** With content: counts, drafts, inactive rows and recently changed records.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testWithContent() throws Exception
|
||||
{
|
||||
CmsQBitConfig config = new CmsQBitConfig().withBackendName(CmsTestSupport.BACKEND);
|
||||
QInstance qInstance = install(config);
|
||||
CmsTestSupport.session(qInstance, CmsSecurity.permissionsFor(config, Set.of(CmsRole.ADMIN)));
|
||||
|
||||
Integer siteId = insert("cms_site", new QRecord().withValue("name", "Example").withValue("shortName", "ex"));
|
||||
insert("cms_page", new QRecord().withValue("name", "Home").withValue("slug", "home").withValue("title", "Welcome <home>")
|
||||
.withValue("pageTypeId", 1).withValue("siteId", siteId).withValue("isPublished", true));
|
||||
insert("cms_page", new QRecord().withValue("name", "Draft").withValue("slug", "draft").withValue("title", "Work in progress")
|
||||
.withValue("pageTypeId", 1).withValue("siteId", siteId));
|
||||
insert("cms_snippet", new QRecord().withValue("name", "Footer").withValue("shortName", "footer").withValue("siteId", siteId).withValue("isActive", false));
|
||||
|
||||
String top = render(qInstance, "cmsHome");
|
||||
assertThat(top).contains("2 pages", "1 unpublished", "1 site");
|
||||
|
||||
String pages = render(qInstance, "cmsPagesHome");
|
||||
assertThat(pages).contains("Welcome <home>", "Work in progress", "/app/cms_page/", "Unpublished pages", "Inactive snippets", "Recently changed", "New page");
|
||||
assertThat(pages).doesNotContain("Welcome <home>");
|
||||
|
||||
CmsTestSupport.session(qInstance, CmsSecurity.permissionsFor(config, Set.of(CmsRole.VIEWER)));
|
||||
assertThat(render(qInstance, "cmsPagesHome")).contains("Work in progress").doesNotContain("New page");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A user who may read nothing sees that, not zeros; DEGRADED shows the
|
||||
** schema problem.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testNoAccessAndDegraded() throws Exception
|
||||
{
|
||||
CmsQBitConfig config = new CmsQBitConfig().withBackendName(CmsTestSupport.BACKEND);
|
||||
QInstance qInstance = install(config);
|
||||
CmsTestSupport.session(qInstance, Set.of());
|
||||
assertThat(render(qInstance, "cmsHome")).contains("do not have access");
|
||||
assertThat(render(qInstance, "cmsNavigationHome")).contains("do not have access to Navigation");
|
||||
|
||||
RDBMSBackendMetaData backend = CmsTestSupport.h2Backend();
|
||||
QInstance unmigrated = CmsTestSupport.blankInstance(backend);
|
||||
CmsQBitConfig degraded = new CmsQBitConfig().withBackendName(CmsTestSupport.BACKEND).withStartupMode(CmsStartupMode.DEGRADED);
|
||||
new CmsQBitProducer().withQBitConfig(degraded).produce(unmigrated);
|
||||
CmsTestSupport.session(unmigrated, Set.of());
|
||||
assertThat(render(unmigrated, "cmsHome")).contains("not up to date");
|
||||
|
||||
QWidgetMetaData orphan = new QWidgetMetaData().withName("x").withDefaultValue(CmsHomeRenderer.KEY_NAME_PREFIX, "nope");
|
||||
assertThatThrownBy(() -> new CmsHomeRenderer().render(new RenderWidgetInput().withWidgetMetaData(orphan))).hasMessageContaining("No CMS install");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Pluralizing labels.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testPlural()
|
||||
{
|
||||
assertThat(CmsHomeRenderer.plural(1, "Page")).isEqualTo("1 page");
|
||||
assertThat(CmsHomeRenderer.plural(2, "Page Type")).isEqualTo("2 page types");
|
||||
assertThat(CmsTableDefinition.pluralize("Category")).isEqualTo("Categories");
|
||||
assertThat(CmsTableDefinition.pluralize("Key")).isEqualTo("Keys");
|
||||
assertThat(CmsHtml.escape(null)).isEmpty();
|
||||
assertThat(CmsHtml.escape("<a href='x'>&\"")).isEqualTo("<a href='x'>&"");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static QInstance install(CmsQBitConfig config) throws Exception
|
||||
{
|
||||
RDBMSBackendMetaData backend = CmsTestSupport.h2Backend();
|
||||
CmsTestSupport.migrate(backend, Map.of());
|
||||
QInstance qInstance = CmsTestSupport.blankInstance(backend);
|
||||
qInstance.setEnforcePermissionsInCoreActions(true);
|
||||
new CmsQBitProducer().withQBitConfig(config).produce(qInstance);
|
||||
return (qInstance);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static String render(QInstance qInstance, String widgetName) throws QException
|
||||
{
|
||||
RawHTML html = (RawHTML) new RenderWidgetAction().execute(new RenderWidgetInput().withWidgetMetaData(qInstance.getWidget(widgetName))).getWidgetData();
|
||||
return (html.getHtml());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static Integer insert(String table, QRecord record) throws QException
|
||||
{
|
||||
return (new InsertAction().execute(new InsertInput(table).withRecord(record)).getRecords().get(0).getValueInteger("id"));
|
||||
}
|
||||
}
|
||||