@opengis/cms 0.0.57 → 0.0.58
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 +131 -131
- package/dist/AddNewItemInTree-05PSSEFi.js +76 -0
- package/dist/ArticlesPage-CFjE_cw_.js +298 -0
- package/dist/CollectionsBreadcrumb-BCxeRikP.js +4 -0
- package/dist/CollectionsBreadcrumb.vue_vue_type_script_setup_true_lang-umRzB5mY.js +53 -0
- package/dist/CollectionsPage-DHfPNql6.js +124 -0
- package/dist/{CreateForm-BMOBeP4G.js → CreateForm-5FvT45vH.js} +1 -1
- package/dist/Dashboard-C1eGscNd.js +358 -0
- package/dist/EditCollectionPage-DIr1tdtn.js +187 -0
- package/dist/{EmptyData-DaZt_nAm.js → EmptyData-DxPrSXhV.js} +1 -1
- package/dist/{MenuAddPage-Bf48Z-ah.js → MenuAddPage-D-p3gFgm.js} +40 -35
- package/dist/MenuBody-rN5j4YBu.js +125 -0
- package/dist/MenuItemPage-BoJw885D.js +1027 -0
- package/dist/MenuList-DFEBS0NB.js +172 -0
- package/dist/MenuPage-BCZB_S8j.js +107 -0
- package/dist/MenuWrapper-AZ_8s-zd.js +12 -0
- package/dist/MonacoEditor-Db-3Jc3E.js +4 -0
- package/dist/{UniversalTable.vue_vue_type_script_setup_true_lang-CJGTsd1V.js → UniversalTable-CzqPG-tY.js} +12 -12
- package/dist/{UniversalTablePagination.vue_vue_type_script_setup_true_lang-GYZd_gkA.js → UniversalTablePagination-4gL47A7I.js} +1 -1
- package/dist/VsFormTags-CMjiu9sY.js +114 -0
- package/dist/VsPreview-DwETkOpb.js +63 -0
- package/dist/contentForm-CtMhQTG0.js +489 -0
- package/dist/getField-CpwVE28P.js +179 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.html +29 -29
- package/dist/index.js +72 -71
- package/dist/style.css +1 -1
- package/dist/vs-builder-edit-D-q1o8tF.js +604 -0
- package/dist/vs-builder-monaco-B3Jj0V31.js +33 -0
- package/dist/vs-builder-preview-BH4VAM3a.js +44 -0
- package/dist/vs-form-custom-datatable-BDZo48w3.js +317 -0
- package/dist/vs-form-integer-BZ855R3g.js +61 -0
- package/dist/vs-form-media-select-NY27EaG1.js +837 -0
- package/dist/vs-form-reference-list-Dtv8fJJU.js +1536 -0
- package/dist/vs-form-reletion-link-BhzNQszm.js +34 -0
- package/dist/vs-form-tiptap-DDFQjRjY.js +4 -0
- package/dist/vs-form-tiptap.vue_vue_type_script_setup_true_lang-DGgsqXwg.js +11 -0
- package/dist/vs-richtext-md-C098v_6Q.js +4 -0
- package/dist/vs-richtext-md.vue_vue_type_script_setup_true_lang-Ct8uTV-J.js +14 -0
- package/input-types.json +9 -9
- package/locales/en.json +815 -814
- package/locales/uk.json +813 -812
- package/module/cms/cls/content.status.json +17 -17
- package/module/cms/cls/user_type.json +9 -9
- package/module/cms/form/admin.users.form.json +77 -77
- package/module/cms/select/cms.page_type.sql +1 -1
- package/module/cms/select/news_tag_id.sql +11 -11
- package/module/cms/table/admin.users.table.json +53 -53
- package/module/cms/table/collection.default.table.json +96 -96
- package/module/cms/table/single.default.table.json +116 -116
- package/package.json +68 -68
- package/plugin.js +43 -43
- package/server/app.js +35 -35
- package/server/config.js +4 -4
- package/server/functions/getContent.js +45 -45
- package/server/functions/getDraftKey.js +22 -22
- package/server/functions/getSearchData.js +31 -31
- package/server/functions/getTags.js +30 -30
- package/server/functions/getUser.js +27 -27
- package/server/functions/utils/mock.reply.js +55 -55
- package/server/index.js +22 -22
- package/server/migrations/fixes.sql +129 -129
- package/server/migrations/site.sql +595 -595
- package/server/plugins/adminHook.js +78 -78
- package/server/plugins/hook.js +59 -59
- package/server/plugins/vite.js +75 -75
- package/server/routes/category/controllers/cms.category.delete.js +21 -21
- package/server/routes/category/controllers/cms.category.get.js +17 -17
- package/server/routes/category/controllers/cms.category.list.js +16 -16
- package/server/routes/category/controllers/cms.category.post.js +21 -21
- package/server/routes/category/controllers/cms.category.put.js +23 -23
- package/server/routes/category/index.mjs +22 -22
- package/server/routes/cms/controllers/cmsStat.js +55 -55
- package/server/routes/cms/controllers/cmsSuggest.js +57 -57
- package/server/routes/cms/controllers/deleteContent.js +113 -113
- package/server/routes/cms/controllers/deleteMedia.js +76 -76
- package/server/routes/cms/controllers/downloadMedia.js +84 -84
- package/server/routes/cms/controllers/getContent.js +113 -113
- package/server/routes/cms/controllers/getContentBySlug.js +93 -93
- package/server/routes/cms/controllers/insertContent.js +217 -217
- package/server/routes/cms/controllers/listMedia.js +155 -155
- package/server/routes/cms/controllers/metadataMedia.js +39 -39
- package/server/routes/cms/controllers/properties.get.js +18 -18
- package/server/routes/cms/controllers/properties.post.js +99 -99
- package/server/routes/cms/controllers/searchContent.js +214 -214
- package/server/routes/cms/controllers/translate.js +89 -89
- package/server/routes/cms/controllers/updateContent.js +266 -266
- package/server/routes/cms/controllers/uploadMedia.js +79 -79
- package/server/routes/cms/functions/getSettings.js +48 -48
- package/server/routes/cms/index.mjs +112 -112
- package/server/routes/cms/utils/additionalData.js +35 -35
- package/server/routes/cms/utils/getCollection.js +89 -89
- package/server/routes/cms/utils/getSingle.js +188 -188
- package/server/routes/cms/utils/inputTypes.js +5 -5
- package/server/routes/cms/utils/insertContentLocalization.js +104 -104
- package/server/routes/cms/utils/requestTranslation.js +85 -85
- package/server/routes/cms/utils/updateLocalization.js +47 -47
- package/server/routes/cmsSpace/controllers/deleteSpace.js +25 -25
- package/server/routes/cmsSpace/controllers/getSpaces.js +27 -27
- package/server/routes/cmsSpace/controllers/insertSpace.js +21 -21
- package/server/routes/cmsSpace/controllers/updateSpace.js +23 -23
- package/server/routes/cmsSpace/index.mjs +20 -20
- package/server/routes/contentType/controllers/addContentType.js +160 -160
- package/server/routes/contentType/controllers/contentTypeList.js +54 -54
- package/server/routes/contentType/controllers/delContentType.js +75 -75
- package/server/routes/contentType/controllers/editContentType.js +88 -88
- package/server/routes/contentType/controllers/getContentType.js +65 -65
- package/server/routes/contentType/index.mjs +35 -35
- package/server/routes/contentType/utils/updateContents.js +44 -44
- package/server/routes/contentType/utils/updateCustomContentTable.js +53 -53
- package/server/routes/feedback/controllers/email.list.js +24 -24
- package/server/routes/feedback/controllers/feedback.js +48 -48
- package/server/routes/feedback/controllers/feedback.list.js +37 -37
- package/server/routes/feedback/controllers/news.subscriptions.js +44 -44
- package/server/routes/feedback/index.mjs +71 -71
- package/server/routes/logs/controllers/export.user.logs.js +77 -77
- package/server/routes/logs/controllers/user.logs.js +44 -44
- package/server/routes/logs/index.mjs +9 -9
- package/server/routes/menu/controllers/addMenu.js +37 -37
- package/server/routes/menu/controllers/delMenu.js +31 -31
- package/server/routes/menu/controllers/editMenu.js +41 -41
- package/server/routes/menu/controllers/getMenu.js +24 -24
- package/server/routes/menu/functions/getMenu.js +50 -50
- package/server/routes/menu/index.mjs +13 -13
- package/server/routes/migration/controllers/collectionToCustom.js +137 -137
- package/server/routes/migration/index.mjs +8 -8
- package/server/routes/root.mjs +8 -8
- package/server/routes/tags/controllers/add.tags.js +24 -24
- package/server/routes/tags/controllers/del.tags.js +19 -19
- package/server/routes/tags/controllers/edit.tags.js +25 -25
- package/server/routes/tags/controllers/get.tags.js +15 -15
- package/server/routes/tags/index.mjs +14 -14
- package/server/templates/cls/cms.category_type.json +9 -9
- package/server/templates/cls/cms.content_review_status.json +9 -9
- package/server/templates/cls/cms.content_status.json +9 -9
- package/server/templates/cls/cms.content_type.json +9 -9
- package/server/templates/cls/cms.lang.json +9 -9
- package/server/templates/page/login.html +126 -126
- package/utils.d.ts +52 -52
- package/utils.js +8 -8
- package/dist/ArticlesPage-BcR1hbds.js +0 -286
- package/dist/BuilderPage-CK_osM89.js +0 -386
- package/dist/CollectionsBreadcrumb.vue_vue_type_script_setup_true_lang-CnOe9ORD.js +0 -45
- package/dist/CollectionsPage-JfmrHNR_.js +0 -110
- package/dist/EditCollectionPage-Cw3GQYRe.js +0 -809
- package/dist/MenuItemPage-CXn5HC8j.js +0 -1366
- package/dist/MenuPage-tJZtK46W.js +0 -106
- package/dist/contentForm-B6gHgGkz.js +0 -586
- package/dist/getField-Y5WXnRR0.js +0 -2948
|
@@ -1,214 +1,214 @@
|
|
|
1
|
-
import { pgClients, getFilterSQL } from '@opengis/fastify-table/utils.js';
|
|
2
|
-
|
|
3
|
-
import additionalData from '../utils/additionalData.js';
|
|
4
|
-
|
|
5
|
-
const filterList = [
|
|
6
|
-
{
|
|
7
|
-
ua: 'Статус',
|
|
8
|
-
name: 'status',
|
|
9
|
-
type: 'Check'
|
|
10
|
-
},
|
|
11
|
-
{
|
|
12
|
-
name: 'published_at',
|
|
13
|
-
type: 'Date'
|
|
14
|
-
},
|
|
15
|
-
{
|
|
16
|
-
name: 'search',
|
|
17
|
-
columns: 'slug,title',
|
|
18
|
-
type: 'Text'
|
|
19
|
-
},
|
|
20
|
-
];
|
|
21
|
-
|
|
22
|
-
const matchFilterType = {
|
|
23
|
-
select: 'check',
|
|
24
|
-
datetime: 'date'
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const maxLimit = 100;
|
|
28
|
-
|
|
29
|
-
export default async function searchContent({
|
|
30
|
-
pg = pgClients.client, query = {},
|
|
31
|
-
}, reply) {
|
|
32
|
-
if (!pg?.pk?.['site.contents']) {
|
|
33
|
-
return reply.status(404).send({ error: 'table not found', code: 404 });
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const t1 = Date.now();
|
|
37
|
-
|
|
38
|
-
const {
|
|
39
|
-
locale, // to do?
|
|
40
|
-
search,
|
|
41
|
-
filter,
|
|
42
|
-
contentType,
|
|
43
|
-
asc,
|
|
44
|
-
tags,
|
|
45
|
-
} = query;
|
|
46
|
-
|
|
47
|
-
if (contentType && typeof contentType !== 'string') {
|
|
48
|
-
return reply.status(400).send({ error: 'invalid query params: contentType must be a string', code: 400 });
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (tags && typeof tags !== 'string') {
|
|
52
|
-
return reply.status(400).send({ error: 'invalid query params: tags must be a string', code: 400 });
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// add for singletone offset
|
|
56
|
-
const page = Math.max(1, parseInt(query.page || 1, 10));
|
|
57
|
-
const limit = Math.min(query.limit || 16, maxLimit);
|
|
58
|
-
const offset = (page - 1) * limit;
|
|
59
|
-
|
|
60
|
-
const { q } = filter || search ? await getFilterSQL({
|
|
61
|
-
pg,
|
|
62
|
-
table: 'site.contents',
|
|
63
|
-
filter,
|
|
64
|
-
search,
|
|
65
|
-
searchColumn: 'a.slug,a.title',
|
|
66
|
-
query: `status='published'`,
|
|
67
|
-
filterList,
|
|
68
|
-
}) : { q: `status='published'` };
|
|
69
|
-
|
|
70
|
-
// 1. Query all metadata
|
|
71
|
-
const q1 = `
|
|
72
|
-
SELECT a.title, a.slug, a.status, a.content_type_id, b.name as content_type_name, b.title as content_type_title, a.content_id,
|
|
73
|
-
b.type as content_type, b.columns, b.table_name, case when b.type = 'single' then main_image else null end as main_image
|
|
74
|
-
FROM site.contents a
|
|
75
|
-
LEFT JOIN site.content_types b ON a.content_type_id = b.content_type_id
|
|
76
|
-
WHERE b.visible
|
|
77
|
-
AND a.slug IS NOT NULL
|
|
78
|
-
${tags ? `AND case when b.type = 'single' then a.content_id in (SELECT data_id FROM site.tag_data td left join site.tags ts on td.tag_id=ts.tag_id WHERE ts.slug = any($1) or ts.tag_id = any($1)) else true end` : ''}
|
|
79
|
-
${contentType ? `and ((b.name=any($${tags ? 2 : 1}) or a.content_type_id=any($${tags ? 2 : 1}) or b.title=any($${tags ? 2 : 1})) ${contentType.includes('pages') ? `or b.type='single')` : ')'}` : ''}
|
|
80
|
-
${q ? `AND (CASE WHEN b.type = 'single' THEN ${q.replace(/"title"/g, 'a.title').replace(/"slug"/g, 'a.slug').replace(/status/g, 'a.status')} ELSE TRUE END)` : ''}
|
|
81
|
-
`;
|
|
82
|
-
|
|
83
|
-
const { rows: contents = [] } = await pg.query(q1, [(tags ? tags.split(',') : null), (contentType ? contentType.split(',') : null)].filter(Boolean))
|
|
84
|
-
.catch(err => {
|
|
85
|
-
console.error(q1, err.toString(), err.stack);
|
|
86
|
-
return {};
|
|
87
|
-
}) || {};
|
|
88
|
-
|
|
89
|
-
if (!contents.length) {
|
|
90
|
-
return reply.status(404).send({ error: 'content not found', code: 404 });
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const singles = contents.filter(el => el.content_type === 'single');
|
|
94
|
-
const collections = contents.filter(el => el.content_type === 'collection' && el.table_name);
|
|
95
|
-
|
|
96
|
-
// const totals = await pg.query(`select json_object_agg(oid::regclass, reltuples) from pg_class`).then(el => el.rows?.[0]?.json_object_agg || {});
|
|
97
|
-
const allSingles = await pg.query(`select count(*)::int from site.contents where content_type_id in (select content_type_id from site.content_types where type = 'single') and status='published'`).then(el => el.rows?.[0]?.count || 0);
|
|
98
|
-
const allCollectionTables = await pg.query(`select array_agg(table_name) from site.content_types where type = 'collection' and table_name is not null`).then(el => el.rows?.[0]?.array_agg || []);
|
|
99
|
-
|
|
100
|
-
const allCollections = await Promise.all(allCollectionTables.map(async tablename => {
|
|
101
|
-
const total = pg.queryCache ? await pg.queryCache(`select count(*)::int from data."${tablename}" where status='published'`, { table: `data.${tablename}` }).then(el => el.rows?.[0]?.count || 0) : 0;
|
|
102
|
-
return total;
|
|
103
|
-
})).then(el => el.reduce((acc, curr) => acc + (curr || 0), 0) || 0);
|
|
104
|
-
|
|
105
|
-
// collections
|
|
106
|
-
const collectionResults = await Promise.all(collections.map(async row => {
|
|
107
|
-
const { table_name: table, columns = [] } = row;
|
|
108
|
-
|
|
109
|
-
const collectionFilters = columns.filter(el =>
|
|
110
|
-
['title', 'status', 'published_at'].includes(el.name) || el.type === 'text'
|
|
111
|
-
).map(({ name, type }) => ({ name, type: matchFilterType[type] || type })); // select column => check type filter
|
|
112
|
-
|
|
113
|
-
collectionFilters.push({
|
|
114
|
-
name: 'search',
|
|
115
|
-
columns: collectionFilters
|
|
116
|
-
.filter(({ type }) => type === 'text')
|
|
117
|
-
.map(({ name }) => name).join(','),
|
|
118
|
-
type: 'Text',
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
const order = {
|
|
122
|
-
true: 'published_at ASC NULLS LAST',
|
|
123
|
-
false: 'published_at DESC NULLS LAST'
|
|
124
|
-
}[!!asc];
|
|
125
|
-
|
|
126
|
-
const { optimizedSQL = `SELECT * FROM data."${table}" where status='published' ORDER BY ${order}` } =
|
|
127
|
-
filter || search ? await getFilterSQL({
|
|
128
|
-
pg,
|
|
129
|
-
table: `data."${table}"`,
|
|
130
|
-
filter,
|
|
131
|
-
search,
|
|
132
|
-
searchColumn: 'slug,title',
|
|
133
|
-
query: `status='published'`,
|
|
134
|
-
filterList: collectionFilters,
|
|
135
|
-
order: 'published_at DESC NULLS LAST'
|
|
136
|
-
}) : {};
|
|
137
|
-
|
|
138
|
-
const q2 = `select id, ${columns.filter(el =>
|
|
139
|
-
['title', 'status', 'published_at', 'slug', 'main_image'].includes(el.name) || el.type === 'text'
|
|
140
|
-
).map(({ name }) => name === 'published_at' ? name : `"${name}"`).join(',')} from (${optimizedSQL}) q where ${tags ? 'id in (SELECT data_id FROM site.tag_data td left join site.tags ts on td.tag_id=ts.tag_id WHERE ts.slug = any($1))' : '1=1'} limit ${offset + limit}`;
|
|
141
|
-
|
|
142
|
-
// const total = pg.queryCache ? await pg.queryCache(`select count(*)::int from data."${table}"`, { table: `data.${table}` }).then(el => el.rows?.[0]?.count || 0) : 0;
|
|
143
|
-
|
|
144
|
-
const { rows: items = [] } = await pg.query(q2, [tags ? tags.split(',') : null].filter(Boolean)).catch(err => {
|
|
145
|
-
console.error(q2, err.toString(), err.stack);
|
|
146
|
-
return { rows: [], /*total: total // totals[`data.${table}`] || totals[`data."${table}"`] || 0*/ };
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
// await additionalData(pg, items, locale);
|
|
150
|
-
|
|
151
|
-
return {
|
|
152
|
-
rows: items.map(el => ({
|
|
153
|
-
...row,
|
|
154
|
-
...el,
|
|
155
|
-
slug: el.slug,
|
|
156
|
-
title: el.title,
|
|
157
|
-
status: el.status,
|
|
158
|
-
published_at: el.published_at,
|
|
159
|
-
table_name: undefined,
|
|
160
|
-
columns: undefined
|
|
161
|
-
})),
|
|
162
|
-
// total: total /*totals[`data.${table}`] || totals[`data."${table}"`] || 0,*/
|
|
163
|
-
};
|
|
164
|
-
}));
|
|
165
|
-
|
|
166
|
-
const allRows = [
|
|
167
|
-
...singles.map(row => ({
|
|
168
|
-
...row,
|
|
169
|
-
columns: undefined,
|
|
170
|
-
table_name: undefined
|
|
171
|
-
})),
|
|
172
|
-
...collectionResults.map(el => el.rows || []).flat()
|
|
173
|
-
];
|
|
174
|
-
|
|
175
|
-
const collectionIds = pg.queryCache ? await Promise.all(contents.filter(el => el.content_type === 'collection' && el.table_name).map(async row => pg.queryCache(`select array_agg(id) from data."${row.table_name}"`))).then(el => el.map(res => res.rows[0]?.array_agg || []).flat()) : [];
|
|
176
|
-
|
|
177
|
-
// add tags etc. for current page of rows
|
|
178
|
-
const allTags = pg.queryCache ? await pg.queryCache(
|
|
179
|
-
`select a.data_id, b.tag_id, b.value, b.color, b.slug from site.tag_data a left join site.tags b on a.tag_id=b.tag_id
|
|
180
|
-
where a.data_id = any($1)`,
|
|
181
|
-
{ args: [contents.map(el => el.content_id).concat(collectionIds)], table: 'site.tag_data' },
|
|
182
|
-
).then(el => el.rows || []) : [];
|
|
183
|
-
|
|
184
|
-
// sort
|
|
185
|
-
const sorted = allRows.sort((a, b) => {
|
|
186
|
-
const aDate = new Date(a.published_at || 0);
|
|
187
|
-
const bDate = new Date(b.published_at || 0);
|
|
188
|
-
return bDate - aDate;
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
const paginated = sorted.slice(offset, offset + limit);
|
|
192
|
-
|
|
193
|
-
paginated.forEach(row => {
|
|
194
|
-
if (row.content_type === 'single' && row.content_id) {
|
|
195
|
-
row.tag_list = allTags
|
|
196
|
-
.filter(tag => tag.data_id === row.content_id)
|
|
197
|
-
.map(({ tag_id: id, value: text, color, slug }) => ({ id, text, color, slug }));
|
|
198
|
-
} else if (row.content_type === 'collection' && row.id) {
|
|
199
|
-
row.tag_list = allTags
|
|
200
|
-
.filter(tag => tag.data_id === row.id)
|
|
201
|
-
.map(({ tag_id: id, value: text, color, slug }) => ({ id, text, color, slug }));
|
|
202
|
-
}
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
return reply.status(200).send({
|
|
206
|
-
time: Date.now() - t1,
|
|
207
|
-
page,
|
|
208
|
-
limit,
|
|
209
|
-
total: allSingles + allCollections,
|
|
210
|
-
filtered: sorted.length,
|
|
211
|
-
count: paginated.length,
|
|
212
|
-
rows: paginated,
|
|
213
|
-
});
|
|
214
|
-
}
|
|
1
|
+
import { pgClients, getFilterSQL } from '@opengis/fastify-table/utils.js';
|
|
2
|
+
|
|
3
|
+
import additionalData from '../utils/additionalData.js';
|
|
4
|
+
|
|
5
|
+
const filterList = [
|
|
6
|
+
{
|
|
7
|
+
ua: 'Статус',
|
|
8
|
+
name: 'status',
|
|
9
|
+
type: 'Check'
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
name: 'published_at',
|
|
13
|
+
type: 'Date'
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: 'search',
|
|
17
|
+
columns: 'slug,title',
|
|
18
|
+
type: 'Text'
|
|
19
|
+
},
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const matchFilterType = {
|
|
23
|
+
select: 'check',
|
|
24
|
+
datetime: 'date'
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const maxLimit = 100;
|
|
28
|
+
|
|
29
|
+
export default async function searchContent({
|
|
30
|
+
pg = pgClients.client, query = {},
|
|
31
|
+
}, reply) {
|
|
32
|
+
if (!pg?.pk?.['site.contents']) {
|
|
33
|
+
return reply.status(404).send({ error: 'table not found', code: 404 });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const t1 = Date.now();
|
|
37
|
+
|
|
38
|
+
const {
|
|
39
|
+
locale, // to do?
|
|
40
|
+
search,
|
|
41
|
+
filter,
|
|
42
|
+
contentType,
|
|
43
|
+
asc,
|
|
44
|
+
tags,
|
|
45
|
+
} = query;
|
|
46
|
+
|
|
47
|
+
if (contentType && typeof contentType !== 'string') {
|
|
48
|
+
return reply.status(400).send({ error: 'invalid query params: contentType must be a string', code: 400 });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (tags && typeof tags !== 'string') {
|
|
52
|
+
return reply.status(400).send({ error: 'invalid query params: tags must be a string', code: 400 });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// add for singletone offset
|
|
56
|
+
const page = Math.max(1, parseInt(query.page || 1, 10));
|
|
57
|
+
const limit = Math.min(query.limit || 16, maxLimit);
|
|
58
|
+
const offset = (page - 1) * limit;
|
|
59
|
+
|
|
60
|
+
const { q } = filter || search ? await getFilterSQL({
|
|
61
|
+
pg,
|
|
62
|
+
table: 'site.contents',
|
|
63
|
+
filter,
|
|
64
|
+
search,
|
|
65
|
+
searchColumn: 'a.slug,a.title',
|
|
66
|
+
query: `status='published'`,
|
|
67
|
+
filterList,
|
|
68
|
+
}) : { q: `status='published'` };
|
|
69
|
+
|
|
70
|
+
// 1. Query all metadata
|
|
71
|
+
const q1 = `
|
|
72
|
+
SELECT a.title, a.slug, a.status, a.content_type_id, b.name as content_type_name, b.title as content_type_title, a.content_id,
|
|
73
|
+
b.type as content_type, b.columns, b.table_name, case when b.type = 'single' then main_image else null end as main_image
|
|
74
|
+
FROM site.contents a
|
|
75
|
+
LEFT JOIN site.content_types b ON a.content_type_id = b.content_type_id
|
|
76
|
+
WHERE b.visible
|
|
77
|
+
AND a.slug IS NOT NULL
|
|
78
|
+
${tags ? `AND case when b.type = 'single' then a.content_id in (SELECT data_id FROM site.tag_data td left join site.tags ts on td.tag_id=ts.tag_id WHERE ts.slug = any($1) or ts.tag_id = any($1)) else true end` : ''}
|
|
79
|
+
${contentType ? `and ((b.name=any($${tags ? 2 : 1}) or a.content_type_id=any($${tags ? 2 : 1}) or b.title=any($${tags ? 2 : 1})) ${contentType.includes('pages') ? `or b.type='single')` : ')'}` : ''}
|
|
80
|
+
${q ? `AND (CASE WHEN b.type = 'single' THEN ${q.replace(/"title"/g, 'a.title').replace(/"slug"/g, 'a.slug').replace(/status/g, 'a.status')} ELSE TRUE END)` : ''}
|
|
81
|
+
`;
|
|
82
|
+
|
|
83
|
+
const { rows: contents = [] } = await pg.query(q1, [(tags ? tags.split(',') : null), (contentType ? contentType.split(',') : null)].filter(Boolean))
|
|
84
|
+
.catch(err => {
|
|
85
|
+
console.error(q1, err.toString(), err.stack);
|
|
86
|
+
return {};
|
|
87
|
+
}) || {};
|
|
88
|
+
|
|
89
|
+
if (!contents.length) {
|
|
90
|
+
return reply.status(404).send({ error: 'content not found', code: 404 });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const singles = contents.filter(el => el.content_type === 'single');
|
|
94
|
+
const collections = contents.filter(el => el.content_type === 'collection' && el.table_name);
|
|
95
|
+
|
|
96
|
+
// const totals = await pg.query(`select json_object_agg(oid::regclass, reltuples) from pg_class`).then(el => el.rows?.[0]?.json_object_agg || {});
|
|
97
|
+
const allSingles = await pg.query(`select count(*)::int from site.contents where content_type_id in (select content_type_id from site.content_types where type = 'single') and status='published'`).then(el => el.rows?.[0]?.count || 0);
|
|
98
|
+
const allCollectionTables = await pg.query(`select array_agg(table_name) from site.content_types where type = 'collection' and table_name is not null`).then(el => el.rows?.[0]?.array_agg || []);
|
|
99
|
+
|
|
100
|
+
const allCollections = await Promise.all(allCollectionTables.map(async tablename => {
|
|
101
|
+
const total = pg.queryCache ? await pg.queryCache(`select count(*)::int from data."${tablename}" where status='published'`, { table: `data.${tablename}` }).then(el => el.rows?.[0]?.count || 0) : 0;
|
|
102
|
+
return total;
|
|
103
|
+
})).then(el => el.reduce((acc, curr) => acc + (curr || 0), 0) || 0);
|
|
104
|
+
|
|
105
|
+
// collections
|
|
106
|
+
const collectionResults = await Promise.all(collections.map(async row => {
|
|
107
|
+
const { table_name: table, columns = [] } = row;
|
|
108
|
+
|
|
109
|
+
const collectionFilters = columns.filter(el =>
|
|
110
|
+
['title', 'status', 'published_at'].includes(el.name) || el.type === 'text'
|
|
111
|
+
).map(({ name, type }) => ({ name, type: matchFilterType[type] || type })); // select column => check type filter
|
|
112
|
+
|
|
113
|
+
collectionFilters.push({
|
|
114
|
+
name: 'search',
|
|
115
|
+
columns: collectionFilters
|
|
116
|
+
.filter(({ type }) => type === 'text')
|
|
117
|
+
.map(({ name }) => name).join(','),
|
|
118
|
+
type: 'Text',
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const order = {
|
|
122
|
+
true: 'published_at ASC NULLS LAST',
|
|
123
|
+
false: 'published_at DESC NULLS LAST'
|
|
124
|
+
}[!!asc];
|
|
125
|
+
|
|
126
|
+
const { optimizedSQL = `SELECT * FROM data."${table}" where status='published' ORDER BY ${order}` } =
|
|
127
|
+
filter || search ? await getFilterSQL({
|
|
128
|
+
pg,
|
|
129
|
+
table: `data."${table}"`,
|
|
130
|
+
filter,
|
|
131
|
+
search,
|
|
132
|
+
searchColumn: 'slug,title',
|
|
133
|
+
query: `status='published'`,
|
|
134
|
+
filterList: collectionFilters,
|
|
135
|
+
order: 'published_at DESC NULLS LAST'
|
|
136
|
+
}) : {};
|
|
137
|
+
|
|
138
|
+
const q2 = `select id, ${columns.filter(el =>
|
|
139
|
+
['title', 'status', 'published_at', 'slug', 'main_image'].includes(el.name) || el.type === 'text'
|
|
140
|
+
).map(({ name }) => name === 'published_at' ? name : `"${name}"`).join(',')} from (${optimizedSQL}) q where ${tags ? 'id in (SELECT data_id FROM site.tag_data td left join site.tags ts on td.tag_id=ts.tag_id WHERE ts.slug = any($1))' : '1=1'} limit ${offset + limit}`;
|
|
141
|
+
|
|
142
|
+
// const total = pg.queryCache ? await pg.queryCache(`select count(*)::int from data."${table}"`, { table: `data.${table}` }).then(el => el.rows?.[0]?.count || 0) : 0;
|
|
143
|
+
|
|
144
|
+
const { rows: items = [] } = await pg.query(q2, [tags ? tags.split(',') : null].filter(Boolean)).catch(err => {
|
|
145
|
+
console.error(q2, err.toString(), err.stack);
|
|
146
|
+
return { rows: [], /*total: total // totals[`data.${table}`] || totals[`data."${table}"`] || 0*/ };
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// await additionalData(pg, items, locale);
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
rows: items.map(el => ({
|
|
153
|
+
...row,
|
|
154
|
+
...el,
|
|
155
|
+
slug: el.slug,
|
|
156
|
+
title: el.title,
|
|
157
|
+
status: el.status,
|
|
158
|
+
published_at: el.published_at,
|
|
159
|
+
table_name: undefined,
|
|
160
|
+
columns: undefined
|
|
161
|
+
})),
|
|
162
|
+
// total: total /*totals[`data.${table}`] || totals[`data."${table}"`] || 0,*/
|
|
163
|
+
};
|
|
164
|
+
}));
|
|
165
|
+
|
|
166
|
+
const allRows = [
|
|
167
|
+
...singles.map(row => ({
|
|
168
|
+
...row,
|
|
169
|
+
columns: undefined,
|
|
170
|
+
table_name: undefined
|
|
171
|
+
})),
|
|
172
|
+
...collectionResults.map(el => el.rows || []).flat()
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
const collectionIds = pg.queryCache ? await Promise.all(contents.filter(el => el.content_type === 'collection' && el.table_name).map(async row => pg.queryCache(`select array_agg(id) from data."${row.table_name}"`))).then(el => el.map(res => res.rows[0]?.array_agg || []).flat()) : [];
|
|
176
|
+
|
|
177
|
+
// add tags etc. for current page of rows
|
|
178
|
+
const allTags = pg.queryCache ? await pg.queryCache(
|
|
179
|
+
`select a.data_id, b.tag_id, b.value, b.color, b.slug from site.tag_data a left join site.tags b on a.tag_id=b.tag_id
|
|
180
|
+
where a.data_id = any($1)`,
|
|
181
|
+
{ args: [contents.map(el => el.content_id).concat(collectionIds)], table: 'site.tag_data' },
|
|
182
|
+
).then(el => el.rows || []) : [];
|
|
183
|
+
|
|
184
|
+
// sort
|
|
185
|
+
const sorted = allRows.sort((a, b) => {
|
|
186
|
+
const aDate = new Date(a.published_at || 0);
|
|
187
|
+
const bDate = new Date(b.published_at || 0);
|
|
188
|
+
return bDate - aDate;
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const paginated = sorted.slice(offset, offset + limit);
|
|
192
|
+
|
|
193
|
+
paginated.forEach(row => {
|
|
194
|
+
if (row.content_type === 'single' && row.content_id) {
|
|
195
|
+
row.tag_list = allTags
|
|
196
|
+
.filter(tag => tag.data_id === row.content_id)
|
|
197
|
+
.map(({ tag_id: id, value: text, color, slug }) => ({ id, text, color, slug }));
|
|
198
|
+
} else if (row.content_type === 'collection' && row.id) {
|
|
199
|
+
row.tag_list = allTags
|
|
200
|
+
.filter(tag => tag.data_id === row.id)
|
|
201
|
+
.map(({ tag_id: id, value: text, color, slug }) => ({ id, text, color, slug }));
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
return reply.status(200).send({
|
|
206
|
+
time: Date.now() - t1,
|
|
207
|
+
page,
|
|
208
|
+
limit,
|
|
209
|
+
total: allSingles + allCollections,
|
|
210
|
+
filtered: sorted.length,
|
|
211
|
+
count: paginated.length,
|
|
212
|
+
rows: paginated,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
@@ -1,90 +1,90 @@
|
|
|
1
|
-
import { pgClients, eventStream } from "@opengis/fastify-table/utils.js";
|
|
2
|
-
|
|
3
|
-
import insertContentLocalization from "../utils/insertContentLocalization.js";
|
|
4
|
-
|
|
5
|
-
const getCollectionId = async (id, pg) => pg.query(`select content_type_id as id, table_name as table, columns FROM site.content_types
|
|
6
|
-
where $1 in (content_type_id, name)`, [id]).then(el => el.rows?.[0] || {});
|
|
7
|
-
|
|
8
|
-
function sequence(arr, data, fn) {
|
|
9
|
-
return arr.reduce((promise, id) => promise.then(() => fn({
|
|
10
|
-
...data, id,
|
|
11
|
-
})), Promise.resolve());
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export async function translateContent({
|
|
15
|
-
pg = pgClients.client, query, user,
|
|
16
|
-
}, reply) {
|
|
17
|
-
if (!query.id) {
|
|
18
|
-
return reply.status(400).send({ error: 'not enough query params: id', code: 400 });
|
|
19
|
-
}
|
|
20
|
-
if (!query.to) {
|
|
21
|
-
return reply.status(400).send({ error: 'not enough query params: to', code: 400 });
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const { id, collection = 'pages', from = 'uk', to, nocache, skip } = query;
|
|
25
|
-
const { id: collectionId, table, columns = [] } = await getCollectionId(collection, pg);
|
|
26
|
-
|
|
27
|
-
if (!collectionId) {
|
|
28
|
-
return reply.status(404).send({ error: 'collection not found', code: 404 });
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const schemaKeys = columns.filter(el => el.name && el.localization).map(el => el.name);
|
|
32
|
-
|
|
33
|
-
if (!schemaKeys.length) {
|
|
34
|
-
return reply.status(400).send({ error: 'collection does not contain any fields with localization enabled', code: 400 });
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const result = await insertContentLocalization({ table, id, from, to, nocache, skip, schemaKeys, user }, pg);
|
|
38
|
-
|
|
39
|
-
if (result?.error) {
|
|
40
|
-
return reply.status(result.code).send(result);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return result;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export async function translateCollection({
|
|
47
|
-
pg = pgClients.client, params, query, user,
|
|
48
|
-
}, reply) {
|
|
49
|
-
if (!query.to) {
|
|
50
|
-
return reply.status(400).send({ error: 'not enough query params: to', code: 400 });
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const { id: collectionId, table, columns = [] } = await getCollectionId(params.id, pg);
|
|
54
|
-
|
|
55
|
-
if (!collectionId) {
|
|
56
|
-
return reply.status(404).send({ error: 'collection not found', code: 404 });
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const schemaKeys = columns.filter(el => el.name && el.localization).map(el => el.name);
|
|
60
|
-
|
|
61
|
-
if (!schemaKeys.length) {
|
|
62
|
-
return reply.status(400).send({ error: 'collection does not contain any fields with localization enabled', code: 400 });
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const { from = 'uk', to, nocache, debug } = query;
|
|
66
|
-
|
|
67
|
-
const send = eventStream(reply);
|
|
68
|
-
|
|
69
|
-
send(`target localization: ${to}`);
|
|
70
|
-
send(`skip existing: ${!nocache}`);
|
|
71
|
-
send('localization enabled for schema keys: ' + schemaKeys.join(','));
|
|
72
|
-
|
|
73
|
-
try {
|
|
74
|
-
const ids = !table
|
|
75
|
-
? await pg.query(`select array_agg(content_id) from site.contents where content_type_id=$1`, [collectionId]).then(el => el.rows?.[0]?.array_agg || [])
|
|
76
|
-
: await pg.query(`select array_agg(id) from data."${table}"`).then(el => el.rows?.[0]?.array_agg || []);
|
|
77
|
-
|
|
78
|
-
const skip = await pg.query('select array_agg(object_id) from site.localization where object_id=any($1::text[]) and split_part(field_key,\':\',2)=$2', [ids, to]).then(el => el.rows?.[0]?.array_agg || []);
|
|
79
|
-
const filteredIds = nocache ? ids : ids.filter(id => !skip.includes(id));
|
|
80
|
-
|
|
81
|
-
send(`target localization already exists for ${ids.length - filteredIds.length}/${ids.length} rows`);
|
|
82
|
-
|
|
83
|
-
await sequence(debug ? filteredIds.slice(0, 1) : filteredIds, { send, pg, from, to, table, nocache: true, skip, schemaKeys, user }, insertContentLocalization);
|
|
84
|
-
return send('translation complete', true);
|
|
85
|
-
} catch (err) {
|
|
86
|
-
return send(err.toString(), true);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
1
|
+
import { pgClients, eventStream } from "@opengis/fastify-table/utils.js";
|
|
2
|
+
|
|
3
|
+
import insertContentLocalization from "../utils/insertContentLocalization.js";
|
|
4
|
+
|
|
5
|
+
const getCollectionId = async (id, pg) => pg.query(`select content_type_id as id, table_name as table, columns FROM site.content_types
|
|
6
|
+
where $1 in (content_type_id, name)`, [id]).then(el => el.rows?.[0] || {});
|
|
7
|
+
|
|
8
|
+
function sequence(arr, data, fn) {
|
|
9
|
+
return arr.reduce((promise, id) => promise.then(() => fn({
|
|
10
|
+
...data, id,
|
|
11
|
+
})), Promise.resolve());
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function translateContent({
|
|
15
|
+
pg = pgClients.client, query, user,
|
|
16
|
+
}, reply) {
|
|
17
|
+
if (!query.id) {
|
|
18
|
+
return reply.status(400).send({ error: 'not enough query params: id', code: 400 });
|
|
19
|
+
}
|
|
20
|
+
if (!query.to) {
|
|
21
|
+
return reply.status(400).send({ error: 'not enough query params: to', code: 400 });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const { id, collection = 'pages', from = 'uk', to, nocache, skip } = query;
|
|
25
|
+
const { id: collectionId, table, columns = [] } = await getCollectionId(collection, pg);
|
|
26
|
+
|
|
27
|
+
if (!collectionId) {
|
|
28
|
+
return reply.status(404).send({ error: 'collection not found', code: 404 });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const schemaKeys = columns.filter(el => el.name && el.localization).map(el => el.name);
|
|
32
|
+
|
|
33
|
+
if (!schemaKeys.length) {
|
|
34
|
+
return reply.status(400).send({ error: 'collection does not contain any fields with localization enabled', code: 400 });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const result = await insertContentLocalization({ table, id, from, to, nocache, skip, schemaKeys, user }, pg);
|
|
38
|
+
|
|
39
|
+
if (result?.error) {
|
|
40
|
+
return reply.status(result.code).send(result);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function translateCollection({
|
|
47
|
+
pg = pgClients.client, params, query, user,
|
|
48
|
+
}, reply) {
|
|
49
|
+
if (!query.to) {
|
|
50
|
+
return reply.status(400).send({ error: 'not enough query params: to', code: 400 });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const { id: collectionId, table, columns = [] } = await getCollectionId(params.id, pg);
|
|
54
|
+
|
|
55
|
+
if (!collectionId) {
|
|
56
|
+
return reply.status(404).send({ error: 'collection not found', code: 404 });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const schemaKeys = columns.filter(el => el.name && el.localization).map(el => el.name);
|
|
60
|
+
|
|
61
|
+
if (!schemaKeys.length) {
|
|
62
|
+
return reply.status(400).send({ error: 'collection does not contain any fields with localization enabled', code: 400 });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const { from = 'uk', to, nocache, debug } = query;
|
|
66
|
+
|
|
67
|
+
const send = eventStream(reply);
|
|
68
|
+
|
|
69
|
+
send(`target localization: ${to}`);
|
|
70
|
+
send(`skip existing: ${!nocache}`);
|
|
71
|
+
send('localization enabled for schema keys: ' + schemaKeys.join(','));
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const ids = !table
|
|
75
|
+
? await pg.query(`select array_agg(content_id) from site.contents where content_type_id=$1`, [collectionId]).then(el => el.rows?.[0]?.array_agg || [])
|
|
76
|
+
: await pg.query(`select array_agg(id) from data."${table}"`).then(el => el.rows?.[0]?.array_agg || []);
|
|
77
|
+
|
|
78
|
+
const skip = await pg.query('select array_agg(object_id) from site.localization where object_id=any($1::text[]) and split_part(field_key,\':\',2)=$2', [ids, to]).then(el => el.rows?.[0]?.array_agg || []);
|
|
79
|
+
const filteredIds = nocache ? ids : ids.filter(id => !skip.includes(id));
|
|
80
|
+
|
|
81
|
+
send(`target localization already exists for ${ids.length - filteredIds.length}/${ids.length} rows`);
|
|
82
|
+
|
|
83
|
+
await sequence(debug ? filteredIds.slice(0, 1) : filteredIds, { send, pg, from, to, table, nocache: true, skip, schemaKeys, user }, insertContentLocalization);
|
|
84
|
+
return send('translation complete', true);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
return send(err.toString(), true);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
90
|
export default null;
|