@opengis/fastify-table 2.1.9 → 2.1.11

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 (40) hide show
  1. package/dist/module/core/cls/constraint_type.json +14 -0
  2. package/dist/module/core/cls/constraint_type_table.json +18 -0
  3. package/dist/module/core/select/core.user_mentioned.sql +1 -1
  4. package/dist/server/plugins/auth/funcs/getUserPermissions.d.ts +2 -0
  5. package/dist/server/plugins/auth/funcs/getUserPermissions.d.ts.map +1 -0
  6. package/dist/server/plugins/auth/funcs/getUserPermissions.js +24 -0
  7. package/dist/server/plugins/auth/onRequest.d.ts +4 -0
  8. package/dist/server/plugins/auth/onRequest.d.ts.map +1 -0
  9. package/dist/server/plugins/auth/onRequest.js +104 -0
  10. package/dist/server/plugins/migration/index.d.ts +3 -0
  11. package/dist/server/plugins/migration/index.d.ts.map +1 -0
  12. package/dist/server/plugins/migration/index.js +5 -0
  13. package/dist/server/plugins/policy/funcs/checkAuth.d.ts +4 -0
  14. package/dist/server/plugins/policy/funcs/checkAuth.d.ts.map +1 -0
  15. package/dist/server/plugins/policy/funcs/checkAuth.js +104 -0
  16. package/dist/server/plugins/table/funcs/getFilterSQL/index.d.ts +1 -1
  17. package/dist/server/plugins/table/funcs/getFilterSQL/index.d.ts.map +1 -1
  18. package/dist/server/plugins/table/funcs/getFilterSQL/index.js +29 -5
  19. package/dist/server/plugins/table/funcs/getFilterSQL/util/formatValue.d.ts.map +1 -1
  20. package/dist/server/plugins/table/funcs/getFilterSQL/util/formatValue.js +12 -1
  21. package/dist/server/plugins/table/funcs/getFilterSQL/util/getFilterQuery.d.ts.map +1 -1
  22. package/dist/server/plugins/table/funcs/getFilterSQL/util/getFilterQuery.js +1 -3
  23. package/dist/server/routes/access/controllers/access.group.post.js +1 -1
  24. package/dist/server/routes/access/controllers/access.resources.d.ts +6 -0
  25. package/dist/server/routes/access/controllers/access.resources.d.ts.map +1 -0
  26. package/dist/server/routes/access/controllers/access.resources.js +14 -0
  27. package/dist/server/routes/access/controllers/resources.d.ts +11 -0
  28. package/dist/server/routes/access/controllers/resources.d.ts.map +1 -0
  29. package/dist/server/routes/access/controllers/resources.js +14 -0
  30. package/dist/server/routes/access/functions/resources.d.ts +6 -0
  31. package/dist/server/routes/access/functions/resources.d.ts.map +1 -0
  32. package/dist/server/routes/access/functions/resources.js +11 -0
  33. package/dist/server/routes/file/controllers/resize.d.ts.map +1 -1
  34. package/dist/server/routes/file/controllers/resize.js +0 -15
  35. package/dist/server/routes/table/controllers/suggest.d.ts +16 -0
  36. package/dist/server/routes/table/controllers/suggest.d.ts.map +1 -1
  37. package/dist/server/routes/table/controllers/suggest.js +1 -1
  38. package/package.json +1 -1
  39. package/dist/log/migration/dist-technical-cls.json +0 -1
  40. package/dist/log/migration/dist-technical-cls.sql +0 -2
@@ -0,0 +1,14 @@
1
+ [
2
+ {
3
+ "id": "u",
4
+ "text": "UK"
5
+ },
6
+ {
7
+ "id": "p",
8
+ "text": "PK"
9
+ },
10
+ {
11
+ "id": "f",
12
+ "text": "FK"
13
+ }
14
+ ]
@@ -0,0 +1,18 @@
1
+ [
2
+ {
3
+ "id": "u",
4
+ "text": "UK"
5
+ },
6
+ {
7
+ "id": "p",
8
+ "text": "PK"
9
+ },
10
+ {
11
+ "id": "f",
12
+ "text": "FK"
13
+ },
14
+ {
15
+ "id": "c",
16
+ "text": "CHECK"
17
+ }
18
+ ]
@@ -1,2 +1,2 @@
1
- select uid, coalesce(sur_name,'')||coalesce(' '||user_name,'') as text, email from admin.users
1
+ select uid, coalesce(sur_name,'')||coalesce(' '||user_name,'') as text, email from admin.users
2
2
  where enabled order by coalesce(sur_name,'')||coalesce(' '||user_name,'')
