@opengis/cms 0.0.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.
Files changed (143) hide show
  1. package/.gitlab-ci.yml +36 -0
  2. package/README.md +5 -0
  3. package/config.example +21 -0
  4. package/docs/.vitepress/abbr.mjs +26 -0
  5. package/docs/.vitepress/config.mjs +119 -0
  6. package/docs/.vitepress/navigation.mjs +82 -0
  7. package/docs/.vitepress/theme/Layout.vue +17 -0
  8. package/docs/.vitepress/theme/components/NavigationLinks.vue +102 -0
  9. package/docs/.vitepress/theme/components/Panzoom.vue +169 -0
  10. package/docs/.vitepress/theme/index.mjs +15 -0
  11. package/docs/.vitepress/theme/style.scss +136 -0
  12. package/docs/abbr.json +4 -0
  13. package/docs/api/builder/cms.builder.delete.md +65 -0
  14. package/docs/api/builder/cms.builder.get.md +70 -0
  15. package/docs/api/builder/cms.builder.list.md +98 -0
  16. package/docs/api/builder/cms.builder.post.md +72 -0
  17. package/docs/api/builder/cms.builder.put.md +88 -0
  18. package/docs/api/category/cms.category.delete.md +60 -0
  19. package/docs/api/category/cms.category.get.md +61 -0
  20. package/docs/api/category/cms.category.list.md +77 -0
  21. package/docs/api/category/cms.category.post.md +62 -0
  22. package/docs/api/category/cms.category.put.md +78 -0
  23. package/docs/api/index.md +50 -0
  24. package/docs/api/manager/cms.manager.delete.md +64 -0
  25. package/docs/api/manager/cms.manager.get.md +72 -0
  26. package/docs/api/manager/cms.manager.list.md +96 -0
  27. package/docs/api/manager/cms.manager.post.md +70 -0
  28. package/docs/api/manager/cms.manager.put.md +86 -0
  29. package/docs/api/media/del.md +64 -0
  30. package/docs/api/media/edit.md +92 -0
  31. package/docs/api/media/list.md +70 -0
  32. package/docs/api/media/metadata.md +57 -0
  33. package/docs/api/media/preview.md +33 -0
  34. package/docs/api/media/upload.md +84 -0
  35. package/docs/db/erd.md +173 -0
  36. package/docs/db/index.md +7 -0
  37. package/docs/index.md +39 -0
  38. package/docs/public/logo-dark.svg +24 -0
  39. package/docs/public/logo-light.svg +24 -0
  40. package/docs/public/logo-short.svg +15 -0
  41. package/docs/public/logo.svg +19 -0
  42. package/docs/readme/index.md +6 -0
  43. package/docs/src/vs-button.vue +157 -0
  44. package/docs/vue/basic/button.md +144 -0
  45. package/docs/vue/index.md +9 -0
  46. package/editor/index.html +14 -0
  47. package/editor/public/vite.svg +1 -0
  48. package/editor/src/App.vue +4 -0
  49. package/editor/src/assets/tailwind/tailwind.js +62 -0
  50. package/editor/src/assets/vue.svg +1 -0
  51. package/editor/src/components/builder/vs-builder-content.vue +163 -0
  52. package/editor/src/components/builder/vs-builder-menu.vue +142 -0
  53. package/editor/src/components/formats/index.js +8 -0
  54. package/editor/src/components/formats/vs-manager-table-date.vue +29 -0
  55. package/editor/src/components/formats/vs-manager-table-switch.vue +16 -0
  56. package/editor/src/components/icons/icon-actions.vue +24 -0
  57. package/editor/src/components/icons/icon-arrow-left.vue +19 -0
  58. package/editor/src/components/icons/icon-check.vue +23 -0
  59. package/editor/src/components/icons/icon-chewron-right.vue +16 -0
  60. package/editor/src/components/icons/icon-close.vue +22 -0
  61. package/editor/src/components/icons/icon-edit.vue +22 -0
  62. package/editor/src/components/icons/icon-folder.vue +18 -0
  63. package/editor/src/components/icons/icon-folder2.vue +17 -0
  64. package/editor/src/components/icons/icon-home.vue +16 -0
  65. package/editor/src/components/icons/icon-image.vue +18 -0
  66. package/editor/src/components/icons/icon-logo.vue +22 -0
  67. package/editor/src/components/icons/icon-media.vue +22 -0
  68. package/editor/src/components/icons/icon-point.vue +11 -0
  69. package/editor/src/components/icons/icon-search.vue +22 -0
  70. package/editor/src/components/icons/icon-table.vue +22 -0
  71. package/editor/src/components/icons/icon-users.vue +18 -0
  72. package/editor/src/components/icons/icon.plus.vue +18 -0
  73. package/editor/src/components/manager/children/vs-manager-collection-content.vue +55 -0
  74. package/editor/src/components/manager/children/vs-manager-collection-item-content.vue +116 -0
  75. package/editor/src/components/manager/children/vs-manager-single-content.vue +112 -0
  76. package/editor/src/components/manager/manager-table/vs-manager-colection-table-add.vue +84 -0
  77. package/editor/src/components/manager/manager-table/vs-manager-collection-table.vue +59 -0
  78. package/editor/src/components/manager/vs-manager-menu.vue +73 -0
  79. package/editor/src/components/media/Breadcrumb.vue +73 -0
  80. package/editor/src/components/shared-components/vs-not-data.vue +213 -0
  81. package/editor/src/components/vs-main-menu.vue +53 -0
  82. package/editor/src/helpers/debounce.js +10 -0
  83. package/editor/src/helpers/translite.js +19 -0
  84. package/editor/src/main.js +30 -0
  85. package/editor/src/misc/import-file.js +32 -0
  86. package/editor/src/pages/vs-builder.vue +22 -0
  87. package/editor/src/pages/vs-layout.vue +17 -0
  88. package/editor/src/pages/vs-manager.vue +30 -0
  89. package/editor/src/pages/vs-media.vue +398 -0
  90. package/editor/src/router/router.js +9 -0
  91. package/editor/src/router/routes.config.js +40 -0
  92. package/editor/src/style.css +0 -0
  93. package/editor/src/templates/form-columns.js +70 -0
  94. package/editor/src/templates/form-template.js +22 -0
  95. package/editor/vite.config.js +37 -0
  96. package/package.json +48 -0
  97. package/server/app.js +25 -0
  98. package/server/config.js +5 -0
  99. package/server/index.js +23 -0
  100. package/server/migrations/media.sql +30 -0
  101. package/server/plugins/hook.js +91 -0
  102. package/server/plugins/vite.js +80 -0
  103. package/server/routes/builder/controllers/cms.builder.delete.js +21 -0
  104. package/server/routes/builder/controllers/cms.builder.get.js +17 -0
  105. package/server/routes/builder/controllers/cms.builder.list.js +16 -0
  106. package/server/routes/builder/controllers/cms.builder.post.js +21 -0
  107. package/server/routes/builder/controllers/cms.builder.put.js +23 -0
  108. package/server/routes/builder/index.mjs +22 -0
  109. package/server/routes/category/controllers/cms.category.delete.js +21 -0
  110. package/server/routes/category/controllers/cms.category.get.js +17 -0
  111. package/server/routes/category/controllers/cms.category.list.js +16 -0
  112. package/server/routes/category/controllers/cms.category.post.js +21 -0
  113. package/server/routes/category/controllers/cms.category.put.js +23 -0
  114. package/server/routes/category/index.mjs +22 -0
  115. package/server/routes/manager/controllers/cms.manager.delete.js +22 -0
  116. package/server/routes/manager/controllers/cms.manager.get.js +21 -0
  117. package/server/routes/manager/controllers/cms.manager.list.js +31 -0
  118. package/server/routes/manager/controllers/cms.manager.post.js +28 -0
  119. package/server/routes/manager/controllers/cms.manager.put.js +23 -0
  120. package/server/routes/manager/index.mjs +22 -0
  121. package/server/routes/media/controllers/delete.js +59 -0
  122. package/server/routes/media/controllers/edit.js +94 -0
  123. package/server/routes/media/controllers/list.js +74 -0
  124. package/server/routes/media/controllers/metadata.js +51 -0
  125. package/server/routes/media/controllers/preview.js +47 -0
  126. package/server/routes/media/controllers/upload.js +79 -0
  127. package/server/routes/media/index.mjs +16 -0
  128. package/server/routes/root.mjs +15 -0
  129. package/server/templates/cls/cms.category_type.json +10 -0
  130. package/server/templates/cls/cms.content_review_status.json +10 -0
  131. package/server/templates/cls/cms.content_status.json +10 -0
  132. package/server/templates/cls/cms.content_type.json +10 -0
  133. package/server/templates/cls/cms.lang.json +10 -0
  134. package/server/templates/page/login.html +59 -0
  135. package/server/templates/select/cms.category_id.sql +1 -0
  136. package/server/templates/select/cms.type_id.sql +1 -0
  137. package/test/config.js +17 -0
  138. package/test/files/eye.svg +4 -0
  139. package/test/helper.js +30 -0
  140. package/test/routes/builder.test.js +99 -0
  141. package/test/routes/category.test.js +97 -0
  142. package/test/routes/manager.test.js +103 -0
  143. package/test/routes/media.test.js +252 -0
