@opengis/cms 0.0.10 → 0.0.12
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/dist/cms.js +3615 -2931
- package/dist/cms.umd.cjs +13 -13
- package/dist/style.css +1 -0
- package/package.json +5 -5
- package/server/app.js +5 -2
- package/server/migrations/site.sql +428 -0
- package/server/routes/site/controllers/deleteMedia.js +47 -0
- package/server/routes/site/controllers/downloadMedia.js +48 -0
- package/server/routes/site/controllers/getPermissions.js +16 -0
- package/server/routes/site/controllers/listMedia.js +60 -0
- package/server/routes/site/controllers/metadataMedia.js +37 -0
- package/server/routes/site/controllers/setPermissions.js +50 -0
- package/server/routes/site/controllers/uploadMedia.js +66 -0
- package/server/routes/site/index.mjs +34 -0
- package/server/migrations/media.sql +0 -30
package/dist/style.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.draggable-item[data-v-eefd185d]{cursor:move;-webkit-user-select:none;user-select:none;padding:4px;transition:all .2s ease;border-radius:6px;border:2px solid transparent;position:relative;z-index:1}.draggable-item[data-v-eefd185d]:hover{background-color:#f3f4f6}.draggable-item.drag-over[data-v-eefd185d]{background-color:#e0f2fe;border-color:#3b82f6;box-shadow:0 0 0 2px #3b82f633;transform:scale(1.02)}.draggable-item.dragging[data-v-eefd185d]{opacity:.5;background-color:#dbeafe;border-color:#3b82f6}.draggable-item button[data-v-eefd185d]{position:relative;z-index:2}.root-drag-over[data-v-84b80de2]{border-color:#3b82f6;background-color:#dbeafe;box-shadow:0 0 0 4px #3b82f633;transform:scale(1.01)}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengis/cms",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
4
4
|
"description": "cms",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "Softpro",
|
|
@@ -23,10 +23,10 @@
|
|
|
23
23
|
"docs:preview": "vitepress preview docs"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@opengis/fastify-auth": "
|
|
27
|
-
"@opengis/fastify-file": "
|
|
28
|
-
"@opengis/fastify-table": "
|
|
29
|
-
"@opengis/v3-core": "^0.3.
|
|
26
|
+
"@opengis/fastify-auth": "1.0.85",
|
|
27
|
+
"@opengis/fastify-file": "1.0.76",
|
|
28
|
+
"@opengis/fastify-table": "1.3.42",
|
|
29
|
+
"@opengis/v3-core": "^0.3.165",
|
|
30
30
|
"@opengis/v3-filter": "^0.0.74",
|
|
31
31
|
"@vitejs/plugin-vue": "^5.0.4",
|
|
32
32
|
"cross-env": "^7.0.3",
|
package/server/app.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
|
|
2
4
|
import { config, execMigrations } from '@opengis/fastify-table/utils.js';
|
|
3
5
|
|
|
4
6
|
config.prefix = config.prefix || '/api';
|
|
5
7
|
const { prefix } = config;
|
|
6
8
|
|
|
7
|
-
const
|
|
9
|
+
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
10
|
|
|
9
11
|
export default async function (fastify) {
|
|
10
12
|
// core
|
|
@@ -21,5 +23,6 @@ export default async function (fastify) {
|
|
|
21
23
|
fastify.register(import('./routes/category/index.mjs'), { prefix });
|
|
22
24
|
fastify.register(import('./routes/manager/index.mjs'), { prefix });
|
|
23
25
|
fastify.register(import('./routes/media/index.mjs'), { prefix });
|
|
24
|
-
|
|
26
|
+
fastify.register(import('./routes/site/index.mjs'), { prefix });
|
|
27
|
+
execMigrations(path.join(dirname, 'migrations')).catch(err => console.log(err));
|
|
25
28
|
}
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
create schema if not exists admin;
|
|
2
|
+
create table if not exists admin.users (uid text primary key default next_id());
|
|
3
|
+
|
|
4
|
+
create schema if not exists site;
|
|
5
|
+
create table if not exists site.categories (category_id text primary key default next_id());
|
|
6
|
+
|
|
7
|
+
-- drop table if exists site.articles cascade;
|
|
8
|
+
CREATE TABLE if not exists site.articles (
|
|
9
|
+
article_id text PRIMARY KEY default next_id(),
|
|
10
|
+
title VARCHAR (100) NOT NULL,
|
|
11
|
+
slug VARCHAR (50) UNIQUE,
|
|
12
|
+
content TEXT,
|
|
13
|
+
excerpt TEXT,
|
|
14
|
+
status VARCHAR not null DEFAULT 'draft' CHECK (status::text = ANY (ARRAY['draft', 'published', 'archived']::text[])),
|
|
15
|
+
published_at TIMESTAMP,
|
|
16
|
+
is_visible BOOLEAN not null DEFAULT TRUE,
|
|
17
|
+
meta_title VARCHAR,
|
|
18
|
+
meta_description TEXT,
|
|
19
|
+
created_at TIMESTAMP DEFAULT NOW(),
|
|
20
|
+
updated_at TIMESTAMP DEFAULT NOW(),
|
|
21
|
+
created_by text REFERENCES admin.users(uid),
|
|
22
|
+
updated_by text REFERENCES admin.users(uid),
|
|
23
|
+
author_id text REFERENCES admin.users(uid) ON DELETE SET NULL,
|
|
24
|
+
category_id text REFERENCES site.categories(category_id) ON DELETE SET NULL
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
CREATE INDEX if not exists idx_published_articles ON site.articles(published_at) WHERE status = 'published';
|
|
28
|
+
CREATE INDEX if not exists idx_articles_status ON site.articles(status);
|
|
29
|
+
CREATE INDEX if not exists idx_articles_category_id ON site.articles(category_id);
|
|
30
|
+
CREATE INDEX if not exists idx_articles_author_id ON site.articles(author_id);
|
|
31
|
+
CREATE INDEX if not exists idx_articles_is_visible ON site.articles(is_visible);
|
|
32
|
+
CREATE INDEX if not exists idx_articles_published_at ON site.articles(published_at);
|
|
33
|
+
|
|
34
|
+
COMMENT ON TABLE site.articles IS 'Articles for the site, including content, metadata, status, and authorship.';
|
|
35
|
+
COMMENT ON COLUMN site.articles.article_id IS 'Primary key for the article, generated via next_id().';
|
|
36
|
+
COMMENT ON COLUMN site.articles.title IS 'Title of the article.';
|
|
37
|
+
COMMENT ON COLUMN site.articles.slug IS 'URL-friendly unique identifier for the article.';
|
|
38
|
+
COMMENT ON COLUMN site.articles.content IS 'Full body content of the article.';
|
|
39
|
+
COMMENT ON COLUMN site.articles.excerpt IS 'Optional summary or teaser text.';
|
|
40
|
+
COMMENT ON COLUMN site.articles.status IS 'Publication status: draft, published, or archived.';
|
|
41
|
+
COMMENT ON COLUMN site.articles.published_at IS 'Timestamp when the article was published.';
|
|
42
|
+
COMMENT ON COLUMN site.articles.is_visible IS 'Boolean flag indicating if the article is visible on the site.';
|
|
43
|
+
COMMENT ON COLUMN site.articles.meta_title IS 'SEO meta title for the article.';
|
|
44
|
+
COMMENT ON COLUMN site.articles.meta_description IS 'SEO meta description for the article.';
|
|
45
|
+
COMMENT ON COLUMN site.articles.created_at IS 'Timestamp when the article was created.';
|
|
46
|
+
COMMENT ON COLUMN site.articles.updated_at IS 'Timestamp when the article was last updated.';
|
|
47
|
+
COMMENT ON COLUMN site.articles.created_by IS 'User ID of the admin who created the article.';
|
|
48
|
+
COMMENT ON COLUMN site.articles.updated_by IS 'User ID of the admin who last updated the article.';
|
|
49
|
+
COMMENT ON COLUMN site.articles.author_id IS 'User ID of the article''s author.';
|
|
50
|
+
COMMENT ON COLUMN site.articles.category_id IS 'Category ID to which the article belongs.';
|
|
51
|
+
|
|
52
|
+
-- drop table if exists site.media cascade;
|
|
53
|
+
CREATE TABLE if not exists site.media (
|
|
54
|
+
media_id text PRIMARY KEY default next_id(),
|
|
55
|
+
filename TEXT NOT NULL,
|
|
56
|
+
filetype TEXT NOT NULL,
|
|
57
|
+
filesize INTEGER NOT NULL,
|
|
58
|
+
url TEXT,
|
|
59
|
+
description TEXT,
|
|
60
|
+
alt text,
|
|
61
|
+
mime VARCHAR(100),
|
|
62
|
+
preview_url TEXT,
|
|
63
|
+
created_at TIMESTAMP DEFAULT NOW(),
|
|
64
|
+
updated_at TIMESTAMP DEFAULT NOW(),
|
|
65
|
+
created_by text REFERENCES admin.users(uid),
|
|
66
|
+
updated_by text REFERENCES admin.users(uid)
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
COMMENT ON TABLE site.media IS 'Stores uploaded media files with metadata, URLs, and audit info.';
|
|
70
|
+
COMMENT ON COLUMN site.media.media_id IS 'Unique ID for the media item.';
|
|
71
|
+
COMMENT ON COLUMN site.media.filename IS 'Original name of the uploaded file.';
|
|
72
|
+
COMMENT ON COLUMN site.media.filetype IS 'Logical type of file (e.g., image, video, document).';
|
|
73
|
+
COMMENT ON COLUMN site.media.filesize IS 'Size of the file in bytes.';
|
|
74
|
+
COMMENT ON COLUMN site.media.url IS 'URL where the file can be accessed.';
|
|
75
|
+
COMMENT ON COLUMN site.media.description IS 'Optional description or caption.';
|
|
76
|
+
COMMENT ON COLUMN site.media.alt IS 'Alternative text for screen readers or image fallbacks.';
|
|
77
|
+
COMMENT ON COLUMN site.media.mime IS 'MIME type indicating the file format (e.g., image/png).';
|
|
78
|
+
COMMENT ON COLUMN site.media.preview_url IS 'Optional preview or thumbnail URL.';
|
|
79
|
+
COMMENT ON COLUMN site.media.created_at IS 'Timestamp of when the media record was created.';
|
|
80
|
+
COMMENT ON COLUMN site.media.updated_at IS 'Timestamp of the last update to the media record.';
|
|
81
|
+
COMMENT ON COLUMN site.media.created_by IS 'User ID who uploaded the media.';
|
|
82
|
+
COMMENT ON COLUMN site.media.updated_by IS 'User ID who last updated the media.';
|
|
83
|
+
|
|
84
|
+
CREATE INDEX if not exists idx_media_filetype ON site.media(filetype);
|
|
85
|
+
CREATE INDEX if not exists idx_media_filesize ON site.media(filesize);
|
|
86
|
+
CREATE INDEX if not exists idx_media_mime ON site.media(mime);
|
|
87
|
+
CREATE INDEX if not exists idx_media_created_at ON site.media(created_at);
|
|
88
|
+
CREATE INDEX if not exists idx_media_created_by ON site.media(created_by);
|
|
89
|
+
CREATE INDEX if not exists idx_media_updated_at ON site.media(updated_at);
|
|
90
|
+
CREATE INDEX if not exists idx_media_updated_by ON site.media(updated_by);
|
|
91
|
+
|
|
92
|
+
-- drop table if exists site.article_media cascade;
|
|
93
|
+
CREATE TABLE if not exists site.article_media (
|
|
94
|
+
article_id text not null REFERENCES site.articles(article_id) ON DELETE CASCADE,
|
|
95
|
+
media_id text not null REFERENCES site.media(media_id) ON DELETE CASCADE,
|
|
96
|
+
related_id text NOT NULL,
|
|
97
|
+
related_type VARCHAR(50) NOT NULL,
|
|
98
|
+
field VARCHAR(50) NOT NULL,
|
|
99
|
+
order_index INTEGER DEFAULT 0,
|
|
100
|
+
PRIMARY KEY (article_id, media_id, field)
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
COMMENT ON TABLE site.article_media IS 'Join table for associating media with articles, supporting multiple fields and order.';
|
|
104
|
+
COMMENT ON COLUMN site.article_media.article_id IS 'Foreign key to the article that uses the media.';
|
|
105
|
+
COMMENT ON COLUMN site.article_media.media_id IS 'Foreign key to the media file linked to the article.';
|
|
106
|
+
COMMENT ON COLUMN site.article_media.related_id IS 'ID of the related entity (for polymorphic associations).';
|
|
107
|
+
COMMENT ON COLUMN site.article_media.related_type IS 'Type of the related entity (e.g., article, gallery).';
|
|
108
|
+
COMMENT ON COLUMN site.article_media.field IS 'Field name that defines the role of media (e.g., cover, inline).';
|
|
109
|
+
COMMENT ON COLUMN site.article_media.order_index IS 'Ordering index for multiple media in the same field.';
|
|
110
|
+
|
|
111
|
+
CREATE INDEX if not exists idx_article_media_related ON site.article_media(related_type, related_id);
|
|
112
|
+
CREATE INDEX if not exists idx_article_media_ordering ON site.article_media(article_id, field, order_index);
|
|
113
|
+
|
|
114
|
+
-- drop table if exists site.content_types cascade;
|
|
115
|
+
CREATE TABLE if not exists site.content_types (
|
|
116
|
+
content_type_id text PRIMARY KEY default next_id(),
|
|
117
|
+
name VARCHAR (50) NOT NULL UNIQUE,
|
|
118
|
+
display_name VARCHAR (50) NOT NULL,
|
|
119
|
+
table_name VARCHAR(50) NOT NULL,
|
|
120
|
+
status VARCHAR (20) DEFAULT 'draft' CHECK (status::text = ANY (ARRAY['draft', 'published', 'archived']::text[])),
|
|
121
|
+
visible BOOLEAN DEFAULT TRUE,
|
|
122
|
+
localized BOOLEAN DEFAULT FALSE,
|
|
123
|
+
type VARCHAR(20) NOT NULL DEFAULT 'collection' CHECK (type::text = ANY (ARRAY['collection', 'single']::text[])),
|
|
124
|
+
schema JSONB,
|
|
125
|
+
description TEXT,
|
|
126
|
+
icon VARCHAR (50),
|
|
127
|
+
color VARCHAR (20),
|
|
128
|
+
created_at TIMESTAMP DEFAULT NOW(),
|
|
129
|
+
updated_at TIMESTAMP DEFAULT NOW(),
|
|
130
|
+
created_by text REFERENCES admin.users(uid),
|
|
131
|
+
updated_by text REFERENCES admin.users(uid)
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
COMMENT ON TABLE site.content_types IS 'Defines reusable content structures for dynamic content management.';
|
|
135
|
+
COMMENT ON COLUMN site.content_types.content_type_id IS 'Unique identifier for the content type.';
|
|
136
|
+
COMMENT ON COLUMN site.content_types.name IS 'Internal name used in the system (must be unique).';
|
|
137
|
+
COMMENT ON COLUMN site.content_types.display_name IS 'Human-readable label shown in UI.';
|
|
138
|
+
COMMENT ON COLUMN site.content_types.table_name IS 'Physical table where entries of this type are stored.';
|
|
139
|
+
COMMENT ON COLUMN site.content_types.status IS 'Status of the content type (draft, published, archived).';
|
|
140
|
+
COMMENT ON COLUMN site.content_types.visible IS 'Controls whether this content type appears in admin UI.';
|
|
141
|
+
COMMENT ON COLUMN site.content_types.localized IS 'Indicates whether the content type supports localization.';
|
|
142
|
+
COMMENT ON COLUMN site.content_types.type IS 'Whether this content type is a collection or a single instance.';
|
|
143
|
+
COMMENT ON COLUMN site.content_types.schema IS 'JSON schema defining the structure and fields of this content type.';
|
|
144
|
+
COMMENT ON COLUMN site.content_types.description IS 'Optional description of the content type.';
|
|
145
|
+
COMMENT ON COLUMN site.content_types.icon IS 'UI icon identifier for this type.';
|
|
146
|
+
COMMENT ON COLUMN site.content_types.color IS 'UI color code or name for this type.';
|
|
147
|
+
COMMENT ON COLUMN site.content_types.created_at IS 'Timestamp when the content type was created.';
|
|
148
|
+
COMMENT ON COLUMN site.content_types.updated_at IS 'Timestamp when the content type was last modified.';
|
|
149
|
+
COMMENT ON COLUMN site.content_types.created_by IS 'Admin user who created the content type.';
|
|
150
|
+
COMMENT ON COLUMN site.content_types.updated_by IS 'Admin user who last updated the content type.';
|
|
151
|
+
|
|
152
|
+
CREATE INDEX if not exists idx_content_types_status ON site.content_types(status);
|
|
153
|
+
CREATE INDEX if not exists idx_content_types_visible ON site.content_types(visible);
|
|
154
|
+
CREATE INDEX if not exists idx_content_types_type ON site.content_types(type);
|
|
155
|
+
CREATE INDEX if not exists idx_content_types_table_name ON site.content_types(table_name);
|
|
156
|
+
|
|
157
|
+
-- drop table if exists site.content_attributes cascade;
|
|
158
|
+
CREATE TABLE if not exists site.content_attributes (
|
|
159
|
+
content_attribute_id text PRIMARY KEY default next_id(),
|
|
160
|
+
content_type_id text REFERENCES site.content_types(content_type_id) ON DELETE CASCADE,
|
|
161
|
+
name VARCHAR NOT NULL,
|
|
162
|
+
type VARCHAR NOT NULL,
|
|
163
|
+
is_required BOOLEAN DEFAULT FALSE,
|
|
164
|
+
is_unique BOOLEAN DEFAULT FALSE,
|
|
165
|
+
minLength numeric,
|
|
166
|
+
maxLength numeric,
|
|
167
|
+
private BOOLEAN DEFAULT FALSE,
|
|
168
|
+
min numeric,
|
|
169
|
+
max numeric,
|
|
170
|
+
relation_type VARCHAR (20),
|
|
171
|
+
related_to VARCHAR(50),
|
|
172
|
+
default_value TEXT,
|
|
173
|
+
options JSONB,
|
|
174
|
+
enum text[],
|
|
175
|
+
created_at TIMESTAMP DEFAULT NOW(),
|
|
176
|
+
updated_at TIMESTAMP DEFAULT NOW(),
|
|
177
|
+
created_by text REFERENCES admin.users(uid),
|
|
178
|
+
updated_by text REFERENCES admin.users(uid)
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
COMMENT ON TABLE site.content_attributes IS 'Defines fields (attributes) for a dynamic content type schema.';
|
|
182
|
+
COMMENT ON COLUMN site.content_attributes.content_attribute_id IS 'Primary key for the content attribute.';
|
|
183
|
+
COMMENT ON COLUMN site.content_attributes.content_type_id IS 'Foreign key to the content type this attribute belongs to.';
|
|
184
|
+
COMMENT ON COLUMN site.content_attributes.name IS 'Machine name of the attribute.';
|
|
185
|
+
COMMENT ON COLUMN site.content_attributes.type IS 'Data type of the attribute (e.g., text, number, boolean).';
|
|
186
|
+
COMMENT ON COLUMN site.content_attributes.is_required IS 'Whether this field must be filled.';
|
|
187
|
+
COMMENT ON COLUMN site.content_attributes.is_unique IS 'Whether values for this field must be unique.';
|
|
188
|
+
COMMENT ON COLUMN site.content_attributes.minLength IS 'Minimum length constraint (for strings).';
|
|
189
|
+
COMMENT ON COLUMN site.content_attributes.maxLength IS 'Maximum length constraint (for strings).';
|
|
190
|
+
COMMENT ON COLUMN site.content_attributes.private IS 'Whether this field is hidden in public APIs.';
|
|
191
|
+
COMMENT ON COLUMN site.content_attributes.min IS 'Minimum numeric value (if applicable).';
|
|
192
|
+
COMMENT ON COLUMN site.content_attributes.max IS 'Maximum numeric value (if applicable).';
|
|
193
|
+
COMMENT ON COLUMN site.content_attributes.relation_type IS 'Type of relation, if any (e.g., one-to-many).';
|
|
194
|
+
COMMENT ON COLUMN site.content_attributes.related_to IS 'Target entity/content type this field relates to.';
|
|
195
|
+
COMMENT ON COLUMN site.content_attributes.default_value IS 'Default value assigned to this field.';
|
|
196
|
+
COMMENT ON COLUMN site.content_attributes.options IS 'Custom configuration or metadata in JSON format.';
|
|
197
|
+
COMMENT ON COLUMN site.content_attributes.enum IS 'List of allowed enum values for this field.';
|
|
198
|
+
COMMENT ON COLUMN site.content_attributes.created_at IS 'Timestamp when this attribute was created.';
|
|
199
|
+
COMMENT ON COLUMN site.content_attributes.updated_at IS 'Timestamp when this attribute was last updated.';
|
|
200
|
+
COMMENT ON COLUMN site.content_attributes.created_by IS 'Admin user who created the content type.';
|
|
201
|
+
COMMENT ON COLUMN site.content_attributes.updated_by IS 'Admin user who last updated the content type.';
|
|
202
|
+
|
|
203
|
+
CREATE INDEX if not exists idx_attributes_by_type ON site.content_attributes(content_type_id);
|
|
204
|
+
CREATE INDEX if not exists idx_attributes_name ON site.content_attributes(name);
|
|
205
|
+
CREATE INDEX if not exists idx_attributes_type ON site.content_attributes(type);
|
|
206
|
+
|
|
207
|
+
-- drop table if exists site.article_translations cascade;
|
|
208
|
+
CREATE TABLE if not exists site.article_translations (
|
|
209
|
+
article_translation_id text PRIMARY KEY default next_id(),
|
|
210
|
+
article_id text REFERENCES site.articles(article_id) ON DELETE CASCADE,
|
|
211
|
+
locale VARCHAR(10) NOT NULL,
|
|
212
|
+
title VARCHAR (100) NOT NULL,
|
|
213
|
+
content TEXT,
|
|
214
|
+
excerpt TEXT,
|
|
215
|
+
meta_title VARCHAR (100),
|
|
216
|
+
meta_description TEXT,
|
|
217
|
+
created_at TIMESTAMP DEFAULT NOW(),
|
|
218
|
+
updated_at TIMESTAMP DEFAULT NOW(),
|
|
219
|
+
created_by text REFERENCES admin.users(uid),
|
|
220
|
+
updated_by text REFERENCES admin.users(uid),
|
|
221
|
+
UNIQUE (article_id, locale)
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
COMMENT ON TABLE site.article_translations IS 'Stores translated content for articles across different locales.';
|
|
225
|
+
COMMENT ON COLUMN site.article_translations.article_translation_id IS 'Primary key for the article translation.';
|
|
226
|
+
COMMENT ON COLUMN site.article_translations.article_id IS 'Foreign key linking to the main article.';
|
|
227
|
+
COMMENT ON COLUMN site.article_translations.locale IS 'Locale code for the translation (e.g., en, fr).';
|
|
228
|
+
COMMENT ON COLUMN site.article_translations.title IS 'Translated title of the article.';
|
|
229
|
+
COMMENT ON COLUMN site.article_translations.content IS 'Translated content body.';
|
|
230
|
+
COMMENT ON COLUMN site.article_translations.excerpt IS 'Translated excerpt or teaser text.';
|
|
231
|
+
COMMENT ON COLUMN site.article_translations.meta_title IS 'Translated SEO meta title.';
|
|
232
|
+
COMMENT ON COLUMN site.article_translations.meta_description IS 'Translated SEO meta description.';
|
|
233
|
+
COMMENT ON COLUMN site.article_translations.created_at IS 'Timestamp of when this translation was created.';
|
|
234
|
+
COMMENT ON COLUMN site.article_translations.updated_at IS 'Timestamp of when this translation was last updated.';
|
|
235
|
+
COMMENT ON COLUMN site.article_translations.created_by IS 'Admin user who created this translation.';
|
|
236
|
+
COMMENT ON COLUMN site.article_translations.updated_by IS 'Admin user who last updated this translation.';
|
|
237
|
+
|
|
238
|
+
CREATE INDEX if not exists idx_article_translations_article_id ON site.article_translations(article_id);
|
|
239
|
+
CREATE INDEX if not exists idx_article_translations_locale ON site.article_translations(locale);
|
|
240
|
+
|
|
241
|
+
-- drop table if exists site.single_type_values cascade;
|
|
242
|
+
CREATE TABLE if not exists site.single_type_values (
|
|
243
|
+
single_type_value_id text PRIMARY KEY default next_id(),
|
|
244
|
+
type_name VARCHAR(100) NOT NULL,
|
|
245
|
+
key VARCHAR(255) NOT NULL,
|
|
246
|
+
value TEXT,
|
|
247
|
+
value_type VARCHAR(50),
|
|
248
|
+
created_at TIMESTAMP DEFAULT NOW(),
|
|
249
|
+
updated_at TIMESTAMP DEFAULT NOW(),
|
|
250
|
+
created_by text REFERENCES admin.users(uid),
|
|
251
|
+
updated_by text REFERENCES admin.users(uid),
|
|
252
|
+
UNIQUE(type_name, key)
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
COMMENT ON TABLE site.single_type_values IS 'Stores key-value pairs for single-type settings or configurations.';
|
|
256
|
+
COMMENT ON COLUMN site.single_type_values.single_type_value_id IS 'Primary key for the single type value entry.';
|
|
257
|
+
COMMENT ON COLUMN site.single_type_values.type_name IS 'Type of the setting or configuration.';
|
|
258
|
+
COMMENT ON COLUMN site.single_type_values.key IS 'Unique key for the setting or configuration value.';
|
|
259
|
+
COMMENT ON COLUMN site.single_type_values.value IS 'The value associated with the key.';
|
|
260
|
+
COMMENT ON COLUMN site.single_type_values.value_type IS 'Type of value (e.g., string, integer, boolean).';
|
|
261
|
+
COMMENT ON COLUMN site.single_type_values.created_at IS 'Timestamp when the value entry was created.';
|
|
262
|
+
COMMENT ON COLUMN site.single_type_values.updated_at IS 'Timestamp when the value entry was last updated.';
|
|
263
|
+
COMMENT ON COLUMN site.single_type_values.created_by IS 'User who created the value entry.';
|
|
264
|
+
COMMENT ON COLUMN site.single_type_values.updated_by IS 'User who last updated the value entry.';
|
|
265
|
+
|
|
266
|
+
CREATE INDEX if not exists idx_single_type_values_type_name ON site.single_type_values(type_name);
|
|
267
|
+
CREATE INDEX if not exists idx_single_type_values_key ON site.single_type_values(key);
|
|
268
|
+
|
|
269
|
+
-- drop table if exists site.settings cascade;
|
|
270
|
+
CREATE TABLE if not exists site.settings (
|
|
271
|
+
id serial PRIMARY KEY CHECK (id = 1),
|
|
272
|
+
key VARCHAR (50) NOT NULL UNIQUE,
|
|
273
|
+
value TEXT NOT NULL,
|
|
274
|
+
created_at TIMESTAMP DEFAULT NOW(),
|
|
275
|
+
updated_at TIMESTAMP DEFAULT NOW(),
|
|
276
|
+
created_by text REFERENCES admin.users(uid),
|
|
277
|
+
updated_by text REFERENCES admin.users(uid)
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
COMMENT ON TABLE site.settings IS 'Stores global settings for the site, with only one record allowed.';
|
|
281
|
+
COMMENT ON COLUMN site.settings.id IS 'Unique ID for the settings table (always 1).';
|
|
282
|
+
COMMENT ON COLUMN site.settings.key IS 'Unique key for the setting (e.g., "site_title").';
|
|
283
|
+
COMMENT ON COLUMN site.settings.value IS 'The value associated with the key (e.g., "My Site Title").';
|
|
284
|
+
COMMENT ON COLUMN site.settings.created_at IS 'Timestamp when the setting was created.';
|
|
285
|
+
COMMENT ON COLUMN site.settings.updated_at IS 'Timestamp when the setting was last updated.';
|
|
286
|
+
COMMENT ON COLUMN site.settings.created_by IS 'User who created the setting record.';
|
|
287
|
+
COMMENT ON COLUMN site.settings.updated_by IS 'User who last updated the setting record.';
|
|
288
|
+
|
|
289
|
+
CREATE INDEX if not exists idx_settings_key ON site.settings(key);
|
|
290
|
+
|
|
291
|
+
-- drop table if exists site.menus cascade;
|
|
292
|
+
CREATE TABLE if not exists site.menus (
|
|
293
|
+
menu_id text PRIMARY KEY default next_id(),
|
|
294
|
+
name VARCHAR(100) UNIQUE NOT NULL,
|
|
295
|
+
slug VARCHAR(100) UNIQUE NOT NULL,
|
|
296
|
+
description TEXT,
|
|
297
|
+
created_at TIMESTAMP DEFAULT NOW(),
|
|
298
|
+
updated_at TIMESTAMP DEFAULT NOW(),
|
|
299
|
+
created_by text REFERENCES admin.users(uid),
|
|
300
|
+
updated_by text REFERENCES admin.users(uid)
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
COMMENT ON TABLE site.menus IS 'Stores menu information with a name, slug, description, and audit fields.';
|
|
304
|
+
COMMENT ON COLUMN site.menus.menu_id IS 'Primary key for the menu (auto-generated).';
|
|
305
|
+
COMMENT ON COLUMN site.menus.name IS 'Unique name for the menu.';
|
|
306
|
+
COMMENT ON COLUMN site.menus.slug IS 'Unique slug for the menu, used for URLs or other references.';
|
|
307
|
+
COMMENT ON COLUMN site.menus.description IS 'Optional description of the menu.';
|
|
308
|
+
COMMENT ON COLUMN site.menus.created_at IS 'Timestamp when the menu was created.';
|
|
309
|
+
COMMENT ON COLUMN site.menus.updated_at IS 'Timestamp when the menu was last updated.';
|
|
310
|
+
COMMENT ON COLUMN site.menus.created_by IS 'Admin user who created the menu.';
|
|
311
|
+
COMMENT ON COLUMN site.menus.updated_by IS 'Admin user who last updated the menu.';
|
|
312
|
+
|
|
313
|
+
CREATE INDEX if not exists idx_menus_name ON site.menus(name);
|
|
314
|
+
CREATE INDEX if not exists idx_menus_slug ON site.menus(slug);
|
|
315
|
+
|
|
316
|
+
-- drop table if exists site.menu_items cascade;
|
|
317
|
+
CREATE TABLE if not exists site.menu_items (
|
|
318
|
+
menu_item_id text PRIMARY KEY default next_id(),
|
|
319
|
+
menu_id text NOT NULL REFERENCES site.menus(menu_id) ON DELETE CASCADE,
|
|
320
|
+
parent_id text REFERENCES site.menu_items(menu_item_id) ON DELETE CASCADE,
|
|
321
|
+
title VARCHAR(255) NOT NULL,
|
|
322
|
+
url TEXT,
|
|
323
|
+
order_index INTEGER DEFAULT 0,
|
|
324
|
+
target VARCHAR(20),
|
|
325
|
+
icon VARCHAR(100),
|
|
326
|
+
created_at TIMESTAMP DEFAULT NOW(),
|
|
327
|
+
updated_at TIMESTAMP DEFAULT NOW(),
|
|
328
|
+
created_by text REFERENCES admin.users(uid),
|
|
329
|
+
updated_by text REFERENCES admin.users(uid)
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
COMMENT ON TABLE site.menu_items IS 'Stores items within menus, supporting hierarchical (nested) structures.';
|
|
333
|
+
COMMENT ON COLUMN site.menu_items.menu_item_id IS 'Primary key for the menu item (auto-generated).';
|
|
334
|
+
COMMENT ON COLUMN site.menu_items.menu_id IS 'Foreign key linking to the associated menu.';
|
|
335
|
+
COMMENT ON COLUMN site.menu_items.parent_id IS 'Foreign key linking to the parent menu item (for nested structure).';
|
|
336
|
+
COMMENT ON COLUMN site.menu_items.title IS 'Title of the menu item.';
|
|
337
|
+
COMMENT ON COLUMN site.menu_items.url IS 'URL or path for the menu item.';
|
|
338
|
+
COMMENT ON COLUMN site.menu_items.order_index IS 'Index used for ordering the menu items.';
|
|
339
|
+
COMMENT ON COLUMN site.menu_items.target IS 'Target attribute for the menu link (e.g., "_blank" for a new tab).';
|
|
340
|
+
COMMENT ON COLUMN site.menu_items.icon IS 'Icon associated with the menu item.';
|
|
341
|
+
COMMENT ON COLUMN site.menu_items.created_at IS 'Timestamp when the menu item was created.';
|
|
342
|
+
COMMENT ON COLUMN site.menu_items.updated_at IS 'Timestamp when the menu item was last updated.';
|
|
343
|
+
COMMENT ON COLUMN site.menu_items.created_by IS 'User who created the menu item.';
|
|
344
|
+
COMMENT ON COLUMN site.menu_items.updated_by IS 'User who last updated the menu item.';
|
|
345
|
+
|
|
346
|
+
CREATE INDEX if not exists idx_menu_items_menu_id ON site.menu_items(menu_id);
|
|
347
|
+
CREATE INDEX if not exists idx_menu_items_parent_id ON site.menu_items(parent_id);
|
|
348
|
+
CREATE INDEX if not exists idx_menu_items_order_index ON site.menu_items(order_index);
|
|
349
|
+
|
|
350
|
+
-- drop table if exists site.roles cascade;
|
|
351
|
+
CREATE TABLE if not exists site.roles (
|
|
352
|
+
role_id text PRIMARY KEY default next_id(),
|
|
353
|
+
name VARCHAR(100) UNIQUE NOT NULL,
|
|
354
|
+
description TEXT,
|
|
355
|
+
created_at TIMESTAMP DEFAULT NOW(),
|
|
356
|
+
updated_at TIMESTAMP DEFAULT NOW(),
|
|
357
|
+
created_by text REFERENCES admin.users(uid),
|
|
358
|
+
updated_by text REFERENCES admin.users(uid)
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
COMMENT ON TABLE site.roles IS 'Stores information about user roles, which can be used for access control and permissions.';
|
|
362
|
+
COMMENT ON COLUMN site.roles.role_id IS 'Primary key for the role (auto-generated).';
|
|
363
|
+
COMMENT ON COLUMN site.roles.name IS 'Unique name of the role (e.g., "admin", "editor").';
|
|
364
|
+
COMMENT ON COLUMN site.roles.description IS 'Description of the role and its permissions.';
|
|
365
|
+
COMMENT ON COLUMN site.roles.created_at IS 'Timestamp when the role was created.';
|
|
366
|
+
COMMENT ON COLUMN site.roles.updated_at IS 'Timestamp when the role was last updated.';
|
|
367
|
+
COMMENT ON COLUMN site.roles.created_by IS 'User who created the role.';
|
|
368
|
+
COMMENT ON COLUMN site.roles.updated_by IS 'User who last updated the role.';
|
|
369
|
+
|
|
370
|
+
CREATE INDEX if not exists idx_roles_name ON site.roles(name);
|
|
371
|
+
|
|
372
|
+
-- drop table if exists site.permissions cascade;
|
|
373
|
+
CREATE TABLE if not exists site.permissions (
|
|
374
|
+
permission_id text PRIMARY KEY default next_id(),
|
|
375
|
+
role_id text references site.roles(role_id),
|
|
376
|
+
user_id text references admin.users(uid),
|
|
377
|
+
content_type_id text references site.content_types(content_type_id),
|
|
378
|
+
subject VARCHAR(100) NOT NULL,
|
|
379
|
+
actions TEXT[] NOT NULL check (actions && (ARRAY['read', 'create', 'delete', 'edit']::text[])),
|
|
380
|
+
description TEXT,
|
|
381
|
+
created_at TIMESTAMP DEFAULT NOW(),
|
|
382
|
+
updated_at TIMESTAMP DEFAULT NOW(),
|
|
383
|
+
created_by text REFERENCES admin.users(uid),
|
|
384
|
+
updated_by text REFERENCES admin.users(uid)
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
COMMENT ON TABLE site.permissions IS 'Stores permissions assigned to roles or users, defining actions for specific subjects and content types.';
|
|
389
|
+
COMMENT ON COLUMN site.permissions.permission_id IS 'Primary key for the permission (auto-generated).';
|
|
390
|
+
COMMENT ON COLUMN site.permissions.role_id IS 'Foreign key linking to the role the permission is assigned to.';
|
|
391
|
+
COMMENT ON COLUMN site.permissions.user_id IS 'Foreign key linking to the user who has the permission.';
|
|
392
|
+
COMMENT ON COLUMN site.permissions.content_type_id IS 'Foreign key linking to the content type for which the permission applies.';
|
|
393
|
+
COMMENT ON COLUMN site.permissions.subject IS 'Subject (e.g., "post", "comment") of the permission.';
|
|
394
|
+
COMMENT ON COLUMN site.permissions.actions IS 'Array of actions allowed for the permission (e.g., ["create", "update"]).';
|
|
395
|
+
COMMENT ON COLUMN site.permissions.description IS 'Description of the permission.';
|
|
396
|
+
COMMENT ON COLUMN site.permissions.created_at IS 'Timestamp when the permission was created.';
|
|
397
|
+
COMMENT ON COLUMN site.permissions.updated_at IS 'Timestamp when the permission was last updated.';
|
|
398
|
+
COMMENT ON COLUMN site.permissions.created_by IS 'User who created the permission record.';
|
|
399
|
+
COMMENT ON COLUMN site.permissions.updated_by IS 'User who last updated the permission record.';
|
|
400
|
+
|
|
401
|
+
CREATE INDEX if not exists idx_permissions_role_id ON site.permissions(role_id);
|
|
402
|
+
CREATE INDEX if not exists idx_permissions_user_id ON site.permissions(user_id);
|
|
403
|
+
CREATE INDEX if not exists idx_permissions_content_type_id ON site.permissions(content_type_id);
|
|
404
|
+
CREATE INDEX if not exists idx_permissions_subject ON site.permissions(subject);
|
|
405
|
+
|
|
406
|
+
-- DROP FUNCTION site.getmenu(text);
|
|
407
|
+
CREATE OR REPLACE FUNCTION site.getMenu(_name text)
|
|
408
|
+
RETURNS json AS
|
|
409
|
+
$BODY$
|
|
410
|
+
DECLARE
|
|
411
|
+
|
|
412
|
+
BEGIN
|
|
413
|
+
|
|
414
|
+
return (
|
|
415
|
+
WITH RECURSIVE menu_tree AS (
|
|
416
|
+
SELECT menu_item_id as id, parent_id, title, url, 0 AS level
|
|
417
|
+
FROM site.menu_items
|
|
418
|
+
WHERE parent_id IS NULL AND title = _name
|
|
419
|
+
UNION ALL
|
|
420
|
+
SELECT mi.menu_item_id, mi.parent_id, mi.title, mi.url, level + 1
|
|
421
|
+
FROM site.menu_items mi
|
|
422
|
+
INNER JOIN menu_tree mt ON mi.parent_id = mt.id
|
|
423
|
+
)
|
|
424
|
+
SELECT json_agg(row_to_json(menu_tree.*)) from menu_tree --ORDER BY level, id
|
|
425
|
+
);
|
|
426
|
+
END;
|
|
427
|
+
$BODY$
|
|
428
|
+
LANGUAGE plpgsql VOLATILE COST 100;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { rm } from 'node:fs/promises';
|
|
4
|
+
|
|
5
|
+
import { config, dataDelete, getFolder, pgClients } from "@opengis/fastify-table/utils.js";
|
|
6
|
+
|
|
7
|
+
const rootDir = getFolder(config, 'local');
|
|
8
|
+
|
|
9
|
+
export default async function deleteMedia({
|
|
10
|
+
pg = pgClients.client, params = {}, user = {}, method = 'DELETE',
|
|
11
|
+
}, reply) {
|
|
12
|
+
if (!config.debug && method !== 'DELETE') {
|
|
13
|
+
return reply.status(403).send('access restricted');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (!params?.id) {
|
|
17
|
+
return reply.status(400).send('not enough params: id');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!pg.pk?.['site.media']) {
|
|
21
|
+
return reply.status(404).send('table not found');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const { url: relpath, id } = await pg.query(
|
|
25
|
+
'select media_id as id, url from site.media where media_id = $1 and url is not null',
|
|
26
|
+
[params.id],
|
|
27
|
+
).then(el => el.rows?.[0] || {});
|
|
28
|
+
|
|
29
|
+
if (!id) {
|
|
30
|
+
return reply.status(404).send('media not found: ' + params.id);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const res = await dataDelete({
|
|
34
|
+
pg,
|
|
35
|
+
id,
|
|
36
|
+
table: 'site.media',
|
|
37
|
+
uid: user?.uid || 0,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const filepath = path.join(rootDir, relpath);
|
|
41
|
+
|
|
42
|
+
if (existsSync(filepath)) {
|
|
43
|
+
await rm(filepath, { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return { id, ...res || {} };
|
|
47
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
|
|
5
|
+
import { config, getFolder, pgClients } from "@opengis/fastify-table/utils.js";
|
|
6
|
+
|
|
7
|
+
const rootDir = getFolder(config, 'local');
|
|
8
|
+
|
|
9
|
+
export default async function downloadMedia({
|
|
10
|
+
pg = pgClients.client, params = {}, query = {},
|
|
11
|
+
}, reply) {
|
|
12
|
+
if (!params?.id) {
|
|
13
|
+
return reply.status(400).send('not enough params: id');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (!pg.pk?.['site.media']) {
|
|
17
|
+
return reply.status(404).send('table not found');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const { filename, mime, id, url: relpath } = await pg.query(
|
|
21
|
+
'select media_id as id, filename, mime, url from site.media where media_id = $1 and url is not null',
|
|
22
|
+
[params.id],
|
|
23
|
+
).then(el => el.rows?.[0] || {});
|
|
24
|
+
|
|
25
|
+
if (!id) {
|
|
26
|
+
return reply.status(404).send('media not found: ' + params.id);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const filepath = path.join(rootDir, relpath);
|
|
30
|
+
|
|
31
|
+
if (!existsSync(filepath)) {
|
|
32
|
+
return reply.status(404).send('file not found');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const buffer = await readFile(filepath, { buffer: true });
|
|
36
|
+
|
|
37
|
+
// skip xml load for preview
|
|
38
|
+
if (query.preview && path.extname(filename) !== '.xml') {
|
|
39
|
+
return reply.headers({ 'Content-Type': mime }).send(buffer);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return reply
|
|
43
|
+
.headers({
|
|
44
|
+
'Content-Type': mime,
|
|
45
|
+
'Content-Disposition': `attachment; filename=${filename || path.basename(filepath)}`,
|
|
46
|
+
})
|
|
47
|
+
.send(buffer);
|
|
48
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { pgClients } from '@opengis/fastify-table/utils.js';
|
|
2
|
+
|
|
3
|
+
export default async function getPermissions(req, reply) {
|
|
4
|
+
const { pg = pgClients.client, params = {}, user = {} } = req;
|
|
5
|
+
|
|
6
|
+
if (!user?.uid) {
|
|
7
|
+
return reply.status(401).send('unauthorized');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { rows = [] } = await pg.query(
|
|
11
|
+
`select * from site.permissions where ${params.id ? 'user_id=$1' : 'true'}`,
|
|
12
|
+
[params.id].filter(Boolean),
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
return { permissions: rows };
|
|
16
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
3
|
+
import { readdir, stat } from 'node:fs/promises';
|
|
4
|
+
|
|
5
|
+
import { config, getFolder, pgClients } from '@opengis/fastify-table/utils.js';
|
|
6
|
+
|
|
7
|
+
const rootDir = getFolder(config, 'local');
|
|
8
|
+
mkdirSync(path.join(rootDir, 'media'), { recursive: true });
|
|
9
|
+
|
|
10
|
+
export default async function listMedia(req, reply) {
|
|
11
|
+
const { pg = pgClients.client, query = {} } = req;
|
|
12
|
+
const { subdir = '' } = query;
|
|
13
|
+
|
|
14
|
+
if (!pg.pk?.['site.media']) {
|
|
15
|
+
return reply.status(404).send('table not found');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (typeof subdir !== 'string' || subdir.includes('..')) {
|
|
19
|
+
return reply.status(403).send('invalid params: subdir');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const relpath = path.join('/media', subdir).replace(/\\/g, '/');
|
|
23
|
+
const dirpath = path.join(rootDir, relpath).replace(/\\/g, '/');
|
|
24
|
+
|
|
25
|
+
if (!existsSync(dirpath)) {
|
|
26
|
+
return { data: [], relpath, msg: 'directory not exists' };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const { rows = [] } = await pg.query(
|
|
30
|
+
`select
|
|
31
|
+
media_id as id, filename, filetype, filesize, url, description, alt,
|
|
32
|
+
mime, preview_url, created_at, updated_at, created_by, updated_by
|
|
33
|
+
from site.media
|
|
34
|
+
where url ~ $1`,
|
|
35
|
+
[relpath],
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
if (!rows.length && false) {
|
|
39
|
+
return { data: [], relpath, msg: 'empty rows' };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const isDirectory = (await stat(dirpath)).isDirectory();
|
|
43
|
+
|
|
44
|
+
const items = isDirectory ? await readdir(dirpath, { withFileTypes: true }) : [];
|
|
45
|
+
|
|
46
|
+
rows.forEach(row => Object.assign(row, {
|
|
47
|
+
url: `${req.routeOptions.url}/${row.id}/file`,
|
|
48
|
+
filepath: row.url,
|
|
49
|
+
metadata: `${req.routeOptions.url}/${row.id}`,
|
|
50
|
+
exists: !!items.find(el => el.isFile() && el.name === path.basename(row.url))
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
const result = { relpath, data: rows };
|
|
54
|
+
|
|
55
|
+
if (config.debug) {
|
|
56
|
+
Object.assign(result, { rootDir });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return result;
|
|
60
|
+
}
|