rimecms 0.23.27 → 0.24.1
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 +32 -19
- package/dist/adapter-sqlite/where.js +202 -154
- package/dist/core/config/shared/find-title.d.ts +2 -2
- package/dist/core/config/shared/find-title.js +1 -1
- package/dist/core/operations/hooks/before-read/set-document-title.server.js +15 -2
- package/dist/fields/relation/component/Cell.svelte +10 -4
- package/dist/fields/relation/component/default/Default.svelte +1 -1
- package/dist/fields/rich-text/index.d.ts +2 -0
- package/dist/fields/rich-text/index.js +4 -0
- package/dist/panel/components/sections/collection/list/row/Row.svelte +2 -0
- package/dist/panel/components/ui/breadcrumb/BreadCrumb.svelte +4 -0
- package/dist/panel/components/ui/page-header/PageHeader.svelte +4 -0
- package/dist/panel/context/documentForm.svelte.js +13 -1
- package/dist/site/components/avatar/avatar.svelte +1 -1
- package/dist/site/components/avatar/avatar.svelte.d.ts +1 -1
- package/dist/site/components/button/button.svelte +2 -2
- package/dist/site/components/card-document/card-document.svelte +92 -0
- package/dist/site/components/card-document/card-document.svelte.d.ts +7 -0
- package/dist/site/components/dialog/dialog-content.css +47 -0
- package/dist/site/components/dialog/dialog-content.svelte +28 -0
- package/dist/site/components/dialog/dialog-content.svelte.d.ts +10 -0
- package/dist/site/components/dialog/dialog-overlay.css +8 -0
- package/dist/site/components/dialog/dialog-overlay.svelte +12 -0
- package/dist/site/components/dialog/dialog-overlay.svelte.d.ts +5 -0
- package/dist/site/components/dialog/index.d.ts +8 -0
- package/dist/site/components/dialog/index.js +10 -0
- package/dist/site/components/feed/feed.svelte +12 -9
- package/dist/site/components/feed/feed.svelte.d.ts +5 -17
- package/dist/site/components/folder/folder.svelte +53 -0
- package/dist/site/components/folder/folder.svelte.d.ts +7 -0
- package/dist/site/components/nav/nav.svelte +15 -11
- package/dist/site/components/nav/post-button/post-button.svelte +52 -0
- package/dist/site/components/nav/post-button/post-button.svelte.d.ts +3 -0
- package/dist/site/components/post/post-actions/comment-button.svelte +115 -0
- package/dist/site/components/post/post-actions/comment-button.svelte.d.ts +6 -0
- package/dist/site/components/post/post-actions/like-button.svelte +76 -0
- package/dist/site/components/post/post-actions/like-button.svelte.d.ts +6 -0
- package/dist/site/components/post/post-actions/post-actions.svelte +21 -11
- package/dist/site/components/post/post-actions/post-actions.svelte.d.ts +1 -1
- package/dist/site/components/post/post-attachments.svelte +26 -0
- package/dist/site/components/post/post-attachments.svelte.d.ts +6 -0
- package/dist/site/components/post/post-date.svelte +12 -0
- package/dist/site/components/post/post-date.svelte.d.ts +6 -0
- package/dist/site/components/post/post-with-comments.svelte +42 -0
- package/dist/site/components/post/post-with-comments.svelte.d.ts +6 -0
- package/dist/site/components/post/post.svelte +28 -11
- package/dist/site/components/post/post.svelte.d.ts +1 -2
- package/dist/site/components/rich-text/bubble-menu/bubble-menu.css +17 -0
- package/dist/site/components/rich-text/bubble-menu/bubble-menu.svelte +119 -0
- package/dist/site/components/rich-text/bubble-menu/bubble-menu.svelte.d.ts +11 -0
- package/dist/site/components/rich-text/bubble-menu/icon-button/icon-button.css +15 -0
- package/dist/site/components/rich-text/bubble-menu/icon-button/icon-button.svelte +31 -0
- package/dist/site/components/rich-text/bubble-menu/icon-button/icon-button.svelte.d.ts +13 -0
- package/dist/site/components/rich-text/context.svelte.d.ts +12 -0
- package/dist/site/components/rich-text/context.svelte.js +32 -0
- package/dist/site/components/rich-text/features/bold.d.ts +2 -0
- package/dist/site/components/rich-text/features/bold.js +21 -0
- package/dist/site/components/rich-text/features/link/component/link-selector.css +4 -0
- package/dist/site/components/rich-text/features/link/component/link-selector.svelte +214 -0
- package/dist/site/components/rich-text/features/link/component/link-selector.svelte.d.ts +11 -0
- package/dist/site/components/rich-text/features/link/link.d.ts +2 -0
- package/dist/site/components/rich-text/features/link/link.js +36 -0
- package/dist/site/components/rich-text/rich-text.css +170 -0
- package/dist/site/components/rich-text/rich-text.svelte +84 -0
- package/dist/site/components/rich-text/rich-text.svelte.d.ts +9 -0
- package/dist/site/components/upload-thumbnail/upload-thumbnail.svelte +76 -0
- package/dist/site/components/upload-thumbnail/upload-thumbnail.svelte.d.ts +8 -0
- package/dist/site/components/upload-thumbnail/util.d.ts +1 -0
- package/dist/site/components/upload-thumbnail/util.js +13 -0
- package/dist/site/components/user-inline/user-inline.svelte +1 -1
- package/dist/site/components/user-inline/user-inline.svelte.d.ts +1 -1
- package/dist/site/styles/colors.css +2 -0
- package/dist/site/styles/fonts.css +160 -0
- package/dist/site/styles/fonts.mixins.css +5 -0
- package/dist/site/styles/main.css +8 -6
- package/dist/site/styles/shadows.css +10 -0
- package/dist/site/styles/sizes.css +43 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,6 +31,7 @@ Headless CMS powered by SvelteKit.
|
|
|
31
31
|
### Content Management
|
|
32
32
|
|
|
33
33
|
Fields types:
|
|
34
|
+
|
|
34
35
|
- Blocks
|
|
35
36
|
- Tree (nested array)
|
|
36
37
|
- Tabs
|
|
@@ -52,6 +53,7 @@ Fields types:
|
|
|
52
53
|
npx sv create my-app
|
|
53
54
|
cd my-app
|
|
54
55
|
```
|
|
56
|
+
|
|
55
57
|
> [!NOTE]
|
|
56
58
|
> Select TypeScript when prompted
|
|
57
59
|
|
|
@@ -83,7 +85,7 @@ import { sveltekit } from '@sveltejs/kit/vite';
|
|
|
83
85
|
import { rime } from 'rimecms/vite';
|
|
84
86
|
|
|
85
87
|
export default defineConfig({
|
|
86
|
-
|
|
88
|
+
plugins: [rime(), sveltekit()]
|
|
87
89
|
});
|
|
88
90
|
```
|
|
89
91
|
|
|
@@ -198,29 +200,37 @@ export default rime({
|
|
|
198
200
|
|
|
199
201
|
```ts
|
|
200
202
|
export const load = async (event: LayoutServerLoadEvent) => {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
203
|
+
const { rime } = event.locals;
|
|
204
|
+
// Get an Area document
|
|
205
|
+
const menu = await rime.area('menu').find();
|
|
206
|
+
// Get all pages documents
|
|
207
|
+
const pages = await rime.collection('pages').findAll({ locale: 'en' });
|
|
208
|
+
// Get a page byId
|
|
209
|
+
const home = await rime.collection('pages').findById({ locale: 'en', id: 'some-id' });
|
|
210
|
+
// Get a user with a query
|
|
211
|
+
const [user] = await rime.collection('users').find({
|
|
212
|
+
query: `where[email][equals]=some@email.com` // qs query or ParsedQsQuery
|
|
213
|
+
});
|
|
214
|
+
// Get some config values
|
|
215
|
+
const languages = rime.config.getLocalesCodes();
|
|
216
|
+
const collections = rime.config.collections;
|
|
217
|
+
//...
|
|
216
218
|
};
|
|
217
219
|
```
|
|
218
220
|
|
|
219
221
|
### From the API :
|
|
222
|
+
|
|
220
223
|
```ts
|
|
221
|
-
const { docs } = await fetch('http://localhost:5173/api/pages').then(r => r.json())
|
|
222
|
-
const { docs } = await fetch('http://localhost:5173/api/pages?sort=title&limit=1').then(r =>
|
|
223
|
-
|
|
224
|
+
const { docs } = await fetch('http://localhost:5173/api/pages').then((r) => r.json());
|
|
225
|
+
const { docs } = await fetch('http://localhost:5173/api/pages?sort=title&limit=1').then((r) =>
|
|
226
|
+
r.json()
|
|
227
|
+
);
|
|
228
|
+
const { docs } = await fetch(
|
|
229
|
+
'http://localhost:5173/api/pages?where[author][equals]=some-id&locale=en`;'
|
|
230
|
+
).then((r) => r.json());
|
|
231
|
+
const { docs } = await fetch(
|
|
232
|
+
'http://localhost:5173/api/pages?where[author.email][equals]=some@email.com&locale=en`;'
|
|
233
|
+
).then((r) => r.json());
|
|
224
234
|
```
|
|
225
235
|
|
|
226
236
|
## DEPLOYING
|
|
@@ -228,10 +238,12 @@ const { docs } = await fetch('http://localhost:5173/api/pages?where[author][like
|
|
|
228
238
|
For now I am using it with @svelte/adapter-node, other adapter not tested and probably not working.
|
|
229
239
|
|
|
230
240
|
With the node adapter :
|
|
241
|
+
|
|
231
242
|
```sh
|
|
232
243
|
npx rime build
|
|
233
244
|
npx rime build -d # to copy the database directory
|
|
234
245
|
```
|
|
246
|
+
|
|
235
247
|
It's doing bascically `vite build` under the hood and create the polka server file inside an app directory, plus giving some info on how to run it.
|
|
236
248
|
|
|
237
249
|
## ROADMAP
|
|
@@ -246,6 +258,7 @@ It's doing bascically `vite build` under the hood and create the polka server fi
|
|
|
246
258
|
- [x] Document version
|
|
247
259
|
- [x] collection nested
|
|
248
260
|
- [x] more better-auth integration
|
|
261
|
+
- [x] Handle relation poperties in queries
|
|
249
262
|
- [~] Documentation
|
|
250
263
|
- [ ] Live Edit system in practice
|
|
251
264
|
- [ ] auto-saved draft
|
|
@@ -7,61 +7,40 @@ import {} from '../index.js';
|
|
|
7
7
|
import * as drizzleORM from 'drizzle-orm';
|
|
8
8
|
import { and, eq, getTableColumns, inArray, or } from 'drizzle-orm';
|
|
9
9
|
export const buildWhereParam = ({ query, slug, db, locale, tables, configCtx }) => {
|
|
10
|
+
// Helper to get table by key with correct typing
|
|
10
11
|
function getTable(key) {
|
|
11
12
|
return tables[key];
|
|
12
13
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
function getTablesAndColumns(slug) {
|
|
15
|
+
// Get main table and localized table if applicable
|
|
16
|
+
const table = getTable(slug);
|
|
17
|
+
const tableNameLocales = withLocalesSuffix(slug);
|
|
18
|
+
const tableLocales = getTable(tableNameLocales);
|
|
19
|
+
// Get localized and unlocalized columns
|
|
20
|
+
const localizedColumns = locale && tableNameLocales in tables ? Object.keys(getTableColumns(tableLocales)) : [];
|
|
21
|
+
const unlocalizedColumns = Object.keys(getTableColumns(table));
|
|
22
|
+
return { table, tableLocales, localizedColumns, unlocalizedColumns };
|
|
23
|
+
}
|
|
24
|
+
const {
|
|
25
|
+
// Get main table and localized table if applicable
|
|
26
|
+
table, tableLocales, localizedColumns, unlocalizedColumns } = getTablesAndColumns(slug);
|
|
18
27
|
const buildCondition = (conditionObject) => {
|
|
19
28
|
// Handle nested AND conditions
|
|
20
29
|
if ('and' in conditionObject && Array.isArray(conditionObject.and)) {
|
|
21
|
-
const subConditions = conditionObject.and
|
|
22
|
-
.map((condition) => buildCondition(condition))
|
|
23
|
-
.filter(Boolean);
|
|
30
|
+
const subConditions = conditionObject.and.map((condition) => buildCondition(condition)).filter(Boolean);
|
|
24
31
|
return subConditions.length ? and(...subConditions) : false;
|
|
25
32
|
}
|
|
26
33
|
// Handle nested OR conditions
|
|
27
34
|
if ('or' in conditionObject && Array.isArray(conditionObject.or)) {
|
|
28
|
-
const subConditions = conditionObject.or
|
|
29
|
-
.map((condition) => buildCondition(condition))
|
|
30
|
-
.filter(Boolean);
|
|
35
|
+
const subConditions = conditionObject.or.map((condition) => buildCondition(condition)).filter(Boolean);
|
|
31
36
|
return subConditions.length ? or(...subConditions) : false;
|
|
32
37
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const idOperator = conditionObject.id;
|
|
38
|
-
delete conditionObject.id;
|
|
39
|
-
conditionObject.ownerId = idOperator;
|
|
40
|
-
}
|
|
41
|
-
// Handle versionId field for versioned collections
|
|
42
|
-
// if "versionId" inside the query it should refer to the id of the version table (current)
|
|
43
|
-
if (hasVersionsSuffix(slug) && 'versionId' in conditionObject) {
|
|
44
|
-
// Replace id with ownerId and keep the same operator and value
|
|
45
|
-
const idOperator = conditionObject.versionId;
|
|
46
|
-
delete conditionObject.versionId;
|
|
47
|
-
conditionObject.id = idOperator;
|
|
48
|
-
}
|
|
49
|
-
// Handle regular field conditions
|
|
50
|
-
const [column, operatorObj] = Object.entries(conditionObject)[0];
|
|
51
|
-
const [operator, rawValue] = Object.entries(operatorObj)[0];
|
|
52
|
-
if (!isOperator(operator)) {
|
|
53
|
-
throw new RimeError(RimeError.INVALID_DATA, operator + 'is not supported');
|
|
54
|
-
}
|
|
55
|
-
// get the correct Drizzle operator
|
|
56
|
-
const fn = operators[operator];
|
|
57
|
-
// Format compared value to support Date, Arrays,...
|
|
58
|
-
const value = formatValue({ operator, value: rawValue });
|
|
59
|
-
// Convert dot notation to double underscore
|
|
60
|
-
// for fields included in groups or tabs
|
|
61
|
-
const sqlColumn = column.replace(/\./g, '__');
|
|
38
|
+
conditionObject = normalizedForVersion(conditionObject, slug);
|
|
39
|
+
const {
|
|
40
|
+
// Get condition members
|
|
41
|
+
column, sqlColumn, fn, operator, rawValue, value } = getConditionMembers(conditionObject);
|
|
62
42
|
// Handle hierarchy fields (_parent, _position) in versioned collections
|
|
63
|
-
if (
|
|
64
|
-
(sqlColumn === '_parent' || sqlColumn === '_position' || sqlColumn === '_path')) {
|
|
43
|
+
if (shouldHandleVersionedHierarchyFields(slug, sqlColumn)) {
|
|
65
44
|
// Get the root table name by removing the '_versions' suffix
|
|
66
45
|
const rootSlug = slug.replace('_versions', '');
|
|
67
46
|
const rootTable = getTable(rootSlug);
|
|
@@ -82,154 +61,182 @@ export const buildWhereParam = ({ query, slug, db, locale, tables, configCtx })
|
|
|
82
61
|
// Look for a relation field
|
|
83
62
|
// Get document config
|
|
84
63
|
const documentConfig = configCtx.getBySlug(slug);
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
//
|
|
89
|
-
|
|
64
|
+
// Get the compiled fields for lookups
|
|
65
|
+
const compiledFields = documentConfig.fields.map((f) => f.compile());
|
|
66
|
+
let fieldConfig = getFieldConfigByPath(column, compiledFields);
|
|
67
|
+
// Track relation-property detection (e.g. attributes.author.name)
|
|
68
|
+
let matchedPrefix = column;
|
|
69
|
+
let relationPropertyPath = null;
|
|
70
|
+
let isRelationProperty = false;
|
|
71
|
+
// If the exact path isn't found, try progressively shorter prefixes to support
|
|
72
|
+
// relation property queries like `attributes.author.name` -> `attributes.author`
|
|
90
73
|
if (!fieldConfig) {
|
|
91
|
-
|
|
92
|
-
|
|
74
|
+
const parts = column.split('.');
|
|
75
|
+
for (let i = parts.length - 1; i > 0; i--) {
|
|
76
|
+
const prefix = parts.slice(0, i).join('.');
|
|
77
|
+
const candidate = getFieldConfigByPath(prefix, compiledFields);
|
|
78
|
+
if (candidate) {
|
|
79
|
+
fieldConfig = candidate;
|
|
80
|
+
// If prefix shorter than original column, record the property suffix
|
|
81
|
+
if (i < parts.length) {
|
|
82
|
+
matchedPrefix = prefix;
|
|
83
|
+
relationPropertyPath = parts.slice(i).join('.');
|
|
84
|
+
isRelationProperty = true;
|
|
85
|
+
}
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// If still not found or not a relation field, log warning and return false condition
|
|
91
|
+
if (!fieldConfig || !isRelationField(fieldConfig)) {
|
|
92
|
+
const message = `the query contains the field "${column}", not found for ${documentConfig.slug} document`;
|
|
93
|
+
logger.warn(message);
|
|
93
94
|
// Return a condition that will always be false instead of returning false
|
|
94
95
|
// This ensures no documents match when a non-existent field is queried
|
|
95
|
-
return eq(table.id, '-1');
|
|
96
|
+
return eq(table.id, '-1');
|
|
96
97
|
}
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
98
|
+
// Helper: normalize various input forms (rawValue, CSV string, or formatted array)
|
|
99
|
+
// into a unique array of values used for multi-valued relation comparisons.
|
|
100
|
+
const normalizeValues = (raw, formatted) => {
|
|
101
|
+
let out;
|
|
102
|
+
if (Array.isArray(raw))
|
|
103
|
+
out = raw;
|
|
104
|
+
else if (typeof raw === 'string' && raw.includes(','))
|
|
105
|
+
out = raw.split(',');
|
|
106
|
+
else if (Array.isArray(formatted))
|
|
107
|
+
out = formatted;
|
|
108
|
+
else
|
|
109
|
+
out = [formatted];
|
|
110
|
+
return Array.from(new Set(out));
|
|
111
|
+
};
|
|
112
|
+
// Handle relation property queries (e.g., attributes.author.name)
|
|
113
|
+
// by building a subquery on the related collection
|
|
114
|
+
const buildRelationPropertyCondition = () => {
|
|
115
|
+
const relatedSlug = fieldConfig.relationTo;
|
|
116
|
+
const relatedTable = getTable(relatedSlug);
|
|
117
|
+
// Build a where clause for the related collection using the same operator/value
|
|
118
|
+
const relatedWhere = { [relationPropertyPath]: { [operator]: rawValue } };
|
|
119
|
+
// Recursive call to buildWhereParam for the related collection
|
|
120
|
+
const relatedCondition = buildWhereParam({
|
|
121
|
+
query: { where: relatedWhere },
|
|
122
|
+
slug: relatedSlug,
|
|
123
|
+
db,
|
|
124
|
+
locale,
|
|
125
|
+
tables,
|
|
126
|
+
configCtx
|
|
127
|
+
});
|
|
128
|
+
if (!relatedCondition) {
|
|
129
|
+
// No documents in related collection match => no parent documents match
|
|
130
|
+
return eq(table.id, '-1');
|
|
131
|
+
}
|
|
132
|
+
// Subquery of related document ids that match the property condition
|
|
133
|
+
const matchingRelatedIds = db.select({ id: relatedTable.id }).from(relatedTable).where(relatedCondition);
|
|
134
|
+
const relsTable = getTable(`${slug}Rels`);
|
|
135
|
+
// Join relation rows to documents by matching the related id and the relation path
|
|
136
|
+
const ownersWithMatching = db
|
|
137
|
+
.select({ id: relsTable.ownerId })
|
|
138
|
+
.from(relsTable)
|
|
139
|
+
.where(and(inArray(relsTable[`${relatedSlug}Id`], matchingRelatedIds), eq(relsTable.path, matchedPrefix), ...(fieldConfig.localized ? [eq(relsTable.locale, locale)] : [])));
|
|
140
|
+
return inArray(table.id, ownersWithMatching);
|
|
141
|
+
};
|
|
142
|
+
// If this is a relation property query, delegate to the relation property handler
|
|
143
|
+
if (isRelationProperty && relationPropertyPath) {
|
|
144
|
+
return buildRelationPropertyCondition();
|
|
102
145
|
}
|
|
103
|
-
//
|
|
146
|
+
// Handle direct relation field queries (e.g., attributes.author)
|
|
147
|
+
// only support a subset of operators for multi-valued relations
|
|
148
|
+
// Unsupported operator for multi-valued relations :
|
|
104
149
|
const supportedRelationManyOperators = ['equals', 'not_equals', 'in_array', 'not_in_array'];
|
|
150
|
+
// Only enforce the restriction for direct relation field queries.
|
|
151
|
+
// If this is a relation property query (e.g. attributes.author.name) we allow
|
|
152
|
+
// any operator because those will be applied to the related collection.
|
|
105
153
|
if (fieldConfig.many && !supportedRelationManyOperators.includes(operator)) {
|
|
106
|
-
|
|
154
|
+
const unsupportedMessage = `the operator "${operator}" is not supported for multi-valued relation field "${column}" in ${documentConfig.slug} document`;
|
|
155
|
+
logger.warn(unsupportedMessage);
|
|
107
156
|
// Return a condition that will always be false
|
|
108
|
-
return eq(table.id, '-1');
|
|
157
|
+
return eq(table.id, '-1');
|
|
109
158
|
}
|
|
110
|
-
//
|
|
111
|
-
// @TODO handle relation props ex: author.email
|
|
159
|
+
// Build relation condition
|
|
112
160
|
const [to, localized] = [fieldConfig.relationTo, fieldConfig.localized];
|
|
113
|
-
const
|
|
114
|
-
const
|
|
161
|
+
const relsTableName = `${slug}Rels`;
|
|
162
|
+
const relsTable = getTable(relsTableName);
|
|
163
|
+
// Helpers for building common relation-owner subqueries (operate on the relations table)
|
|
164
|
+
const buildOwnersWithTotalCount = (count) => db
|
|
165
|
+
.select({ id: relsTable.ownerId })
|
|
166
|
+
.from(relsTable)
|
|
167
|
+
.where(and(eq(relsTable.path, column), ...(localized ? [eq(relsTable.locale, locale)] : [])))
|
|
168
|
+
.groupBy(relsTable.ownerId)
|
|
169
|
+
.having(drizzleORM.eq(drizzleORM.count(relsTable.id), count));
|
|
170
|
+
// Owners that have total relation count equal to the number of provided values
|
|
171
|
+
const buildOwnersWithMatchingCount = (values) => db
|
|
172
|
+
.select({ id: relsTable.ownerId })
|
|
173
|
+
.from(relsTable)
|
|
174
|
+
.where(and(drizzleORM.inArray(relsTable[`${to}Id`], values), eq(relsTable.path, column), ...(localized ? [eq(relsTable.locale, locale)] : [])))
|
|
175
|
+
.groupBy(relsTable.ownerId)
|
|
176
|
+
.having(drizzleORM.eq(drizzleORM.count(relsTable.id), values.length));
|
|
177
|
+
// Owners that have at least one relation row NOT in the provided values
|
|
178
|
+
const buildOwnersWithNonMatching = (values) => db
|
|
179
|
+
.select({ id: relsTable.ownerId })
|
|
180
|
+
.from(relsTable)
|
|
181
|
+
.where(and(drizzleORM.notInArray(relsTable[`${to}Id`], values), eq(relsTable.path, column), ...(localized ? [eq(relsTable.locale, locale)] : [])))
|
|
182
|
+
.groupBy(relsTable.ownerId)
|
|
183
|
+
.having(drizzleORM.gt(drizzleORM.count(relsTable.id), 0));
|
|
184
|
+
// Owners that have any relation rows (to exclude docs with no relations)
|
|
185
|
+
const buildOwnersWithRelations = () => db
|
|
186
|
+
.select({ id: relsTable.ownerId })
|
|
187
|
+
.from(relsTable)
|
|
188
|
+
.where(and(eq(relsTable.path, column), ...(localized ? [eq(relsTable.locale, locale)] : [])))
|
|
189
|
+
.groupBy(relsTable.ownerId)
|
|
190
|
+
.having(drizzleORM.gt(drizzleORM.count(relsTable.id), 0));
|
|
115
191
|
// Handle multi-valued relations specially when operator is `equals` to provide
|
|
116
192
|
// strict equality semantics (the relation set must equal the provided value(s)).
|
|
117
193
|
if (fieldConfig.many && operator === 'equals') {
|
|
118
|
-
// Accept
|
|
119
|
-
|
|
120
|
-
if (Array.isArray(rawValue))
|
|
121
|
-
return rawValue;
|
|
122
|
-
if (typeof rawValue === 'string' && rawValue.includes(','))
|
|
123
|
-
return rawValue.split(',');
|
|
124
|
-
if (Array.isArray(value))
|
|
125
|
-
return value;
|
|
126
|
-
return [value];
|
|
127
|
-
})();
|
|
128
|
-
// Ensure values are unique
|
|
129
|
-
values = Array.from(new Set(values));
|
|
194
|
+
// Accept various input forms and normalize to a unique array
|
|
195
|
+
const values = normalizeValues(rawValue, value);
|
|
130
196
|
// Owners with total relation count equal to values.length
|
|
131
|
-
const ownersWithTotalCount =
|
|
132
|
-
.select({ id: relationTable.ownerId })
|
|
133
|
-
.from(relationTable)
|
|
134
|
-
.where(and(eq(relationTable.path, column), ...(localized ? [eq(relationTable.locale, locale)] : [])))
|
|
135
|
-
.groupBy(relationTable.ownerId)
|
|
136
|
-
.having(drizzleORM.eq(drizzleORM.count(relationTable.id), values.length));
|
|
197
|
+
const ownersWithTotalCount = buildOwnersWithTotalCount(values.length);
|
|
137
198
|
// Owners with matching relations count equal to values.length
|
|
138
|
-
const ownersWithMatchingCount =
|
|
139
|
-
.select({ id: relationTable.ownerId })
|
|
140
|
-
.from(relationTable)
|
|
141
|
-
.where(and(drizzleORM.inArray(relationTable[`${to}Id`], values), eq(relationTable.path, column), ...(localized ? [eq(relationTable.locale, locale)] : [])))
|
|
142
|
-
.groupBy(relationTable.ownerId)
|
|
143
|
-
.having(drizzleORM.eq(drizzleORM.count(relationTable.id), values.length));
|
|
199
|
+
const ownersWithMatchingCount = buildOwnersWithMatchingCount(values);
|
|
144
200
|
return and(inArray(table.id, ownersWithTotalCount), inArray(table.id, ownersWithMatchingCount));
|
|
145
201
|
}
|
|
146
202
|
// For multi-valued relations, allow `in_array` to act as a subset check:
|
|
147
203
|
// The provided values must contain ALL relation values of the document.
|
|
148
204
|
if (fieldConfig.many && operator === 'in_array') {
|
|
149
|
-
// Accept
|
|
150
|
-
|
|
151
|
-
if (Array.isArray(rawValue))
|
|
152
|
-
return rawValue;
|
|
153
|
-
if (typeof rawValue === 'string' && rawValue.includes(','))
|
|
154
|
-
return rawValue.split(',');
|
|
155
|
-
if (Array.isArray(value))
|
|
156
|
-
return value;
|
|
157
|
-
return [value];
|
|
158
|
-
})();
|
|
159
|
-
// Ensure values are unique
|
|
160
|
-
values = Array.from(new Set(values));
|
|
205
|
+
// Accept various input forms and normalize to a unique array
|
|
206
|
+
const values = normalizeValues(rawValue, value);
|
|
161
207
|
// Owners that have at least one relation row not included in the provided set
|
|
162
|
-
const ownersWithNonMatching =
|
|
163
|
-
.select({ id: relationTable.ownerId })
|
|
164
|
-
.from(relationTable)
|
|
165
|
-
.where(and(drizzleORM.notInArray(relationTable[`${to}Id`], values), eq(relationTable.path, column), ...(localized ? [eq(relationTable.locale, locale)] : [])))
|
|
166
|
-
.groupBy(relationTable.ownerId)
|
|
167
|
-
.having(drizzleORM.gt(drizzleORM.count(relationTable.id), 0));
|
|
208
|
+
const ownersWithNonMatching = buildOwnersWithNonMatching(values);
|
|
168
209
|
// Owners that have any relation rows (to exclude docs with no relations)
|
|
169
|
-
const ownersWithRelations =
|
|
170
|
-
.select({ id: relationTable.ownerId })
|
|
171
|
-
.from(relationTable)
|
|
172
|
-
.where(and(eq(relationTable.path, column), ...(localized ? [eq(relationTable.locale, locale)] : [])))
|
|
173
|
-
.groupBy(relationTable.ownerId)
|
|
174
|
-
.having(drizzleORM.gt(drizzleORM.count(relationTable.id), 0));
|
|
210
|
+
const ownersWithRelations = buildOwnersWithRelations();
|
|
175
211
|
// Match documents that have relations and do NOT have any non-matching relation rows
|
|
176
212
|
return and(drizzleORM.notInArray(table.id, ownersWithNonMatching), inArray(table.id, ownersWithRelations));
|
|
177
213
|
}
|
|
178
214
|
// For multi-valued relations, `not_in_array` should match documents where the provided
|
|
179
215
|
// set does NOT contain all of the document's relation values (inverse of `in_array`).
|
|
180
216
|
if (fieldConfig.many && operator === 'not_in_array') {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (typeof rawValue === 'string' && rawValue.includes(','))
|
|
185
|
-
return rawValue.split(',');
|
|
186
|
-
if (Array.isArray(value))
|
|
187
|
-
return value;
|
|
188
|
-
return [value];
|
|
189
|
-
})();
|
|
190
|
-
values = Array.from(new Set(values));
|
|
191
|
-
const ownersWithNonMatching = db
|
|
192
|
-
.select({ id: relationTable.ownerId })
|
|
193
|
-
.from(relationTable)
|
|
194
|
-
.where(and(drizzleORM.notInArray(relationTable[`${to}Id`], values), eq(relationTable.path, column), ...(localized ? [eq(relationTable.locale, locale)] : [])))
|
|
195
|
-
.groupBy(relationTable.ownerId)
|
|
196
|
-
.having(drizzleORM.gt(drizzleORM.count(relationTable.id), 0));
|
|
217
|
+
// Accept various input forms and normalize to a unique array
|
|
218
|
+
const values = normalizeValues(rawValue, value);
|
|
219
|
+
const ownersWithNonMatching = buildOwnersWithNonMatching(values);
|
|
197
220
|
return inArray(table.id, ownersWithNonMatching);
|
|
198
221
|
}
|
|
199
222
|
// For multi-valued relations, `not_equals` is the inverse of `equals` (exact-set inequality)
|
|
200
223
|
if (fieldConfig.many && operator === 'not_equals') {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
return rawValue.split(',');
|
|
206
|
-
if (Array.isArray(value))
|
|
207
|
-
return value;
|
|
208
|
-
return [value];
|
|
209
|
-
})();
|
|
210
|
-
values = Array.from(new Set(values));
|
|
211
|
-
const ownersWithTotalCount = db
|
|
212
|
-
.select({ id: relationTable.ownerId })
|
|
213
|
-
.from(relationTable)
|
|
214
|
-
.where(and(eq(relationTable.path, column), ...(localized ? [eq(relationTable.locale, locale)] : [])))
|
|
215
|
-
.groupBy(relationTable.ownerId)
|
|
216
|
-
.having(drizzleORM.eq(drizzleORM.count(relationTable.id), values.length));
|
|
217
|
-
const ownersWithMatchingCount = db
|
|
218
|
-
.select({ id: relationTable.ownerId })
|
|
219
|
-
.from(relationTable)
|
|
220
|
-
.where(and(inArray(relationTable[`${to}Id`], values), eq(relationTable.path, column), ...(localized ? [eq(relationTable.locale, locale)] : [])))
|
|
221
|
-
.groupBy(relationTable.ownerId)
|
|
222
|
-
.having(drizzleORM.eq(drizzleORM.count(relationTable.id), values.length));
|
|
224
|
+
// Accept various input forms and normalize to a unique array
|
|
225
|
+
const values = normalizeValues(rawValue, value);
|
|
226
|
+
const ownersWithTotalCount = buildOwnersWithTotalCount(values.length);
|
|
227
|
+
const ownersWithMatchingCount = buildOwnersWithMatchingCount(values);
|
|
223
228
|
return or(drizzleORM.notInArray(table.id, ownersWithTotalCount), drizzleORM.notInArray(table.id, ownersWithMatchingCount));
|
|
224
229
|
}
|
|
230
|
+
// Handle single-valued relations and other operators
|
|
225
231
|
// Default behavior (membership checks with in_array etc.)
|
|
226
|
-
const conditions = [
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
232
|
+
const conditions = [
|
|
233
|
+
eq(relsTable.path, column),
|
|
234
|
+
fn(relsTable[`${to}Id`], value),
|
|
235
|
+
...(localized ? [eq(relsTable.locale, locale)] : [])
|
|
236
|
+
];
|
|
230
237
|
return inArray(table.id, db
|
|
231
|
-
.select({ id:
|
|
232
|
-
.from(
|
|
238
|
+
.select({ id: relsTable.ownerId })
|
|
239
|
+
.from(relsTable)
|
|
233
240
|
.where(and(...conditions)));
|
|
234
241
|
};
|
|
235
242
|
return buildCondition(query.where);
|
|
@@ -252,6 +259,7 @@ const operators = {
|
|
|
252
259
|
greater_than: drizzleORM.gt
|
|
253
260
|
};
|
|
254
261
|
const isOperator = (str) => Object.keys(operators).includes(str);
|
|
262
|
+
// Format value based on operator and type
|
|
255
263
|
const formatValue = ({ operator, value }) => {
|
|
256
264
|
switch (true) {
|
|
257
265
|
case typeof value === 'string' &&
|
|
@@ -269,3 +277,43 @@ const formatValue = ({ operator, value }) => {
|
|
|
269
277
|
return value;
|
|
270
278
|
}
|
|
271
279
|
};
|
|
280
|
+
// Extract column, operator, and raw value from condition object
|
|
281
|
+
function getConditionMembers(obj) {
|
|
282
|
+
const [column, operatorObj] = Object.entries(obj)[0];
|
|
283
|
+
const [operator, rawValue] = Object.entries(operatorObj)[0];
|
|
284
|
+
if (!isOperator(operator)) {
|
|
285
|
+
throw new RimeError(RimeError.INVALID_DATA, operator + 'is not supported');
|
|
286
|
+
}
|
|
287
|
+
// get the correct Drizzle operator
|
|
288
|
+
const fn = operators[operator];
|
|
289
|
+
// Convert dot notation to double underscore
|
|
290
|
+
// for fields included in groups or tabs
|
|
291
|
+
const sqlColumn = column.replace(/\./g, '__');
|
|
292
|
+
// Format compared value to support Date, Arrays,...
|
|
293
|
+
const value = formatValue({ operator, value: rawValue });
|
|
294
|
+
return { column, sqlColumn, fn, operator, rawValue, value };
|
|
295
|
+
}
|
|
296
|
+
// Determine if we should handle versioned hierarchy fields
|
|
297
|
+
function shouldHandleVersionedHierarchyFields(slug, sqlColumn) {
|
|
298
|
+
return hasVersionsSuffix(slug) && (sqlColumn === '_parent' || sqlColumn === '_position' || sqlColumn === '_path');
|
|
299
|
+
}
|
|
300
|
+
// Normalize condition object for versioned collections
|
|
301
|
+
function normalizedForVersion(conditionObject, slug) {
|
|
302
|
+
// Handle id field for versioned collections
|
|
303
|
+
// if "id" inside the query it should refer to the root table
|
|
304
|
+
if (hasVersionsSuffix(slug) && 'id' in conditionObject) {
|
|
305
|
+
// Replace id with ownerId and keep the same operator and value
|
|
306
|
+
const idOperator = conditionObject.id;
|
|
307
|
+
delete conditionObject.id;
|
|
308
|
+
conditionObject.ownerId = idOperator;
|
|
309
|
+
}
|
|
310
|
+
// Handle versionId field for versioned collections
|
|
311
|
+
// if "versionId" inside the query it should refer to the id of the version table (current)
|
|
312
|
+
if (hasVersionsSuffix(slug) && 'versionId' in conditionObject) {
|
|
313
|
+
// Replace id with ownerId and keep the same operator and value
|
|
314
|
+
const idOperator = conditionObject.versionId;
|
|
315
|
+
delete conditionObject.versionId;
|
|
316
|
+
conditionObject.id = idOperator;
|
|
317
|
+
}
|
|
318
|
+
return conditionObject;
|
|
319
|
+
}
|
|
@@ -4,8 +4,8 @@ import type { DateField } from '../../../fields/date/index.js';
|
|
|
4
4
|
import type { EmailField } from '../../../fields/email/index.js';
|
|
5
5
|
import type { SlugField } from '../../../fields/slug/index.js';
|
|
6
6
|
import type { TextField } from '../../../fields/text/index.js';
|
|
7
|
-
import type { Field, FormField } from '../../../fields/types.js';
|
|
8
|
-
export declare const hasMaybeTitle: (field: Field) => field is TextField | DateField | SlugField | EmailField;
|
|
7
|
+
import type { Field, FormField, RichTextField } from '../../../fields/types.js';
|
|
8
|
+
export declare const hasMaybeTitle: (field: Field) => field is TextField | DateField | SlugField | EmailField | RichTextField;
|
|
9
9
|
interface TitleFieldResult {
|
|
10
10
|
field: FormFieldBuilder<FormField>;
|
|
11
11
|
path: string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { FormFieldBuilder } from '../../fields/builders/form-field-builder.js';
|
|
2
2
|
import { isGroupField } from '../../../fields/group/index.js';
|
|
3
3
|
import { TabsBuilder } from '../../../fields/tabs/index.js';
|
|
4
|
-
export const hasMaybeTitle = (field) => ['text', 'date', 'slug', 'email'].includes(field.type);
|
|
4
|
+
export const hasMaybeTitle = (field) => ['text', 'date', 'slug', 'email', 'richText'].includes(field.type);
|
|
5
5
|
export function findTitleField(fields = [], basePath = '') {
|
|
6
6
|
for (const field of fields) {
|
|
7
7
|
// Direct check for isTitle
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { getValueAtPath } from '../../../../util/object.js';
|
|
1
|
+
import { getValueAtPath, isObjectLiteral } from '../../../../util/object.js';
|
|
2
|
+
import { richTextJSONToText } from '../../../../fields/rich-text/client.js';
|
|
2
3
|
import { Hooks } from '../index.server.js';
|
|
3
4
|
export const setDocumentTitle = Hooks.beforeRead(async (args) => {
|
|
4
5
|
const config = args.config;
|
|
@@ -7,8 +8,20 @@ export const setDocumentTitle = Hooks.beforeRead(async (args) => {
|
|
|
7
8
|
const hasSelect = Array.isArray(paramSelect) && paramSelect.length;
|
|
8
9
|
const shouldSetTitle = !doc.title && (!hasSelect || (hasSelect && paramSelect.includes('title')));
|
|
9
10
|
if (shouldSetTitle) {
|
|
11
|
+
function computeTitleFromValue(value) {
|
|
12
|
+
// Handle rich text value
|
|
13
|
+
if (isObjectLiteral(value) && 'content' in value) {
|
|
14
|
+
return richTextJSONToText(value);
|
|
15
|
+
}
|
|
16
|
+
if (typeof value === 'string') {
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
return doc.id;
|
|
20
|
+
}
|
|
21
|
+
const titleRaw = getValueAtPath(config.asTitle, doc);
|
|
22
|
+
const title = computeTitleFromValue(titleRaw);
|
|
10
23
|
doc = {
|
|
11
|
-
title
|
|
24
|
+
title,
|
|
12
25
|
...doc
|
|
13
26
|
};
|
|
14
27
|
}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
let { value }: Props = $props();
|
|
18
18
|
let displayCount = $derived(value && value.length > 1);
|
|
19
19
|
|
|
20
|
-
const APIProxy = getAPIProxyContext(API_PROXY.
|
|
20
|
+
const APIProxy = getAPIProxyContext(API_PROXY.ROOT);
|
|
21
21
|
|
|
22
22
|
let APIUrl = $derived.by(() => {
|
|
23
23
|
if (value && value.length && value[0].documentId) {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
});
|
|
34
34
|
</script>
|
|
35
35
|
|
|
36
|
-
<span>
|
|
36
|
+
<span class="rz-relation-cell">
|
|
37
37
|
{#if displayCount}
|
|
38
38
|
{value.length} items
|
|
39
39
|
{:else if ressource?.data?.doc}
|
|
@@ -44,15 +44,21 @@
|
|
|
44
44
|
/>
|
|
45
45
|
{ressource.data.doc.title}
|
|
46
46
|
{:else}
|
|
47
|
-
{ressource.data.doc.title}
|
|
47
|
+
<span class="rz-relation-cell__title">{ressource.data.doc.title}</span>
|
|
48
48
|
{/if}
|
|
49
49
|
{/if}
|
|
50
50
|
</span>
|
|
51
51
|
|
|
52
52
|
<style>
|
|
53
|
-
|
|
53
|
+
.rz-relation-cell {
|
|
54
54
|
display: flex;
|
|
55
55
|
gap: var(--rz-size-2);
|
|
56
56
|
align-items: center;
|
|
57
57
|
}
|
|
58
|
+
.rz-relation-cell__title {
|
|
59
|
+
overflow: hidden;
|
|
60
|
+
display: -webkit-box;
|
|
61
|
+
-webkit-box-orient: vertical;
|
|
62
|
+
-webkit-line-clamp: 1;
|
|
63
|
+
}
|
|
58
64
|
</style>
|
|
@@ -9,6 +9,7 @@ export declare class RichTextFieldBuilder extends FormFieldBuilder<RichTextField
|
|
|
9
9
|
get cell(): import("svelte").Component<{
|
|
10
10
|
value: string;
|
|
11
11
|
}, {}, "">;
|
|
12
|
+
isTitle(): this;
|
|
12
13
|
/**
|
|
13
14
|
* Sets a custom TipTap editor configuration for the rich text field.
|
|
14
15
|
*
|
|
@@ -44,6 +45,7 @@ type RichTextContent = {
|
|
|
44
45
|
};
|
|
45
46
|
export type RichTextField = FormField & {
|
|
46
47
|
type: 'richText';
|
|
48
|
+
isTitle?: true;
|
|
47
49
|
features?: Array<RichTextFeature>;
|
|
48
50
|
defaultValue?: RichTextContent | DefaultValueFn<RichTextContent>;
|
|
49
51
|
};
|