@@ -0,0 +1,2 @@
1
+ export default function getUserPermissions(uid?: string, pg?: any): Promise<any>;
2
+ //# sourceMappingURL=getUserPermissions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getUserPermissions.d.ts","sourceRoot":"","sources":["../../../../../server/plugins/auth/funcs/getUserPermissions.ts"],"names":[],"mappings":"AAUA,wBAA8B,kBAAkB,CAC9C,GAAG,CAAC,EAAE,MAAM,EACZ,EAAE,MAAmB,gBAoBtB"}
@@ -0,0 +1,24 @@
1
+ import pgClients from "../../pg/pgClients.js";
2
+ const q = `select resource_id as name,
3
+ array_intersect(coalesce(a.actions, array['read']), coalesce(c.actions, array['create', 'read','update','delete'])) as actions,
4
+ b.name as role
5
+ from admin.role_access a
6
+ left join admin.roles b on a.role_id=b.role_id and b.enabled
7
+ left join admin.user_roles c on a.role_id=c.role_id
8
+ where resource_id is not null and $1 in (a.user_uid, c.user_uid)`;
9
+ export default async function getUserPermissions(uid, pg = pgClients.client) {
10
+ if (!uid)
11
+ return [];
12
+ // ? in case pg.pk not set yet
13
+ const pks = await pg
14
+ .query(`SELECT json_object_agg(conrelid::regclass, (SELECT attname FROM pg_attribute WHERE attrelid = c.conrelid AND attnum = c.conkey[1]) )
15
+ FROM pg_constraint c WHERE contype = 'p' AND connamespace::regnamespace::text = 'admin'`)
16
+ .then((el) => el.rows?.[0]?.json_object_agg || {});
17
+ const permissions = pks["admin.role_access"] &&
18
+ pks["admin.user_roles"] &&
19
+ pks["admin.users"] &&
20
+ uid
21
+ ? await pg.query(q, [uid]).then((el) => el.rows || [])
22
+ : [];
23
+ return permissions;
24
+ }
@@ -0,0 +1,4 @@
1
+ import { FastifyReply } from "fastify";
2
+ import { ExtendedRequest } from "../../types/core.js";
3
+ export default function onRequest(req: ExtendedRequest, reply: FastifyReply): Promise<null>;
4
+ //# sourceMappingURL=onRequest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"onRequest.d.ts","sourceRoot":"","sources":["../../../../server/plugins/auth/onRequest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAMvC,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAItD,wBAA8B,SAAS,CACrC,GAAG,EAAE,eAAe,EACpB,KAAK,EAAE,YAAY,iBA+HpB"}
@@ -0,0 +1,104 @@
1
+ import { existsSync } from "node:fs";
2
+ import config from "../../../config.js";
3
+ const { prefix = "/api" } = config;
4
+ export default async function onRequest(req, reply) {
5
+ const { hostname, headers, routeOptions } = req;
6
+ const { config: routeConfig, method, handler, url } = routeOptions || {};
7
+ const { policy } = routeConfig || {};
8
+ const isApi = method && url && typeof handler === "function" && url !== "*";
9
+ // handle non-api at vite/vike
10
+ if (!isApi) {
11
+ return null;
12
+ }
13
+ // proxy from old apps to editor, bi etc.
14
+ const validToken = (req.ip === "193.239.152.181" ||
15
+ req.ip === "127.0.0.1" ||
16
+ req.ip?.startsWith?.("192.168.") ||
17
+ config.debug) &&
18
+ req.headers?.token &&
19
+ config.auth?.tokens?.includes?.(headers.token);
20
+ if (validToken && !req?.user?.uid) {
21
+ req.user = {
22
+ uid: req.headers?.uid?.toString?.(),
23
+ user_type: req.headers?.user_type?.toString?.() || "regular",
24
+ };
25
+ }
26
+ const isAdmin = process.env.NODE_ENV === "admin" ||
27
+ hostname?.split?.(":")?.shift?.() === config.adminDomain ||
28
+ config.admin ||
29
+ hostname?.startsWith?.("admin");
30
+ const isPublic = Array.isArray(policy)
31
+ ? policy.includes("public")
32
+ : policy === "L0";
33
+ if (req.cookies?.["session_auth"] &&
34
+ !req.session?.passport?.user?.uid &&
35
+ (config.auth?.disable || config.auth?.user)) {
36
+ req.session = req.session || {};
37
+ req.session.passport = req.session.passport || {}; // ensure passport session exists
38
+ req.session.passport.user = {
39
+ ...(config.auth?.user || {}),
40
+ uid: config.auth?.user?.uid?.toString?.() || "1",
41
+ user_rnokpp: config.auth?.user?.rnokpp,
42
+ user_type: config.auth?.user?.type || "regular",
43
+ };
44
+ req.user = req.session.passport.user;
45
+ }
46
+ // ! intentional: null || undefined > undefined
47
+ req.user = req.user || req.session?.passport?.user || undefined; // fix for user.uid errors, by default user is null, while with express passport it was {}, unauthorized user does not trigger serializer
48
+ // currently 2factor + auth with passwd file not supported
49
+ const ispasswd = (existsSync("passwd") && !config.auth?.["2factor"]) || config.auth?.passwd;
50
+ const loginPageUrl = config.auth?.link?.core?.login || config?.auth?.redirect || "/login";
51
+ if (!req.user?.uid &&
52
+ !config.auth?.disable &&
53
+ isAdmin &&
54
+ !isPublic &&
55
+ !config.auth?.disableRedirect &&
56
+ !req.url.startsWith(prefix) &&
57
+ !req.url.startsWith("/api") &&
58
+ !req.url.includes(loginPageUrl) &&
59
+ !req.url.includes(".") &&
60
+ !req.url.includes("@")) {
61
+ if (isApi) {
62
+ return reply.status(401).send({ error: "unauthorized", code: 401 });
63
+ }
64
+ return reply.redirect(`${loginPageUrl}` + `?redirect=${req.url}`);
65
+ }
66
+ // by default, disable 2factor for id.gov.ua auth
67
+ const check = req.user?.auth_type === "govid" ? config.auth?.["2factor"]?.govid : true;
68
+ const login2faPage = config.auth?.link?.["2fa"]?.login || "/2factor";
69
+ // example: 2factor for admin env only, while public env does not require it
70
+ const checkEnv = () => {
71
+ if (!config.auth?.["2factorEnv"])
72
+ return true;
73
+ if ((config.auth?.["2factorEnv"] &&
74
+ process.env.NODE_ENV === config.auth?.["2factorEnv"]) ||
75
+ (config.auth?.["2factorEnv"] === "admin" && isAdmin)) {
76
+ return true;
77
+ }
78
+ return false;
79
+ };
80
+ // if 2factor is enabled globally + for user and secondFactorPassed not true => redirect to 2factor login page
81
+ if (req.user?.uid &&
82
+ req.user?.twofa &&
83
+ // config.auth?.["2factor"] &&
84
+ // !isPublic &&
85
+ (routeOptions?.method || "GET") === "GET" &&
86
+ !req.session?.secondFactorPassed &&
87
+ !ispasswd &&
88
+ !config.auth?.disableRedirect &&
89
+ !config.auth?.disable &&
90
+ check &&
91
+ checkEnv() &&
92
+ !req.url.startsWith(login2faPage) &&
93
+ !routeOptions.url?.includes?.("/logout") &&
94
+ !routeOptions.url?.includes?.("/2fa") &&
95
+ !routeOptions.url?.includes?.("/assets")) {
96
+ if (isApi) {
97
+ return reply
98
+ .status(403)
99
+ .send({ error: "access restricted: twofa", code: 403 });
100
+ }
101
+ return reply.redirect(login2faPage);
102
+ }
103
+ return null;
104
+ }
@@ -0,0 +1,3 @@
1
+ declare function plugin(): Promise<void>;
2
+ export default plugin;
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../server/plugins/migration/index.ts"],"names":[],"mappings":"AAEA,iBAAe,MAAM,kBAEpB;AAED,eAAe,MAAM,CAAC"}
@@ -0,0 +1,5 @@
1
+ // import execMigrations from './funcs/exec.migrations.js';
2
+ async function plugin() {
3
+ // fastify.decorate('execMigrations', execMigrations);
4
+ }
5
+ export default plugin;
@@ -0,0 +1,4 @@
1
+ import { FastifyReply } from "fastify";
2
+ import { ExtendedRequest } from "../../../types/core.js";
3
+ export default function onRequest(req: ExtendedRequest, reply: FastifyReply): Promise<null>;
4
+ //# sourceMappingURL=checkAuth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkAuth.d.ts","sourceRoot":"","sources":["../../../../../server/plugins/policy/funcs/checkAuth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAMvC,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAIzD,wBAA8B,SAAS,CACrC,GAAG,EAAE,eAAe,EACpB,KAAK,EAAE,YAAY,iBA+HpB"}
@@ -0,0 +1,104 @@
1
+ import { existsSync } from "node:fs";
2
+ import config from "../../../../config.js";
3
+ const { prefix = "/api" } = config;
4
+ export default async function onRequest(req, reply) {
5
+ const { hostname, headers, routeOptions } = req;
6
+ const { config: routeConfig, method, handler, url } = routeOptions || {};
7
+ const { policy } = routeConfig || {};
8
+ const isApi = method && url && typeof handler === "function" && url !== "*";
9
+ // handle non-api at vite/vike
10
+ if (!isApi) {
11
+ return null;
12
+ }
13
+ // proxy from old apps to editor, bi etc.
14
+ const validToken = (req.ip === "193.239.152.181" ||
15
+ req.ip === "127.0.0.1" ||
16
+ req.ip?.startsWith?.("192.168.") ||
17
+ config.debug) &&
18
+ req.headers?.token &&
19
+ config.auth?.tokens?.includes?.(headers.token);
20
+ if (validToken && !req?.user?.uid) {
21
+ req.user = {
22
+ uid: req.headers?.uid?.toString?.(),
23
+ user_type: req.headers?.user_type?.toString?.() || "regular",
24
+ };
25
+ }
26
+ const isAdmin = process.env.NODE_ENV === "admin" ||
27
+ hostname?.split?.(":")?.shift?.() === config.adminDomain ||
28
+ config.admin ||
29
+ hostname?.startsWith?.("admin");
30
+ const isPublic = Array.isArray(policy)
31
+ ? policy.includes("public")
32
+ : policy === "L0";
33
+ if (req.cookies?.["session_auth"] &&
34
+ !req.session?.passport?.user?.uid &&
35
+ (config.auth?.disable || config.auth?.user)) {
36
+ req.session = req.session || {};
37
+ req.session.passport = req.session.passport || {}; // ensure passport session exists
38
+ req.session.passport.user = {
39
+ ...(config.auth?.user || {}),
40
+ uid: config.auth?.user?.uid?.toString?.() || "1",
41
+ user_rnokpp: config.auth?.user?.rnokpp,
42
+ user_type: config.auth?.user?.type || "regular",
43
+ };
44
+ req.user = req.session.passport.user;
45
+ }
46
+ // ! intentional: null || undefined > undefined
47
+ req.user = req.user || req.session?.passport?.user || undefined; // fix for user.uid errors, by default user is null, while with express passport it was {}, unauthorized user does not trigger serializer
48
+ // currently 2factor + auth with passwd file not supported
49
+ const ispasswd = (existsSync("passwd") && !config.auth?.["2factor"]) || config.auth?.passwd;
50
+ const loginPageUrl = config.auth?.link?.core?.login || config?.auth?.redirect || "/login";
51
+ if (!req.user?.uid &&
52
+ !config.auth?.disable &&
53
+ isAdmin &&
54
+ !isPublic &&
55
+ !config.auth?.disableRedirect &&
56
+ !req.url.startsWith(prefix) &&
57
+ !req.url.startsWith("/api") &&
58
+ !req.url.includes(loginPageUrl) &&
59
+ !req.url.includes(".") &&
60
+ !req.url.includes("@")) {
61
+ if (isApi) {
62
+ return reply.status(401).send({ error: "unauthorized", code: 401 });
63
+ }
64
+ return reply.redirect(`${loginPageUrl}` + `?redirect=${req.url}`);
65
+ }
66
+ // by default, disable 2factor for id.gov.ua auth
67
+ const check = req.user?.auth_type === "govid" ? config.auth?.["2factor"]?.govid : true;
68
+ const login2faPage = config.auth?.link?.["2fa"]?.login || "/2factor";
69
+ // example: 2factor for admin env only, while public env does not require it
70
+ const checkEnv = () => {
71
+ if (!config.auth?.["2factorEnv"])
72
+ return true;
73
+ if ((config.auth?.["2factorEnv"] &&
74
+ process.env.NODE_ENV === config.auth?.["2factorEnv"]) ||
75
+ (config.auth?.["2factorEnv"] === "admin" && isAdmin)) {
76
+ return true;
77
+ }
78
+ return false;
79
+ };
80
+ // if 2factor is enabled globally + for user and secondFactorPassed not true => redirect to 2factor login page
81
+ if (req.user?.uid &&
82
+ req.user?.twofa &&
83
+ // config.auth?.["2factor"] &&
84
+ // !isPublic &&
85
+ (routeOptions?.method || "GET") === "GET" &&
86
+ !req.session?.secondFactorPassed &&
87
+ !ispasswd &&
88
+ !config.auth?.disableRedirect &&
89
+ !config.auth?.disable &&
90
+ check &&
91
+ checkEnv() &&
92
+ !req.url.startsWith(login2faPage) &&
93
+ !routeOptions.url?.includes?.("/logout") &&
94
+ !routeOptions.url?.includes?.("/2fa") &&
95
+ !routeOptions.url?.includes?.("/assets")) {
96
+ if (isApi) {
97
+ return reply
98
+ .status(403)
99
+ .send({ error: "access restricted: twofa", code: 403 });
100
+ }
101
+ return reply.redirect(login2faPage);
102
+ }
103
+ return null;
104
+ }
@@ -30,7 +30,7 @@ export default function getFilterSQL({ table, filter, pg, search, searchColumn:
30
30
  q: string;
31
31
  optimizedSQL: string;
32
32
  sqlTable: any;
33
- extraSqlColumns: any;
33
+ extraSqlColumns: string | undefined;
34
34
  tableCount: string;
35
35
  table: any;
36
36
  searchQuery: string;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../../server/plugins/table/funcs/getFilterSQL/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAsC5D,wBAA8B,YAAY,CAAC,EACzC,KAAK,EACL,MAAM,EACN,EAAqB,EACrB,MAAM,EACN,YAAY,EAAE,aAAa,EAC3B,UAAU,EACV,KAAK,EACL,MAAM,EACN,KAAK,EACL,GAAG,EACH,QAAQ,EACR,KAAK,GACN,EAAE;IACD,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,UAAU,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,GAAG,EAAE,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;;;;;;;;;;;;;;;;;;;;;;;;GAsPA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../../server/plugins/table/funcs/getFilterSQL/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAsC5D,wBAA8B,YAAY,CAAC,EACzC,KAAK,EACL,MAAM,EACN,EAAqB,EACrB,MAAM,EACN,YAAY,EAAE,aAAa,EAC3B,UAAU,EACV,KAAK,EACL,MAAM,EACN,KAAK,EACL,GAAG,EACH,QAAQ,EACR,KAAK,GACN,EAAE;IACD,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,UAAU,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,GAAG,EAAE,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;;;;;;;;;;;;;;;;;;;;;;;;GA4QA"}
@@ -5,6 +5,7 @@ import config from "../../../../../config.js";
5
5
  // filter util
6
6
  import getTableSql from "./util/getTableSql.js";
7
7
  import getFilterQuery from "./util/getFilterQuery.js";
8
+ import { getTableColumnMeta } from "../../../../routes/table/controllers/suggest.js";
8
9
  import getOptimizedQuery from "./util/getOptimizedQuery.js";
9
10
  import getFilter from "../getFilter.js";
10
11
  const checkInline = {};
@@ -105,11 +106,11 @@ export default async function getFilterSQL({ table, filter, pg = pgClients.clien
105
106
  })
106
107
  .join(" or ")} )`
107
108
  : "";
108
- const filterResponse = (await getFilter({
109
+ const filterResponse = await getFilter({
109
110
  pg,
110
111
  table,
111
112
  user: { uid },
112
- }, null));
113
+ }, null);
113
114
  const filterList1 = await Promise.all((filterList || [])
114
115
  .concat(filterResponse?.inline || [])
115
116
  .concat(filterResponse?.custom || [])
@@ -118,8 +119,7 @@ export default async function getFilterSQL({ table, filter, pg = pgClients.clien
118
119
  ?.filter((el) => el.id || el.name)
119
120
  ?.map(async (el) => {
120
121
  Object.assign(el, { name: el.name || el.id });
121
- if (el.name &&
122
- extraColumns.find((item) => item?.name === el?.name)) {
122
+ if (el.name && extraColumns.find((item) => item?.name === el?.name)) {
123
123
  Object.assign(el, {
124
124
  extra: {
125
125
  table: extraDataTable,
@@ -128,6 +128,30 @@ export default async function getFilterSQL({ table, filter, pg = pgClients.clien
128
128
  },
129
129
  });
130
130
  }
131
+ if (el.api?.split?.("/")?.[2] === "suggest" &&
132
+ el.api?.split?.("/")?.[3]?.split?.(":")?.length === 2) {
133
+ const [table, column] = el.api.split("/")[3].split(":");
134
+ const body = await getTemplate("table", table);
135
+ const { arr, original, pk } = (await getTableColumnMeta({
136
+ table: body?.table || table,
137
+ template: body ? table : undefined,
138
+ column,
139
+ selectName: body?.columns?.find?.((el) => el.name === column)
140
+ ?.data || body?.meta?.cls?.[column],
141
+ pg,
142
+ })) || {};
143
+ const arr1 = arr ||
144
+ (original && pg?.queryCache
145
+ ? await pg
146
+ .queryCache(original, { table: body?.table || table })
147
+ .then((el) => el.rows || [])
148
+ : undefined);
149
+ Object.assign(el, {
150
+ optionsFromColumns: true,
151
+ options: arr1,
152
+ select: original,
153
+ });
154
+ }
131
155
  if (!el?.data)
132
156
  return el;
133
157
  // const cls = await getTemplate(['cls', 'select'], el.data); // only git cls
@@ -174,7 +198,7 @@ export default async function getFilterSQL({ table, filter, pg = pgClients.clien
174
198
  stateQuery,
175
199
  customQuery,
176
200
  ]
177
- .filter((el) => el)
201
+ .filter(Boolean)
178
202
  .join(" and ");
179
203
  // table
180
204
  const obj = {
@@ -1 +1 @@
1
- {"version":3,"file":"formatValue.d.ts","sourceRoot":"","sources":["../../../../../../../server/plugins/table/funcs/getFilterSQL/util/formatValue.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAe/D,MAAM,CAAC,OAAO,UAAU,WAAW,CAAC,EAClC,EAAE,EACF,KAAK,EACL,MAAW,EACX,IAAI,EACJ,KAAK,EACL,UAAU,EACV,GAAO,EACP,QAAQ,GACT,EAAE;IACD,EAAE,EAAE,UAAU,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC5B,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,GAAG,CAAC;CAChB,OAmLA"}
1
+ {"version":3,"file":"formatValue.d.ts","sourceRoot":"","sources":["../../../../../../../server/plugins/table/funcs/getFilterSQL/util/formatValue.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAe/D,MAAM,CAAC,OAAO,UAAU,WAAW,CAAC,EAClC,EAAE,EACF,KAAK,EACL,MAAW,EACX,IAAI,EACJ,KAAK,EACL,UAAU,EACV,GAAO,EACP,QAAQ,GACT,EAAE;IACD,EAAE,EAAE,UAAU,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC5B,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,GAAG,CAAC;CAChB,OAqMA"}
@@ -10,7 +10,7 @@ const selectFilterTypes = [
10
10
  "button",
11
11
  ];
12
12
  export default function formatValue({ pg, table, filter = {}, name, value, dataTypeID, uid = 1, optimize, }) {
13
- const { extra, sql, select, strict, options /* default: defaultValue, */ } = filter;
13
+ const { extra, sql, select, strict, options, optionsFromColumns /* default: defaultValue, */, } = filter;
14
14
  const pk = pg?.pk && table ? pg.pk[table] : undefined;
15
15
  const filterType = filter.type?.toLowerCase?.() || "text";
16
16
  if (filterType === "text" && value && filter?.columns) {
@@ -132,6 +132,17 @@ export default function formatValue({ pg, table, filter = {}, name, value, dataT
132
132
  const query = `${pk} in (select object_id from ${extra.table} where property_key='${name}' and value_text ${matchNull || `='${value}'`} )`;
133
133
  return { op: "=", query };
134
134
  }
135
+ // if pseudo-suggest column contain commas
136
+ if (optionsFromColumns && options?.length) {
137
+ const optionValues = options.map((e) => e.id);
138
+ const vals = optionValues.filter((e) => value.includes(e));
139
+ return {
140
+ op: "=",
141
+ query: `${name}::text=any('{${vals
142
+ .map((v) => `"${v}"`)
143
+ .join(",")}}'::text[])`,
144
+ };
145
+ }
135
146
  // nullable + default