@@ -0,0 +1,94 @@
1
+ import path from 'node:path';
2
+ import { rename, readdir } from 'node:fs/promises';
3
+ import { existsSync } from 'node:fs';
4
+
5
+ import { config, getToken, getFolder, dataUpdate, pgClients } from "@opengis/fastify-table/utils.js";
6
+
7
+ const rootDir = getFolder(config, 'local');
8
+
9
+ export default async function editMedia({
10
+ pg = pgClients.client, params = {}, user = {}, body = {},
11
+ }, reply) {
12
+
13
+ const tokenData = await getToken({
14
+ uid: user?.uid || 0,
15
+ token: params.token,
16
+ json: 1,
17
+ });
18
+
19
+ const objdata = pg.pk?.['crm.media'] && !tokenData
20
+ ? await pg.query(`select media_id as id, uploaded_name as name, file_path as "relpath" from crm.media where media_id=$1`, [params.token]).then(el => el.rows?.[0] || {})
21
+ : tokenData;
22
+
23
+ const { id, name } = objdata;
24
+ const relpath = (objdata?.relpath || tokenData?.relpath)?.replace(/\\/g, '/');
25
+
26
+ if ((tokenData?.type === 'file' && !id) || !relpath) {
27
+ return reply.status(400).send('invalid params: token');
28
+ }
29
+
30
+ const data = {};
31
+ const oldfilepath = path.join(rootDir, relpath);
32
+
33
+ if (tokenData?.type === 'dir' && !body?.name) {
34
+ return reply.status(400).send('not enough body params: name');
35
+ }
36
+
37
+ if (body?.name) {
38
+ const newfilepath = path.join(rootDir, path.dirname(relpath), body.name);
39
+
40
+ if (!existsSync(oldfilepath)) {
41
+ return reply.status(404).send('file / directory not found');
42
+ }
43
+
44
+ if (existsSync(newfilepath)) {
45
+ return reply.status(400).send('file / directory with such name already exists');
46
+ }
47
+
48
+ // sync file path in db data
49
+ const arr = tokenData?.type === 'dir' ? await readdir(oldfilepath, { withFileTypes: true }) : [];
50
+
51
+ await rename(oldfilepath, newfilepath);
52
+
53
+ if (tokenData?.type === 'dir') {
54
+ const relpaths = arr.filter(el => el.isFile()).map(el => path.join(tokenData.relpath, el.name).replace(/\\/g, '/'));
55
+ const { rows = [] } = pg.pk?.['crm.media'] ? await pg.query(`select media_id as id, file_path as relpath from crm.media where file_path=any($1::text[])`, [relpaths]) : {};
56
+ await Promise.all(rows.map(async (el) => dataUpdate({
57
+ id: el.id,
58
+ table: 'crm.media',
59
+ data: {
60
+ file_path: el.relpath.replace(relpath.replace(/\\/g, '/'), path.join(path.dirname(relpath), body.name).replace(/\\/g, '/'))
61
+ },
62
+ uid: user?.uid || 0,
63
+ })));
64
+ return reply.status(200).send({
65
+ name: body.name,
66
+ relpath: path.join(path.dirname(relpath), body.name).replace(/\\/g, '/'),
67
+ });
68
+ }
69
+ Object.assign(data, {
70
+ uploaded_name: body.name,
71
+ file_path: path.join(path.dirname(relpath), body.name).replace(/\\/g, '/'),
72
+ });
73
+ }
74
+
75
+ if (body?.caption) Object.assign(data, { caption: body.caption });
76
+ if (body?.altname) Object.assign(data, { altname: body.altname });
77
+
78
+ const res = await dataUpdate({
79
+ id,
80
+ table: 'crm.media',
81
+ data,
82
+ uid: user?.uid || 0,
83
+ });
84
+
85
+ return {
86
+ id: tokenData?.id || id,
87
+ name: res?.uploaded_name,
88
+ relpath: res?.file_path,
89
+ size: res?.size,
90
+ extension: res?.ext,
91
+ caption: res?.caption,
92
+ altname: res?.altname,
93
+ };
94
+ }
@@ -0,0 +1,74 @@
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, setToken } 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 = {}, user = {} } = req;
12
+ const { dir } = query;
13
+ const dirpath = path.join(rootDir, 'media', dir || '');
14
+
15
+ if (dir && (typeof dir !== 'string' || dir.includes('..'))) {
16
+ return reply.status(403).send('invalid params: dir');
17
+ }
18
+
19
+ if (!existsSync(dirpath)) {
20
+ return { data: [], dir };
21
+ }
22
+
23
+ const token = setToken({
24
+ ids: [JSON.stringify({ dir, name: dir, relpath: path.join('/media', dir || '').replace(/\\/g, '/') })],
25
+ uid: user?.uid || 0,
26
+ array: 1,
27
+ })?.[0];
28
+
29
+ const isDirectory = (await stat(dirpath)).isDirectory();
30
+
31
+ const items = isDirectory ? await readdir(dirpath) : [];
32
+
33
+ const relpaths = items.map(el => path.join('/media', dir || '', el).replace(/\\/g, '/'));
34
+ const { rows = [] } = await pg.query(`select media_id as id, uploaded_name as name, file_path as relpath from crm.media where file_path = any($1::text[])`, [relpaths]);
35
+ const ids = rows.reduce((acc, curr) => Object.assign(acc, { [curr.relpath]: curr.id }), {});
36
+
37
+ const data = await Promise.all(items.map(async (item) => {
38
+ const itempath = path.join(dirpath, item);
39
+ const extension = path.extname(item) === '' ? null : path.extname(item).split('.').pop();
40
+ const { ctime, mtime, size } = await stat(itempath);
41
+ const type = (await stat(itempath)).isDirectory() ? 'dir' : 'file';
42
+ const childList = type === 'dir' ? await readdir(itempath) : [];
43
+ const count = type === 'dir' ? childList.length : undefined;
44
+ return {
45
+ id: ids[path.join('/media', dir || '', item).replace(/\\/g, '/')],
46
+ name: item,
47
+ type,
48
+ count,
49
+ size,
50
+ extension,
51
+ token: setToken({
52
+ ids: [JSON.stringify({
53
+ id: ids[path.join('/media', dir || '', item).replace(/\\/g, '/')],
54
+ dir,
55
+ relpath: path.join('/media', dir || '', item).replace(/\\/g, '/'),
56
+ name: item,
57
+ type,
58
+ })],
59
+ uid: user?.uid || 0,
60
+ array: 1,
61
+ })[0],
62
+ ctime,
63
+ mtime,
64
+ };
65
+ }));
66
+
67
+ const result = { dir, token, data };
68
+
69
+ if (config.debug) {
70
+ Object.assign(result, { rootDir });
71
+ }
72
+
73
+ return result;
74
+ }
@@ -0,0 +1,51 @@
1
+ import path from 'node:path';
2
+ import { stat } from 'node:fs/promises';
3
+ import { existsSync } from 'node:fs';
4
+
5
+ import { config, getToken, getFolder, pgClients } from "@opengis/fastify-table/utils.js";
6
+
7
+ const rootDir = getFolder(config, 'local');
8
+
9
+ export default async function metadataMedia({
10
+ pg = pgClients.client, params = {}, user = {},
11
+ }, reply) {
12
+ if (!params.token) {
13
+ return reply.status(400).send('not enough params: token');
14
+ }
15
+
16
+ const tokenData = await getToken({
17
+ uid: user?.uid || 0,
18
+ token: params.token,
19
+ json: 1,
20
+ });
21
+
22
+ const { relpath, caption, altname, id } = !tokenData && pg.pk?.['crm.media']
23
+ ? await pg.query(`select media_id as id, file_path as "relpath", caption from crm.media where media_id=$1`, [params.token]).then(el => el.rows?.[0] || {})
24
+ : {};
25
+
26
+ if ((!tokenData?.name || tokenData?.type !== 'file') && !relpath) {
27
+ return reply.status(400).send('invalid params: token');
28
+ }
29
+
30
+ const filepath = tokenData?.name
31
+ ? path.join(rootDir, 'media', tokenData.dir || '', tokenData.name)
32
+ : path.join(rootDir, relpath);
33
+
34
+ if (!existsSync(filepath)) {
35
+ return reply.status(404).send('file not found');
36
+ }
37
+
38
+ const stats = await stat(filepath);
39
+ const { ctime, mtime, size } = stats;
40
+
41
+ return reply.status(200).send({
42
+ id: tokenData?.id || id,
43
+ name: path.basename(filepath),
44
+ size,
45
+ extension: path.extname(filepath)?.split?.('.')?.pop?.(),
46
+ ctime, // creation timestamp
47
+ mtime, // last modification timestamp
48
+ caption,
49
+ altname,
50
+ });
51
+ }
@@ -0,0 +1,47 @@
1
+ import path from 'node:path';
2
+ import { existsSync } from 'node:fs';
3
+ import { readFile } from 'node:fs/promises';
4
+
5
+ import { config, getToken, getFolder } from "@opengis/fastify-table/utils.js";
6
+ import { getMimeType } from '@opengis/fastify-file/utils.js';
7
+
8
+ const rootDir = getFolder(config, 'local');
9
+
10
+ export default async function deleteMedia({
11
+ params = {}, query = {}, user = {},
12
+ }, reply) {
13
+
14
+ if (!params?.token) {
15
+ return reply.status(400).send('not enough params: token');
16
+ }
17
+
18
+ const tokenData = await getToken({
19
+ uid: user?.uid || 0,
20
+ token: params.token,
21
+ json: 1,
22
+ });
23
+
24
+ const { relpath } = tokenData || {};
25
+
26
+ if (!relpath) {
27
+ return reply.status(403).send('invalid token');
28
+ }
29
+
30
+ const filepath = path.join(rootDir, relpath);
31
+
32
+ if (!existsSync(filepath)) {
33
+ return reply.status(404).send('file not found');
34
+ }
35
+
36
+ const buffer = await readFile(filepath, { buffer: true });
37
+
38
+ if (query?.download) {
39
+ const headers = {
40
+ 'Content-Type': getMimeType(filepath),
41
+ 'Content-Disposition': `attachment; filename=${path.basename(filepath)}`
42
+ };
43
+ return reply.headers(headers).send(buffer);
44
+ }
45
+
46
+ return reply.headers({ 'Content-Type': getMimeType(filepath) }).send(buffer);
47
+ }
@@ -0,0 +1,79 @@
1
+ import path from 'node:path';
2
+ import { mkdir, rename } from 'node:fs/promises';
3
+
4
+ import { config, getToken, getFolder, setToken, dataInsert } from "@opengis/fastify-table/utils.js";
5
+ import { uploadMultiPart } from '@opengis/fastify-file/utils.js';
6
+
7
+ const rootDir = getFolder(config, 'local');
8
+
9
+ export default async function uploadMedia(req, reply) {
10
+ const { params = {}, user = {}, body = {} } = req;
11
+
12
+ if (!params.token) {
13
+ return reply.status(400).send('not enough params: token');
14
+ }
15
+
16
+ const tokenData = await getToken({
17
+ uid: user?.uid || 0,
18
+ token: params.token,
19
+ json: 1,
20
+ });
21
+
22
+ if (!tokenData) {
23
+ return reply.status(403).send('invalid params: token');
24
+ }
25
+
26
+ const { name: dirname } = tokenData;
27
+
28
+ // upload assets
29
+ if (req.headers['content-type']?.split?.(';')?.shift?.() === 'multipart/form-data') {
30
+ const file = await uploadMultiPart(req);
31
+ const filename = `${file.newFilename}${path.extname(file.filepath)}`;
32
+ const newfilepath = path.join(rootDir, 'media', dirname || '', filename);
33
+ // console.log(file.filepath, newfilepath);
34
+ await rename(file.filepath, newfilepath);
35
+
36
+ const id = await dataInsert({
37
+ table: 'crm.media',
38
+ data: {
39
+ uploaded_name: file.originalFilename,
40
+ file_path: path.join('/media', dirname || '', filename).replace(/\\/g, '/'),
41
+ ext: path.extname(file.filepath)?.split('.')?.pop?.(),
42
+ size: file.size,
43
+ },
44
+ uid: user?.uid || 0,
45
+ }).then(el => el?.rows?.[0]?.media_id);
46
+ const result = {
47
+ dir: dirname,
48
+ id,
49
+ name: filename,
50
+ type: 'file',
51
+ token: setToken({
52
+ ids: [JSON.stringify({ id, dir: dirname, relpath: path.join('/media', dirname || '', filename).replace(/\\/g, '/'), name: filename, type: 'file' })],
53
+ uid: user?.uid || 0,
54
+ array: 1,
55
+ })[0],
56
+ };
57
+ return reply.status(200).send(result);
58
+ }
59
+
60
+ // create directory
61
+ if (!body?.name || typeof body.name !== 'string' || body.name.includes('..')) {
62
+ return reply.status(400).send('invalid body params: name');
63
+ }
64
+
65
+ const subdir = path.join('/media', dirname || '', body.name).replace(/\\/g, '/');
66
+ const dirpath = path.join(rootDir, subdir);
67
+ await mkdir(dirpath, { recursive: true });
68
+
69
+ return reply.status(200).send({
70
+ dir: subdir,
71
+ name: body.name,
72
+ type: 'dir',
73
+ token: setToken({
74
+ ids: [JSON.stringify({ relpath: subdir, name: body.name, type: 'dir' })],
75
+ uid: user?.uid || 0,
76
+ array: 1,
77
+ })[0],
78
+ });
79
+ }
@@ -0,0 +1,16 @@
1
+ import list from './controllers/list.js';
2
+ import metadata from './controllers/metadata.js';
3
+ import upload from './controllers/upload.js';
4
+ import edit from './controllers/edit.js';
5
+ import del from './controllers/delete.js';
6
+ import preview from './controllers/preview.js';
7
+
8
+ export default async function route(app) {
9
+ app.get('/cms-media', { config: { policy: ['site'] } }, list);
10
+ app.get('/cms-media/:token', { config: { policy: ['site'] } }, preview);
11
+ app.post('/cms-media/:token', { config: { policy: ['site'] } }, upload); // upload file / create empty dir
12
+
13
+ app.get('/cms-media-metadata/:token', { config: { policy: ['site'] } }, metadata);
14
+ app.put('/cms-media/:token', { config: { policy: ['site'] } }, edit);
15
+ app.delete('/cms-media/:token', { config: { policy: ['site'] } }, del);
16
+ }
@@ -0,0 +1,15 @@
1
+ 'use strict'
2
+
3
+ const schema = {
4
+ querystring: {
5
+ name: { type: 'string', pattern: "^(\\d+)$" },
6
+ token: { type: 'string', pattern: "^(\\d+)$" },
7
+ },
8
+ }
9
+
10
+ export default async function (fastify, opts) {
11
+ fastify.get('/test', async function (request, reply) {
12
+ return { root: true }
13
+ });
14
+
15
+ }
@@ -0,0 +1,10 @@
1
+ [
2
+ {
3
+ "id": "1",
4
+ "text": "Important"
5
+ },
6
+ {
7
+ "id": "2",
8
+ "text": "Common"
9
+ }
10
+ ]
@@ -0,0 +1,10 @@
1
+ [
2
+ {
3
+ "id": "1",
4
+ "text": "Pending"
5
+ },
6
+ {
7
+ "id": "2",
8
+ "text": "Approved"
9
+ }
10
+ ]
@@ -0,0 +1,10 @@
1
+ [
2
+ {
3
+ "id": "1",
4
+ "text": "Draft"
5
+ },
6
+ {
7
+ "id": "2",
8
+ "text": "Publish"
9
+ }
10
+ ]
@@ -0,0 +1,10 @@
1
+ [
2
+ {
3
+ "id": "1",
4
+ "text": "Single"
5
+ },
6
+ {
7
+ "id": "2",
8
+ "text": "Collection"
9
+ }
10
+ ]
@@ -0,0 +1,10 @@
1
+ [
2
+ {
3
+ "id": "uk",
4
+ "text": "Українська"
5
+ },
6
+ {
7
+ "id": "en",
8
+ "text": "English"
9
+ }
10
+ ]
@@ -0,0 +1,59 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" dir="">
3
+
4
+ <head>
5
+ <!-- scripts -->
6
+ <script src="https://cdn.tailwindcss.com"></script>
7
+ </head>
8
+
9
+ <body class="bg-[#f3f4f6] flex items-center flex-1 w-full h-[100vh] overflow-x-hidden min-h-full ">
10
+ <main class="w-full">
11
+ <div class="flex w-full max-w-sm mx-auto overflow-hidden bg-white rounded-lg shadow-lg dark:bg-gray-800 lg:max-w-4xl">
12
+ <div class="hidden bg-cover lg:block lg:w-1/2 bg-center" style="background-image: url('https://nipo.gov.ua/wp-content/uploads/2023/02/Pro_nas_2301-768x512.jpg');"></div>
13
+
14
+ <div class="w-full px-6 py-8 md:px-8 lg:w-1/2">
15
+ <div class="flex justify-center mx-auto">
16
+ <img class="w-auto h-7 sm:h-16" src="https://nipo.gov.ua/wp-content/uploads/2023/01/IP_office_logo_UA-1-1.png" alt="logo">
17
+ </div>
18
+
19
+ <p class="mt-3 mb-[32px] text-xl text-center text-gray-600 dark:text-gray-200">
20
+ Український національний офіс інтелектуальної власності та інновацій
21
+ </p>
22
+
23
+ <form action="/api/login" method="get" >
24
+ <div class="mt-4">
25
+ <label class="block mb-2 text-sm font-medium text-gray-600 dark:text-gray-200" for="LoggingEmailAddress">Електронна пошта / Логін</label>
26
+ <input name="username" id="LoggingEmailAddress" class="block w-full px-4 py-2 text-gray-700 bg-white border rounded-lg dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 focus:ring-opacity-40 dark:focus:border-blue-300 focus:outline-none focus:ring focus:ring-blue-300" type="text">
27
+ </div>
28
+
29
+ <div class="mt-4">
30
+ <div class="flex justify-between">
31
+ <label class="block mb-2 text-sm font-medium text-gray-600 dark:text-gray-200" for="loggingPassword">Пароль</label>
32
+ <!-- <a class="text-xs text-gray-500 dark:text-gray-300 hover:underline">Forget Password?</a> -->
33
+ </div>
34
+
35
+ <input id="loggingPassword" name="password" class="block w-full px-4 py-2 text-gray-700 bg-white border rounded-lg dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 focus:ring-opacity-40 dark:focus:border-blue-300 focus:outline-none focus:ring focus:ring-blue-300" type="password">
36
+ </div>
37
+
38
+ <div class="mt-6">
39
+ <button type="submit" class="w-full px-6 py-3 text-sm font-medium tracking-wide text-white capitalize transition-colors duration-300 transform bg-gray-800 rounded-lg hover:bg-gray-700 focus:outline-none focus:ring focus:ring-gray-300 focus:ring-opacity-50">
40
+ Увійти
41
+ </button>
42
+ </div>
43
+
44
+ </form>
45
+ </div>
46
+ </div>
47
+ </main>
48
+
49
+ </body>
50
+
51
+
52
+ <script>
53
+ const error = location.search.includes('wrong_pass')
54
+ if (error) {
55
+ document.getElementById('login_error').innerHTML = 'Не вірний користувач або пароль';
56
+ }
57
+ </script>
58
+
59
+ </html>
@@ -0,0 +1 @@
1
+ select category_id, title from site.categories
@@ -0,0 +1 @@
1
+ select content_type_id, name from site.content_types
package/test/config.js ADDED
@@ -0,0 +1,17 @@
1
+ import config from '../server/config.js';
2
+
3
+ Object.assign(config, {
4
+ pg: config.pg || {
5
+ host: '192.168.3.160',
6
+ port: 5439,
7
+ database: 'ip_ukraine',
8
+ user: 'postgres',
9
+ password: 'postgres',
10
+ },
11
+ redis: config.redis || {
12
+ host: '192.168.3.160',
13
+ port: 6379,
14
+ family: 4,
15
+ },
16
+ });
17
+ export default config;
@@ -0,0 +1,4 @@
1
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M1.3335 8C1.3335 8 3.3335 3.33333 8.00016 3.33333C12.6668 3.33333 14.6668 8 14.6668 8C14.6668 8 12.6668 12.6667 8.00016 12.6667C3.3335 12.6667 1.3335 8 1.3335 8Z" stroke="#6B7280" stroke-linecap="round" stroke-linejoin="round"/>
3
+ <path d="M8 10C9.10457 10 10 9.10457 10 8C10 6.89543 9.10457 6 8 6C6.89543 6 6 6.89543 6 8C6 9.10457 6.89543 10 8 10Z" stroke="#6B7280" stroke-linecap="round" stroke-linejoin="round"/>
4
+ </svg>
package/test/helper.js ADDED
@@ -0,0 +1,30 @@
1
+ // This file contains code that we reuse
2
+ // between our tests.
3
+ import Fastify from 'fastify';
4
+
5
+ import { redisClients, pgClients } from '@opengis/fastify-table/utils.js';
6
+
7
+ import appService from '../server/app.js';
8
+
9
+ // automatically build and tear down our instance
10
+ async function build(t) {
11
+ // you can set all the options supported by the fastify CLI command
12
+ // const argv = [AppPath]
13
+ process.env.NODE_ENV = 'production';
14
+ const app = Fastify({ logger: false });
15
+ app.register(appService);
16
+ // close the app after we are done
17
+ t.after(() => {
18
+ Object.keys(pgClients).forEach((key) => {
19
+ pgClients[key].end();
20
+ });
21
+ Object.keys(redisClients || {}).forEach((key) => {
22
+ redisClients[key].quit();
23
+ });
24
+ app.close();
25
+ });
26
+
27
+ return app;
28
+ }
29
+
30
+ export default build;
@@ -0,0 +1,99 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert';
3
+
4
+ import { config, pgClients } from '@opengis/fastify-table/utils.js';
5
+ import build from '../helper.js';
6
+
7
+ test('cms builder api', async (t) => {
8
+ const app = await build(t);
9
+
10
+ const pg = pgClients.client;
11
+
12
+ const prefix = config.prefix || '/api';
13
+
14
+ const testContentTypeId = 'testContentType';
15
+
16
+ app.addHook('onRequest', async (req) => {
17
+ req.session = { passport: { user: { user_type: 'admin', uid: '1' } } };
18
+ req.user = req.session.passport.user;
19
+ req.uid = req.user.uid;
20
+ req.unittest = 1;
21
+ });
22
+
23
+ await t.test('POST /cms-builder', async () => {
24
+ const res = await app.inject({
25
+ method: 'POST',
26
+ url: `${prefix}/cms-builder`,
27
+ body: {
28
+ content_type_id: testContentTypeId,
29
+ name: 'Created value',
30
+ template: 'news'
31
+ },
32
+ });
33
+ assert.equal(res.statusCode, 200);
34
+ const rep = res.json();
35
+ // console.log(rep);
36
+ });
37
+
38
+ await t.test('PUT /cms-builder', async () => {
39
+ // await pg.query('insert into site.content_types (content_type_id, uid) values ($1, $2)', [testContentTypeId, testUid]);
40
+ const res = await app.inject({
41
+ method: 'PUT',
42
+ url: `${prefix}/cms-builder/${testContentTypeId}`,
43
+ body: {
44
+ name: 'Edited value'
45
+ },
46
+ });
47
+ assert.equal(res.statusCode, 200);
48
+ const rep = res.json();
49
+ // console.log(rep);
50
+ });
51
+
52
+ await t.test('GET /cms-builder/:id', async () => {
53
+ // await pg.query('insert into site.contents (content_id, uid) values ($1, $2)', [testContentId, testUid]);
54
+ const res = await app.inject({
55
+ method: 'GET',
56
+ url: `${prefix}/cms-builder/${testContentTypeId}`,
57
+ });
58
+ assert.equal(res.statusCode, 200);
59
+ const rep = res.json();
60
+ // console.log(rep);
61
+ });
62
+
63
+ await t.test('GET /cms-builder', async () => {
64
+ // await pg.query('insert into site.contents (content_id, uid) values ($1, $2)', [testContentId, testUid]);
65
+ const res = await app.inject({
66
+ method: 'GET',
67
+ url: `${prefix}/cms-builder`,
68
+ });
69
+ assert.equal(res.statusCode, 200);
70
+ const rep = res.json();
71
+ // console.log(rep);
72
+ });
73
+
74
+ await t.test('GET /cms-builder?id', async () => {
75
+ // await pg.query('insert into site.contents (content_id, uid) values ($1, $2)', [testContentId, testUid]);
76
+ const res = await app.inject({
77
+ method: 'GET',
78
+ url: `${prefix}/cms-builder?content_type_id=${testContentTypeId}`,
79
+ });
80
+ assert.equal(res.statusCode, 200);
81
+ const rep = res.json();
82
+ // console.log(rep);
83
+ });
84
+
85
+ await t.test('DELETE /cms-builder/:id', async () => {
86
+ const res = await app.inject({
87
+ method: 'DELETE',
88
+ url: `${prefix}/cms-builder/${testContentTypeId}`,
89
+ });
90
+ assert.equal(res.statusCode, 200);
91
+ const rep = res.json();
92
+ // console.log(rep);
93
+ });
94
+
95
+ await t.test('clean up', async () => {
96
+ const { rowCount: contentTypesCount } = await pg.query('delete from site.content_types where content_type_id=$1', [testContentTypeId]);
97
+ console.log('clean up', contentTypesCount);
98
+ });
99
+ });