@revealui/core 0.5.2 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/client/admin/layout.js +1 -1
- package/dist/collections/operations/sqlAdapter.d.ts.map +1 -1
- package/dist/collections/operations/sqlAdapter.js +37 -5
- package/dist/globals/GlobalOperations.d.ts.map +1 -1
- package/dist/globals/GlobalOperations.js +11 -7
- package/dist/queries/queryBuilder.d.ts.map +1 -1
- package/dist/queries/queryBuilder.js +17 -4
- package/package.json +7 -6
package/README.md
CHANGED
|
@@ -145,6 +145,20 @@ pnpm test
|
|
|
145
145
|
pnpm dev
|
|
146
146
|
```
|
|
147
147
|
|
|
148
|
+
## When to Use This
|
|
149
|
+
|
|
150
|
+
- You're building a content-driven app and need collections, admin UI, and CRUD out of the box
|
|
151
|
+
- You need RBAC/ABAC access control, GDPR compliance, or feature gating by license tier
|
|
152
|
+
- You want a rich text editor (Lexical) integrated with your CMS
|
|
153
|
+
- **Not** for standalone UI components — use `@revealui/presentation`
|
|
154
|
+
- **Not** for raw database queries — use `@revealui/db` directly
|
|
155
|
+
|
|
156
|
+
## JOSHUA Alignment
|
|
157
|
+
|
|
158
|
+
- **Sovereign**: Self-hosted CMS engine — no SaaS dependency for content management, auth, or storage
|
|
159
|
+
- **Unified**: One `buildConfig()` call wires collections, globals, plugins, security, and feature gates into a single configuration
|
|
160
|
+
- **Adaptive**: Plugin system and tier-based feature gating let the platform evolve without breaking existing deployments
|
|
161
|
+
|
|
148
162
|
## Related
|
|
149
163
|
|
|
150
164
|
- [Contracts Package](../contracts/README.md) — Zod schemas and TypeScript types
|
|
@@ -3,5 +3,5 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import Head from 'next/head';
|
|
4
4
|
import { ServerFunctionProvider } from './context/ServerFunctionContext.js';
|
|
5
5
|
export function RootLayout({ children, serverFunction }) {
|
|
6
|
-
return (_jsxs("html", { lang: "en", children: [_jsxs(Head, { children: [_jsx("meta", { charSet: "utf-8" }), _jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }), _jsx("title", { children: "RevealUI Admin" })] }), _jsx("body", { className: "antialiased", children: _jsx(ServerFunctionProvider, { serverFunction: serverFunction, children: _jsx("
|
|
6
|
+
return (_jsxs("html", { lang: "en", children: [_jsxs(Head, { children: [_jsx("meta", { charSet: "utf-8" }), _jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }), _jsx("title", { children: "RevealUI Admin" })] }), _jsx("body", { className: "antialiased", children: _jsx(ServerFunctionProvider, { serverFunction: serverFunction, children: _jsx("main", { id: "revealui-admin", className: "min-h-screen", children: children }) }) })] }));
|
|
7
7
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sqlAdapter.d.ts","sourceRoot":"","sources":["../../../src/collections/operations/sqlAdapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D,MAAM,MAAM,qBAAqB,GAAG;IAClC,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;CACvE,CAAC;
|
|
1
|
+
{"version":3,"file":"sqlAdapter.d.ts","sourceRoot":"","sources":["../../../src/collections/operations/sqlAdapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D,MAAM,MAAM,qBAAqB,GAAG;IAClC,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;CACvE,CAAC;AA6BF,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAM/C;AAkBD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAMvD;AAED,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAG1D;AAED,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAIpF;AAED,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,aAAa,EAAE,MAAM,EACrB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,GAClB,MAAM,CAER;AAED,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,CAKjF;AAED,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE9D;AAED,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE/D;AAED,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAI1E;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAKrF"}
|
|
@@ -9,21 +9,53 @@
|
|
|
9
9
|
* redesign the collection storage layer toward typed tables instead.
|
|
10
10
|
*/
|
|
11
11
|
/** Only lowercase alphanumeric, hyphens, and underscores (1-63 chars, PostgreSQL identifier limit). */
|
|
12
|
-
|
|
12
|
+
function isValidSlug(s) {
|
|
13
|
+
if (s.length < 1 || s.length > 63)
|
|
14
|
+
return false;
|
|
15
|
+
// First char must be lowercase letter
|
|
16
|
+
const first = s.charCodeAt(0);
|
|
17
|
+
if (first < 97 || first > 122)
|
|
18
|
+
return false;
|
|
19
|
+
for (let i = 1; i < s.length; i++) {
|
|
20
|
+
const c = s.charCodeAt(i);
|
|
21
|
+
const isLower = c >= 97 && c <= 122;
|
|
22
|
+
const isDigit = c >= 48 && c <= 57;
|
|
23
|
+
// underscore = 95, hyphen = 45
|
|
24
|
+
if (!(isLower || isDigit || c === 95 || c === 45))
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
13
29
|
export function validateSlug(slug) {
|
|
14
|
-
if (!
|
|
30
|
+
if (!isValidSlug(slug)) {
|
|
15
31
|
throw new Error(`Invalid collection slug: "${slug}". Slugs must start with a lowercase letter and contain only lowercase alphanumeric characters, hyphens, and underscores (max 63 chars).`);
|
|
16
32
|
}
|
|
17
33
|
}
|
|
18
34
|
/** Only lowercase alphanumeric and underscores (PostgreSQL column name safe). */
|
|
19
|
-
|
|
35
|
+
function isValidColumnName(s) {
|
|
36
|
+
if (s.length < 1 || s.length > 63)
|
|
37
|
+
return false;
|
|
38
|
+
// First char must be lowercase letter or underscore
|
|
39
|
+
const first = s.charCodeAt(0);
|
|
40
|
+
const firstIsLower = first >= 97 && first <= 122;
|
|
41
|
+
if (!(firstIsLower || first === 95))
|
|
42
|
+
return false;
|
|
43
|
+
for (let i = 1; i < s.length; i++) {
|
|
44
|
+
const c = s.charCodeAt(i);
|
|
45
|
+
const isLower = c >= 97 && c <= 122;
|
|
46
|
+
const isDigit = c >= 48 && c <= 57;
|
|
47
|
+
if (!(isLower || isDigit || c === 95))
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
20
52
|
export function validateColumnName(column) {
|
|
21
|
-
if (!
|
|
53
|
+
if (!isValidColumnName(column)) {
|
|
22
54
|
throw new Error(`Invalid column name: "${column}". Column names must start with a lowercase letter or underscore and contain only lowercase alphanumeric characters and underscores.`);
|
|
23
55
|
}
|
|
24
56
|
}
|
|
25
57
|
export function escapeIdentifier(identifier) {
|
|
26
|
-
return identifier.
|
|
58
|
+
return identifier.split('"').join('""');
|
|
27
59
|
}
|
|
28
60
|
export function collectionTable(configSlug) {
|
|
29
61
|
validateSlug(configSlug);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GlobalOperations.d.ts","sourceRoot":"","sources":["../../src/globals/GlobalOperations.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"GlobalOperations.d.ts","sourceRoot":"","sources":["../../src/globals/GlobalOperations.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EACV,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,aAAa,EAEd,MAAM,mBAAmB,CAAC;AAG3B;;;;GAIG;AACH,qBAAa,cAAc;IACzB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,EAAE,EAAE;QACF,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;KACvE,GAAG,IAAI,CAAC;gBAGP,MAAM,EAAE,kBAAkB,EAC1B,EAAE,EAAE;QACF,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;KACvE,GAAG,IAAI;IAMJ,IAAI,CACR,OAAO,GAAE;QACP,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,QAAQ,CAAC,EAAE,OAAO,mBAAmB,EAAE,YAAY,CAAC;QACpD,GAAG,CAAC,EAAE,aAAa,CAAC;KAChB,GACL,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAwG3B,MAAM,CAAC,OAAO,EAAE;QAAE,IAAI,EAAE,OAAO,CAAC,cAAc,CAAC,CAAA;KAAE,GAAG,OAAO,CAAC,cAAc,CAAC;CAqElF"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { validateColumnName, validateSlug } from '../collections/operations/sqlAdapter.js';
|
|
1
|
+
import { escapeIdentifier, validateColumnName, validateSlug, } from '../collections/operations/sqlAdapter.js';
|
|
2
2
|
import { afterRead } from '../fields/hooks/afterRead/index.js';
|
|
3
3
|
import { getRelationshipFields } from '../relationships/analyzer.js';
|
|
4
4
|
import { flattenResult } from '../utils/flattenResult.js';
|
|
@@ -22,7 +22,10 @@ export class RevealUIGlobal {
|
|
|
22
22
|
}
|
|
23
23
|
if (this.db?.query) {
|
|
24
24
|
const slug = this.config.slug;
|
|
25
|
-
|
|
25
|
+
try {
|
|
26
|
+
validateSlug(slug);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
26
29
|
throw new Error(`Invalid global slug: "${slug}". Must be lowercase alphanumeric with hyphens/underscores.`);
|
|
27
30
|
}
|
|
28
31
|
const tableName = `global_${slug}`;
|
|
@@ -101,7 +104,10 @@ export class RevealUIGlobal {
|
|
|
101
104
|
const { data } = options;
|
|
102
105
|
if (this.db?.query) {
|
|
103
106
|
const slug = this.config.slug;
|
|
104
|
-
|
|
107
|
+
try {
|
|
108
|
+
validateSlug(slug);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
105
111
|
throw new Error(`Invalid global slug: "${slug}". Must be lowercase alphanumeric with hyphens/underscores.`);
|
|
106
112
|
}
|
|
107
113
|
const tableName = `global_${slug}`;
|
|
@@ -114,9 +120,7 @@ export class RevealUIGlobal {
|
|
|
114
120
|
const keys = Object.keys(data);
|
|
115
121
|
for (const key of keys)
|
|
116
122
|
validateColumnName(key);
|
|
117
|
-
const setClause = keys
|
|
118
|
-
.map((key, i) => `"${key.replace(/"/g, '""')}" = $${i + 1}`)
|
|
119
|
-
.join(', ');
|
|
123
|
+
const setClause = keys.map((key, i) => `"${escapeIdentifier(key)}" = $${i + 1}`).join(', ');
|
|
120
124
|
const values = keys.map((key) => {
|
|
121
125
|
const value = data[key];
|
|
122
126
|
// Serialize non-primitive values to JSON strings for SQLite compatibility
|
|
@@ -147,7 +151,7 @@ export class RevealUIGlobal {
|
|
|
147
151
|
}
|
|
148
152
|
return value;
|
|
149
153
|
});
|
|
150
|
-
const query = `INSERT INTO "${tableName}" (id, ${columns.map((c) => `"${c
|
|
154
|
+
const query = `INSERT INTO "${tableName}" (id, ${columns.map((c) => `"${escapeIdentifier(c)}"`).join(', ')}) VALUES ($1, ${placeholders})`;
|
|
151
155
|
await this.db.query(query, [id, ...values]);
|
|
152
156
|
}
|
|
153
157
|
const updatedDoc = await this.find();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"queryBuilder.d.ts","sourceRoot":"","sources":["../../src/queries/queryBuilder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAExE;;;;;GAKG;AAEH,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"queryBuilder.d.ts","sourceRoot":"","sources":["../../src/queries/queryBuilder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAExE;;;;;GAKG;AAEH,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG,YAAY,CAAC;AAevD;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC;;;OAGG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,WAAW,GAAG,iBAAiB,CAAC,OAAO,CAAC,GAAG,SAAS,EAC3D,MAAM,EAAE,OAAO,EAAE,EACjB,OAAO,GAAE,iBAAsB,GAC9B,MAAM,CA0NR;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,CAAC,EAAE,WAAW,GAAG,OAAO,EAAE,CAoEjE"}
|
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
/** Escape SQL LIKE wildcards (%, _, \) in user input */
|
|
2
|
+
function escapeLikeWildcards(value) {
|
|
3
|
+
let result = '';
|
|
4
|
+
for (const ch of value) {
|
|
5
|
+
if (ch === '%' || ch === '_' || ch === '\\') {
|
|
6
|
+
result += `\\${ch}`;
|
|
7
|
+
}
|
|
8
|
+
else {
|
|
9
|
+
result += ch;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return result;
|
|
13
|
+
}
|
|
1
14
|
/**
|
|
2
15
|
* Builds a WHERE clause from a RevealWhere query object.
|
|
3
16
|
* Supports nested AND/OR conditions and various operators.
|
|
@@ -27,7 +40,7 @@ export function buildWhereClause(where, params, options = {}) {
|
|
|
27
40
|
if (!quoteFields)
|
|
28
41
|
return field;
|
|
29
42
|
// Escape embedded double quotes to prevent SQL injection via identifier breakout
|
|
30
|
-
const escaped = field.
|
|
43
|
+
const escaped = field.split('"').join('""');
|
|
31
44
|
return `"${escaped}"`;
|
|
32
45
|
};
|
|
33
46
|
const whereWithGroups = where;
|
|
@@ -149,7 +162,7 @@ export function buildWhereClause(where, params, options = {}) {
|
|
|
149
162
|
if ('contains' in condition && typeof condition.contains === 'string') {
|
|
150
163
|
const placeholder = getPlaceholder();
|
|
151
164
|
// Escape LIKE wildcards (% and _) in user input to prevent wildcard injection
|
|
152
|
-
const escaped = condition.contains
|
|
165
|
+
const escaped = escapeLikeWildcards(condition.contains);
|
|
153
166
|
params.push(`%${escaped}%`);
|
|
154
167
|
conditions.push(`${quotedField} LIKE ${placeholder} ESCAPE '\\'`);
|
|
155
168
|
}
|
|
@@ -168,7 +181,7 @@ export function buildWhereClause(where, params, options = {}) {
|
|
|
168
181
|
// like (escape wildcards to prevent blind LIKE probing)
|
|
169
182
|
if ('like' in condition && typeof condition.like === 'string') {
|
|
170
183
|
const placeholder = getPlaceholder();
|
|
171
|
-
const escaped = condition.like
|
|
184
|
+
const escaped = escapeLikeWildcards(condition.like);
|
|
172
185
|
params.push(escaped);
|
|
173
186
|
conditions.push(`${quotedField} LIKE ${placeholder} ESCAPE '\\'`);
|
|
174
187
|
}
|
|
@@ -219,7 +232,7 @@ export function extractWhereValues(where) {
|
|
|
219
232
|
break;
|
|
220
233
|
case 'contains':
|
|
221
234
|
if (typeof value === 'string') {
|
|
222
|
-
const escaped = value
|
|
235
|
+
const escaped = escapeLikeWildcards(value);
|
|
223
236
|
values.push(`%${escaped}%`);
|
|
224
237
|
}
|
|
225
238
|
break;
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@revealui/core",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
|
+
"description": "CMS engine, REST API, auth, rich text, admin UI, and plugins for RevealUI",
|
|
4
5
|
"license": "MIT",
|
|
5
6
|
"dependencies": {
|
|
6
7
|
"@electric-sql/pglite": "^0.4.2",
|
|
@@ -23,11 +24,11 @@
|
|
|
23
24
|
"pg": "^8.18.0",
|
|
24
25
|
"yjs": "^13.6.29",
|
|
25
26
|
"zod": "^4.3.6",
|
|
26
|
-
"@revealui/
|
|
27
|
-
"@revealui/
|
|
28
|
-
"@revealui/
|
|
29
|
-
"@revealui/
|
|
30
|
-
"@revealui/
|
|
27
|
+
"@revealui/cache": "0.1.1",
|
|
28
|
+
"@revealui/contracts": "1.3.4",
|
|
29
|
+
"@revealui/resilience": "0.2.1",
|
|
30
|
+
"@revealui/security": "0.2.4",
|
|
31
|
+
"@revealui/utils": "0.3.1"
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|
|
33
34
|
"@types/json-schema": "^7.0.15",
|