136
147
  const query = `${name} ${match}`;
137
148
  return { op: "=", query };
@@ -1 +1 @@
1
- {"version":3,"file":"getFilterQuery.d.ts","sourceRoot":"","sources":["../../../../../../../server/plugins/table/funcs/getFilterSQL/util/getFilterQuery.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAK/D;;;;GAIG;AAEH,iBAAS,cAAc,CAAC,EACtB,EAAqB,EACrB,MAAM,EAAE,SAAc,EACtB,KAAU,EACV,QAAa,EACb,MAAM,EACN,UAAU,EACV,GAAG,EACH,QAAQ,GACT,EAAE;IACD,EAAE,EAAE,UAAU,CAAC;IACf,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,GAAG,EAAE,CAAC;IAChB,MAAM,CAAC,EAAE,GAAG,EAAE,CAAC;IACf,UAAU,CAAC,EAAE,GAAG,EAAE,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,OAgGA;AAED,eAAe,cAAc,CAAC"}
1
+ {"version":3,"file":"getFilterQuery.d.ts","sourceRoot":"","sources":["../../../../../../../server/plugins/table/funcs/getFilterSQL/util/getFilterQuery.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAK/D;;;;GAIG;AAEH,iBAAS,cAAc,CAAC,EACtB,EAAqB,EACrB,MAAM,EAAE,SAAc,EACtB,KAAU,EACV,QAAa,EACb,MAAM,EACN,UAAU,EACV,GAAG,EACH,QAAQ,GACT,EAAE;IACD,EAAE,EAAE,UAAU,CAAC;IACf,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,GAAG,EAAE,CAAC;IAChB,MAAM,CAAC,EAAE,GAAG,EAAE,CAAC;IACf,UAAU,CAAC,EAAE,GAAG,EAAE,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,OA8FA;AAED,eAAe,cAAc,CAAC"}
@@ -25,9 +25,7 @@ function getFilterQuery({ pg = pgClients.client, filter: filterStr = "", table =
25
25
  ?.map?.((el) => `${el.name}=${el.default}`) || []
26
26
  : [];
27
27
  // concat default + request filters
28
- const arr = filterQueryArray
29
- .concat(filterDefaultQueryArray)
30
- .filter((el) => el);
28
+ const arr = filterQueryArray.concat(filterDefaultQueryArray).filter(Boolean);
31
29
  const resultList = [];
32
30
  for (let i = 0; i < arr.length; i += 1) {
33
31
  const item = arr[i];
@@ -30,7 +30,7 @@ export default async function accessGroupPost({ pg = pgClients.client, params, u
30
30
  .map((el) => pg.query("insert into admin.role_access(role_id,route_id,actions) values ($1,$2,$3)", [id, el.path, el.actions])));
31
31
  await Promise.all(routes
32
32
  .filter((el) => el.resource && el.actions)
33
- .map((el) => pg.query("insert into admin.role_access(resource_id,route_id,actions) values ($1,$2,$3)", [id, el.path, el.actions])));
33
+ .map((el) => pg.query("insert into admin.role_access(role_id,resource_id,route_id,actions) values ($1,$2,$3,$4)", [id, el.resource, el.path, el.actions])));
34
34
  const rows = await pg
35
35
  .query(`select a.route_id as path, b.actions as actions from admin.routes a
36
36
  left join admin.role_access b on a.route_id=b.route_id
@@ -0,0 +1,6 @@
1
+ declare const arr: any;
2
+ declare const publicResources: any[];
3
+ declare const accessResources: (req?: any) => Promise<any[]>;
4
+ export { arr as resources, publicResources };
5
+ export default accessResources;
6
+ //# sourceMappingURL=access.resources.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"access.resources.d.ts","sourceRoot":"","sources":["../../../../../server/routes/access/controllers/access.resources.ts"],"names":[],"mappings":"AAEA,QAAA,MAAM,GAAG,KAEH,CAAC;AAEP,QAAA,MAAM,eAAe,EAAE,GAAG,EAIvB,CAAC;AAEJ,QAAA,MAAM,eAAe,GAAU,MAAM,GAAG,KAAG,OAAO,CAAC,GAAG,EAAE,CAEvD,CAAC;AAEF,OAAO,EAAE,GAAG,IAAI,SAAS,EAAE,eAAe,EAAE,CAAC;AAC7C,eAAe,eAAe,CAAC"}
@@ -0,0 +1,14 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ const arr = existsSync("config/resources.json")
3
+ ? JSON.parse(readFileSync("config/resources.json", "utf-8") || "[]")
4
+ : [];
5
+ const publicResources = arr.map(({ name, actions, feature }) => ({
6
+ name,
7
+ actions,
8
+ feature,
9
+ }));
10
+ const accessResources = async (req) => {
11
+ return publicResources;
12
+ };
13
+ export { arr as resources, publicResources };
14
+ export default accessResources;
@@ -0,0 +1,11 @@
1
+ declare const arr: any;
2
+ interface Resource {
3
+ name: string;
4
+ actions: string[];
5
+ feature: string;
6
+ }
7
+ declare const publicResources: Resource[];
8
+ declare const resources: (req?: any) => Promise<Resource[]>;
9
+ export { arr as resources, publicResources };
10
+ export default resources;
11
+ //# sourceMappingURL=resources.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resources.d.ts","sourceRoot":"","sources":["../../../../../server/routes/access/controllers/resources.ts"],"names":[],"mappings":"AAEA,QAAA,MAAM,GAAG,KAEH,CAAC;AAEP,UAAU,QAAQ;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,QAAA,MAAM,eAAe,EAAE,QAAQ,EAM9B,CAAC;AAEF,QAAA,MAAM,SAAS,GAAU,MAAM,GAAG,KAAG,OAAO,CAAC,QAAQ,EAAE,CAEtD,CAAC;AAEF,OAAO,EAAE,GAAG,IAAI,SAAS,EAAE,eAAe,EAAE,CAAC;AAC7C,eAAe,SAAS,CAAC"}
@@ -0,0 +1,14 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ const arr = existsSync("config/resources.json")
3
+ ? JSON.parse(readFileSync("config/resources.json", "utf-8") || "[]")
4
+ : [];
5
+ const publicResources = arr.map(({ name, actions, feature }) => ({
6
+ name,
7
+ actions,
8
+ feature,
9
+ }));
10
+ const resources = async (req) => {
11
+ return publicResources;
12
+ };
13
+ export { arr as resources, publicResources };
14
+ export default resources;
@@ -0,0 +1,6 @@
1
+ declare const resourcesList: any;
2
+ declare const publicResourcesList: any[];
3
+ export { resourcesList, publicResourcesList };
4
+ declare const _default: null;
5
+ export default _default;
6
+ //# sourceMappingURL=resources.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resources.d.ts","sourceRoot":"","sources":["../../../../../server/routes/access/functions/resources.ts"],"names":[],"mappings":"AAEA,QAAA,MAAM,aAAa,KAEb,CAAC;AAEP,QAAA,MAAM,mBAAmB,EAAE,GAAG,EAM7B,CAAC;AAEF,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,CAAC;;AAC9C,wBAAoB"}
@@ -0,0 +1,11 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ const resourcesList = existsSync("config/resources.json")
3
+ ? JSON.parse(readFileSync("config/resources.json", "utf-8") || "[]")
4
+ : [];
5
+ const publicResourcesList = resourcesList.map(({ name, actions, feature }) => ({
6
+ name,
7
+ actions,
8
+ feature,
9
+ }));
10
+ export { resourcesList, publicResourcesList };
11
+ export default null;
@@ -1 +1 @@
1
- {"version":3,"file":"resize.d.ts","sourceRoot":"","sources":["../../../../../server/routes/file/controllers/resize.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAoC5C;;GAEG;AAEH,wBAA8B,MAAM,CAClC,EACE,KAAK,GACN,EAAE;IACD,KAAK,EAAE;QACL,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;QAC1B,IAAI,EAAE,MAAM,CAAC;QACb,CAAC,CAAC,EAAE,MAAM,CAAC;QACX,CAAC,CAAC,EAAE,MAAM,CAAC;QACX,OAAO,CAAC,EAAE,GAAG,CAAC;QACd,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,GAAG,CAAC;KACf,CAAC;CACH,EACD,KAAK,EAAE,YAAY,kBAwJpB"}
1
+ {"version":3,"file":"resize.d.ts","sourceRoot":"","sources":["../../../../../server/routes/file/controllers/resize.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAoC5C;;GAEG;AAEH,wBAA8B,MAAM,CAClC,EACE,KAAK,GACN,EAAE;IACD,KAAK,EAAE;QACL,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;QAC1B,IAAI,EAAE,MAAM,CAAC;QACb,CAAC,CAAC,EAAE,MAAM,CAAC;QACX,CAAC,CAAC,EAAE,MAAM,CAAC;QACX,OAAO,CAAC,EAAE,GAAG,CAAC;QACd,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,GAAG,CAAC;KACf,CAAC;CACH,EACD,KAAK,EAAE,YAAY,kBAwIpB"}
@@ -36,21 +36,6 @@ export default async function resize({ query, }, reply) {
36
36
  .status(400)
37
37
  .send({ error: "invalid query params: filepath", code: 400 });
38
38
  }
39
- // svg не підтримується
40
- if (mimeType === "image/svg+xml") {
41
- const fileData = await downloadFile(filepath, { buffer: true });
42
- if (!fileData?.length) {
43
- return reply
44
- .status(404)
45
- .send({ error: `Файл не знайдено - ${filepath}`, code: 400 });
46
- }
47
- return reply
48
- .headers({
49
- "Content-Type": "image/svg+xml",
50
- "Cache-control": "max-age=604800",
51
- })
52
- .send(fileData);
53
- }
54
39
  const resizePath1 = size
55
40
  ? filepath.replace(basename, `${size}_resized_${basename}`)
56
41
  : filepath.replace(basename, `${w || defaultWidth}_${h || (w ? "" : defaultHeight)}_resized_${basename}`);
@@ -1,2 +1,18 @@
1
+ import { ExtendedPG } from "../../../types/core.js";
2
+ export declare function getTableColumnMeta({ table, template, column, selectName, filtered, startsWith, key, pg, }: {
3
+ table: string;
4
+ template?: string;
5
+ column: any;
6
+ selectName?: string;
7
+ filtered?: any;
8
+ startsWith?: boolean;
9
+ key?: string;
10
+ pg?: ExtendedPG;
11
+ }): Promise<{
12
+ arr: any;
13
+ original: string;
14
+ searchQuery: string;
15
+ pk: string;
16
+ } | null>;
1
17
  export default function suggest(req: any, reply: any): Promise<any>;
2
18
  //# sourceMappingURL=suggest.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"suggest.d.ts","sourceRoot":"","sources":["../../../../../server/routes/table/controllers/suggest.ts"],"names":[],"mappings":"AA0HA,wBAA8B,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,gBA6WzD"}
1
+ {"version":3,"file":"suggest.d.ts","sourceRoot":"","sources":["../../../../../server/routes/table/controllers/suggest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAuBpD,wBAAsB,kBAAkB,CAAC,EACvC,KAAK,EACL,QAAQ,EACR,MAAM,EACN,UAAU,EACV,QAAQ,EACR,UAAU,EACV,GAAG,EACH,EAAqB,GACtB,EAAE;IACD,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,GAAG,CAAC;IACZ,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,GAAG,CAAC;IACf,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,EAAE,CAAC,EAAE,UAAU,CAAC;CACjB;;;;;UA+EA;AAED,wBAA8B,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,gBA6WzD"}
@@ -2,7 +2,7 @@ import path from "node:path";
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import { config, getPGAsync, getTemplate, getSelectMeta, getMeta, applyHook, getSelectVal, logger, getSelect, metaFormat, getColumnCLS, pgClients, getFilter, } from "../../../../utils.js";
4
4
  const defaultLimit = 50;
5
- async function getTableColumnMeta({ table, template, column, selectName, filtered, startsWith, key, pg = pgClients.client, }) {
5
+ export async function getTableColumnMeta({ table, template, column, selectName, filtered, startsWith, key, pg = pgClients.client, }) {
6
6
  if (!table || !column) {
7
7
  return null;
8
8
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengis/fastify-table",
3
- "version": "2.1.9",
3
+ "version": "2.1.11",
4
4
  "type": "module",
5
5
  "description": "core-plugins",
6
6
  "keywords": [
@@ -1 +0,0 @@
1
- [{"name":"core.roles","module":"admin","type":"select","hash":"d5d31d7e7fe19912d4d45bacb3aab3d3","dbhash":"d5d31d7e7fe19912d4d45bacb3aab3d3","update":false},{"name":"core.routes","module":"admin","type":"select","hash":"431032fdbe25c2db997391f61c6e944f","dbhash":"431032fdbe25c2db997391f61c6e944f","update":false},{"name":"core.user_uid","module":"admin","type":"select","hash":"45f7d8a57ecbca5b433be7937be51d5c","dbhash":"45f7d8a57ecbca5b433be7937be51d5c","update":false},{"name":"data.product","module":"shop","type":"select","hash":"9f9f99a1fb323ed0a32823cff47e59c7","dbhash":"9f9f99a1fb323ed0a32823cff47e59c7","update":false},{"name":"shop.categories","module":"shop","type":"select","hash":"549584bc96bd5ab14f872d4ef8df3d82","dbhash":"549584bc96bd5ab14f872d4ef8df3d82","update":false},{"name":"shop.products","module":"shop","type":"select","hash":"099ec0587edd16f26a1d05395a21709e","dbhash":"099ec0587edd16f26a1d05395a21709e","update":false},{"name":"shop.types","module":"shop","type":"select","hash":"a110b4d5bbe74557a7272df65ab10c3e","dbhash":"a110b4d5bbe74557a7272df65ab10c3e","update":false},{"name":"shop.types_parent","module":"shop","type":"select","hash":"14fa482423740f452139ce622f961740","dbhash":"14fa482423740f452139ce622f961740","update":false},{"name":"core.user_mentioned","module":"core","type":"select","hash":"6687f073de73a3ec4b6e0811d9310e7e","dbhash":"5a35e8ca97ce4fde93030521104ca5d3","update":true},{"name":"users.user_type","module":"admin","type":"cls","hash":"065fe92f48196a961a35f48ddd6ba9bd","dbhash":"065fe92f48196a961a35f48ddd6ba9bd","update":false},{"name":"yes_no","module":"admin","type":"cls","hash":"50c527053426248c20b0a2f112ff9046","dbhash":"50c527053426248c20b0a2f112ff9046","update":false},{"name":"currency","module":"shop","type":"cls","hash":"99ee673ab4ac2c222c0272bb7ef8079e","dbhash":"99ee673ab4ac2c222c0272bb7ef8079e","update":false},{"name":"delivery_method","module":"shop","type":"cls","hash":"953a6e6de0e83d7468393bd02e179b05","dbhash":"953a6e6de0e83d7468393bd02e179b05","update":false},{"name":"order_items.order_type","module":"shop","type":"cls","hash":"d99d36a3563ec77131b0800044df5fd0","dbhash":"d99d36a3563ec77131b0800044df5fd0","update":false},{"name":"order_status","module":"shop","type":"cls","hash":"65049e14b7d1faa6c2b0e837ff41f37d","dbhash":"65049e14b7d1faa6c2b0e837ff41f37d","update":false},{"name":"constraint_action","module":"core","type":"cls","hash":"1b7129eae9eb42106ed6e646223c806a","dbhash":"1b7129eae9eb42106ed6e646223c806a","update":false},{"name":"constraint_matchtype","module":"core","type":"cls","hash":"446ad903e69a391748a8a27bae2dc5cd","dbhash":"446ad903e69a391748a8a27bae2dc5cd","update":false},{"name":"constraint_type_full","module":"core","type":"cls","hash":"9e1cc580273f7e73fbc08ee553ad8f64","dbhash":"9e1cc580273f7e73fbc08ee553ad8f64","update":false},{"name":"core.user_type","module":"core","type":"cls","hash":"728bc9e0bcc88de83ec56d8dc7e7efff","dbhash":"728bc9e0bcc88de83ec56d8dc7e7efff","update":false}]
@@ -1,2 +0,0 @@
1
- insert into admin.cls(name,type,data,module,hash) values('core.user_mentioned','sql','select uid, coalesce(sur_name,'''')||coalesce('' ''||user_name,'''') as text, email from admin.users
2
- where enabled order by coalesce(sur_name,'''')||coalesce('' ''||user_name,'''')', 'core','6687f073de73a3ec4b6e0811d9310e7e')