@nocobase/plugin-data-source-main 2.1.0-beta.30 → 2.1.0-beta.33
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/externalVersion.js +8 -8
- package/dist/server/modeling/collections.d.ts +14 -0
- package/dist/server/modeling/collections.js +23 -0
- package/dist/server/modeling/constants.d.ts +1 -1
- package/dist/server/modeling/constants.js +15 -1
- package/dist/server/modeling/fields.js +65 -0
- package/dist/swagger/index.d.ts +81 -0
- package/dist/swagger/index.js +66 -0
- package/package.json +2 -2
package/dist/externalVersion.js
CHANGED
|
@@ -8,16 +8,16 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
module.exports = {
|
|
11
|
-
"@nocobase/client": "2.1.0-beta.
|
|
11
|
+
"@nocobase/client": "2.1.0-beta.33",
|
|
12
12
|
"lodash": "4.18.1",
|
|
13
|
-
"@nocobase/ai": "2.1.0-beta.
|
|
14
|
-
"@nocobase/database": "2.1.0-beta.
|
|
15
|
-
"@nocobase/plugin-error-handler": "2.1.0-beta.
|
|
16
|
-
"@nocobase/server": "2.1.0-beta.
|
|
13
|
+
"@nocobase/ai": "2.1.0-beta.33",
|
|
14
|
+
"@nocobase/database": "2.1.0-beta.33",
|
|
15
|
+
"@nocobase/plugin-error-handler": "2.1.0-beta.33",
|
|
16
|
+
"@nocobase/server": "2.1.0-beta.33",
|
|
17
17
|
"sequelize": "6.35.2",
|
|
18
18
|
"@formily/json-schema": "2.3.7",
|
|
19
|
-
"@nocobase/test": "2.1.0-beta.
|
|
20
|
-
"@nocobase/utils": "2.1.0-beta.
|
|
21
|
-
"@nocobase/actions": "2.1.0-beta.
|
|
19
|
+
"@nocobase/test": "2.1.0-beta.33",
|
|
20
|
+
"@nocobase/utils": "2.1.0-beta.33",
|
|
21
|
+
"@nocobase/actions": "2.1.0-beta.33",
|
|
22
22
|
"dayjs": "1.11.13"
|
|
23
23
|
};
|
|
@@ -63,6 +63,20 @@ export declare function buildTemplateBaseline(input: PlainObject): {
|
|
|
63
63
|
tree?: undefined;
|
|
64
64
|
schema?: undefined;
|
|
65
65
|
inherits?: undefined;
|
|
66
|
+
} | {
|
|
67
|
+
template: "comment";
|
|
68
|
+
logging: any;
|
|
69
|
+
autoGenId: boolean;
|
|
70
|
+
fields: PlainObject[];
|
|
71
|
+
view?: undefined;
|
|
72
|
+
tree?: undefined;
|
|
73
|
+
createdBy?: undefined;
|
|
74
|
+
updatedBy?: undefined;
|
|
75
|
+
createdAt?: undefined;
|
|
76
|
+
updatedAt?: undefined;
|
|
77
|
+
sortable?: undefined;
|
|
78
|
+
schema?: undefined;
|
|
79
|
+
inherits?: undefined;
|
|
66
80
|
} | {
|
|
67
81
|
template: "view";
|
|
68
82
|
view: boolean;
|
|
@@ -209,6 +209,29 @@ function buildTemplateBaseline(input) {
|
|
|
209
209
|
})
|
|
210
210
|
]
|
|
211
211
|
};
|
|
212
|
+
case "comment":
|
|
213
|
+
return {
|
|
214
|
+
template,
|
|
215
|
+
logging: input.logging ?? true,
|
|
216
|
+
autoGenId: false,
|
|
217
|
+
fields: [
|
|
218
|
+
(0, import_fields.normalizeFieldInput)({
|
|
219
|
+
name: "content",
|
|
220
|
+
interface: "vditor",
|
|
221
|
+
type: "text",
|
|
222
|
+
length: "long",
|
|
223
|
+
deletable: false,
|
|
224
|
+
title: "Comment Content"
|
|
225
|
+
}),
|
|
226
|
+
...buildPresetFields({
|
|
227
|
+
includeId: true,
|
|
228
|
+
includeCreatedAt: true,
|
|
229
|
+
includeCreatedBy: true,
|
|
230
|
+
includeUpdatedAt: true,
|
|
231
|
+
includeUpdatedBy: true
|
|
232
|
+
})
|
|
233
|
+
]
|
|
234
|
+
};
|
|
212
235
|
case "view":
|
|
213
236
|
return { template, view: true, schema: input.schema || "public" };
|
|
214
237
|
case "inherit":
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
export type PlainObject = Record<string, any>;
|
|
10
|
-
export type TemplateName = 'general' | 'tree' | 'file' | 'calendar' | 'sql' | 'view' | 'inherit';
|
|
10
|
+
export type TemplateName = 'general' | 'tree' | 'file' | 'calendar' | 'comment' | 'sql' | 'view' | 'inherit';
|
|
11
11
|
export declare const TEMPLATE_NAMES: TemplateName[];
|
|
12
12
|
export declare const MULTI_COMPONENT_INTERFACES: Set<string>;
|
|
13
13
|
export declare const ARRAY_DEFAULT_INTERFACES: Set<string>;
|
|
@@ -51,7 +51,16 @@ __export(constants_exports, {
|
|
|
51
51
|
});
|
|
52
52
|
module.exports = __toCommonJS(constants_exports);
|
|
53
53
|
var import_lodash = __toESM(require("lodash"));
|
|
54
|
-
const TEMPLATE_NAMES = [
|
|
54
|
+
const TEMPLATE_NAMES = [
|
|
55
|
+
"general",
|
|
56
|
+
"tree",
|
|
57
|
+
"file",
|
|
58
|
+
"calendar",
|
|
59
|
+
"comment",
|
|
60
|
+
"sql",
|
|
61
|
+
"view",
|
|
62
|
+
"inherit"
|
|
63
|
+
];
|
|
55
64
|
const MULTI_COMPONENT_INTERFACES = /* @__PURE__ */ new Set([
|
|
56
65
|
"multipleSelect",
|
|
57
66
|
"checkboxGroup",
|
|
@@ -108,6 +117,11 @@ const PLUGIN_REQUIREMENTS = {
|
|
|
108
117
|
packageName: "@nocobase/plugin-field-attachment-url",
|
|
109
118
|
capability: "attachmentURL field"
|
|
110
119
|
},
|
|
120
|
+
comment: {
|
|
121
|
+
runtimeName: "comments",
|
|
122
|
+
packageName: "@nocobase/plugin-comments",
|
|
123
|
+
capability: "comment collection template"
|
|
124
|
+
},
|
|
111
125
|
vditor: {
|
|
112
126
|
runtimeName: "field-markdown-vditor",
|
|
113
127
|
packageName: "@nocobase/plugin-field-markdown-vditor",
|
|
@@ -44,6 +44,7 @@ __export(fields_exports, {
|
|
|
44
44
|
module.exports = __toCommonJS(fields_exports);
|
|
45
45
|
var import_lodash = __toESM(require("lodash"));
|
|
46
46
|
var import_sequelize = require("sequelize");
|
|
47
|
+
var import_utils = require("@nocobase/utils");
|
|
47
48
|
var import_constants = require("./constants");
|
|
48
49
|
function normalizeInterfaceName(value) {
|
|
49
50
|
if (!value) {
|
|
@@ -159,6 +160,39 @@ const ALLOWED_FIELD_TYPES = {
|
|
|
159
160
|
tableoid: ["virtual"],
|
|
160
161
|
mbm: ["belongsToArray"]
|
|
161
162
|
};
|
|
163
|
+
const VALIDATION_TYPE_BY_INTERFACE = {
|
|
164
|
+
input: "string",
|
|
165
|
+
phone: "string",
|
|
166
|
+
email: "string",
|
|
167
|
+
color: "string",
|
|
168
|
+
icon: "string",
|
|
169
|
+
url: "string",
|
|
170
|
+
textarea: "string",
|
|
171
|
+
markdown: "string",
|
|
172
|
+
vditor: "string",
|
|
173
|
+
richText: "string",
|
|
174
|
+
password: "string",
|
|
175
|
+
uuid: "string",
|
|
176
|
+
nanoid: "string",
|
|
177
|
+
code: "string",
|
|
178
|
+
sequence: "string",
|
|
179
|
+
encryption: "string",
|
|
180
|
+
integer: "number",
|
|
181
|
+
number: "number",
|
|
182
|
+
percent: "number",
|
|
183
|
+
unixTimestamp: "number",
|
|
184
|
+
snowflakeId: "number",
|
|
185
|
+
id: "number",
|
|
186
|
+
sort: "number",
|
|
187
|
+
checkbox: "boolean",
|
|
188
|
+
json: "object",
|
|
189
|
+
datetime: "date",
|
|
190
|
+
createdAt: "date",
|
|
191
|
+
updatedAt: "date",
|
|
192
|
+
datetimeNoTz: "date",
|
|
193
|
+
dateOnly: "date",
|
|
194
|
+
time: "date"
|
|
195
|
+
};
|
|
162
196
|
function buildRelationComponentProps(field) {
|
|
163
197
|
return {
|
|
164
198
|
multiple: import_constants.MULTI_COMPONENT_INTERFACES.has(field.interface),
|
|
@@ -380,6 +414,36 @@ function validateFieldType(field) {
|
|
|
380
414
|
);
|
|
381
415
|
}
|
|
382
416
|
}
|
|
417
|
+
function inferValidationType(field) {
|
|
418
|
+
var _a;
|
|
419
|
+
return ((_a = field.validation) == null ? void 0 : _a.type) || VALIDATION_TYPE_BY_INTERFACE[field.interface] || "string";
|
|
420
|
+
}
|
|
421
|
+
function normalizeValidationRule(rule) {
|
|
422
|
+
if (typeof rule === "string") {
|
|
423
|
+
return {
|
|
424
|
+
key: `r_${(0, import_utils.uid)()}`,
|
|
425
|
+
name: rule
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
return {
|
|
429
|
+
...rule,
|
|
430
|
+
key: (rule == null ? void 0 : rule.key) || `r_${(0, import_utils.uid)()}`
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
function normalizeFieldValidation(field) {
|
|
434
|
+
var _a;
|
|
435
|
+
const rulesInput = field.validators || ((_a = field.validation) == null ? void 0 : _a.rules);
|
|
436
|
+
if (!rulesInput) {
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
const rules = (Array.isArray(rulesInput) ? rulesInput : [rulesInput]).map(normalizeValidationRule);
|
|
440
|
+
field.validation = {
|
|
441
|
+
...field.validation || {},
|
|
442
|
+
type: inferValidationType(field),
|
|
443
|
+
rules
|
|
444
|
+
};
|
|
445
|
+
delete field.validators;
|
|
446
|
+
}
|
|
383
447
|
function buildRelationKeyName(name, key = "id") {
|
|
384
448
|
return import_sequelize.Utils.camelize([import_sequelize.Utils.singularize(name || ""), key].join("_"));
|
|
385
449
|
}
|
|
@@ -471,6 +535,7 @@ function normalizeFieldInput(input, context = {}) {
|
|
|
471
535
|
throw new Error(`Field ${field.name || "(unknown)"} is missing interface`);
|
|
472
536
|
}
|
|
473
537
|
validateFieldType(field);
|
|
538
|
+
normalizeFieldValidation(field);
|
|
474
539
|
if (import_constants.RELATION_INTERFACES.has(field.interface) && !field.target) {
|
|
475
540
|
throw new Error(`Relation field ${field.name} requires target`);
|
|
476
541
|
}
|
package/dist/swagger/index.d.ts
CHANGED
|
@@ -1269,6 +1269,63 @@ declare const _default: {
|
|
|
1269
1269
|
};
|
|
1270
1270
|
additionalProperties: boolean;
|
|
1271
1271
|
};
|
|
1272
|
+
validationRule: {
|
|
1273
|
+
type: string;
|
|
1274
|
+
description: string;
|
|
1275
|
+
properties: {
|
|
1276
|
+
key: {
|
|
1277
|
+
type: string;
|
|
1278
|
+
description: string;
|
|
1279
|
+
};
|
|
1280
|
+
name: {
|
|
1281
|
+
type: string;
|
|
1282
|
+
description: string;
|
|
1283
|
+
};
|
|
1284
|
+
args: {
|
|
1285
|
+
type: string;
|
|
1286
|
+
description: string;
|
|
1287
|
+
additionalProperties: boolean;
|
|
1288
|
+
};
|
|
1289
|
+
paramsType: {
|
|
1290
|
+
type: string;
|
|
1291
|
+
enum: string[];
|
|
1292
|
+
description: string;
|
|
1293
|
+
};
|
|
1294
|
+
};
|
|
1295
|
+
required: string[];
|
|
1296
|
+
additionalProperties: boolean;
|
|
1297
|
+
};
|
|
1298
|
+
validation: {
|
|
1299
|
+
type: string;
|
|
1300
|
+
description: string;
|
|
1301
|
+
properties: {
|
|
1302
|
+
type: {
|
|
1303
|
+
type: string;
|
|
1304
|
+
description: string;
|
|
1305
|
+
};
|
|
1306
|
+
rules: {
|
|
1307
|
+
type: string;
|
|
1308
|
+
items: {
|
|
1309
|
+
$ref: string;
|
|
1310
|
+
};
|
|
1311
|
+
};
|
|
1312
|
+
};
|
|
1313
|
+
required: string[];
|
|
1314
|
+
additionalProperties: boolean;
|
|
1315
|
+
};
|
|
1316
|
+
validators: {
|
|
1317
|
+
type: string;
|
|
1318
|
+
description: string;
|
|
1319
|
+
items: {
|
|
1320
|
+
anyOf: ({
|
|
1321
|
+
type: string;
|
|
1322
|
+
$ref?: undefined;
|
|
1323
|
+
} | {
|
|
1324
|
+
$ref: string;
|
|
1325
|
+
type?: undefined;
|
|
1326
|
+
})[];
|
|
1327
|
+
};
|
|
1328
|
+
};
|
|
1272
1329
|
model: {
|
|
1273
1330
|
type: string;
|
|
1274
1331
|
properties: {
|
|
@@ -1364,6 +1421,9 @@ declare const _default: {
|
|
|
1364
1421
|
type: string;
|
|
1365
1422
|
nullable: boolean;
|
|
1366
1423
|
};
|
|
1424
|
+
validation: {
|
|
1425
|
+
$ref: string;
|
|
1426
|
+
};
|
|
1367
1427
|
defaultValue: {
|
|
1368
1428
|
nullable: boolean;
|
|
1369
1429
|
};
|
|
@@ -1483,6 +1543,9 @@ declare const _default: {
|
|
|
1483
1543
|
allowNull: {
|
|
1484
1544
|
type: string;
|
|
1485
1545
|
};
|
|
1546
|
+
validation: {
|
|
1547
|
+
$ref: string;
|
|
1548
|
+
};
|
|
1486
1549
|
defaultValue: {};
|
|
1487
1550
|
enum: {
|
|
1488
1551
|
type: string;
|
|
@@ -1548,6 +1611,12 @@ declare const _default: {
|
|
|
1548
1611
|
target: {
|
|
1549
1612
|
type: string;
|
|
1550
1613
|
};
|
|
1614
|
+
validation: {
|
|
1615
|
+
$ref: string;
|
|
1616
|
+
};
|
|
1617
|
+
validators: {
|
|
1618
|
+
$ref: string;
|
|
1619
|
+
};
|
|
1551
1620
|
defaultValue: {
|
|
1552
1621
|
description: string;
|
|
1553
1622
|
};
|
|
@@ -1616,6 +1685,12 @@ declare const _default: {
|
|
|
1616
1685
|
target: {
|
|
1617
1686
|
type: string;
|
|
1618
1687
|
};
|
|
1688
|
+
validation: {
|
|
1689
|
+
$ref: string;
|
|
1690
|
+
};
|
|
1691
|
+
validators: {
|
|
1692
|
+
$ref: string;
|
|
1693
|
+
};
|
|
1619
1694
|
settings: {
|
|
1620
1695
|
type: string;
|
|
1621
1696
|
additionalProperties: boolean;
|
|
@@ -1642,6 +1717,12 @@ declare const _default: {
|
|
|
1642
1717
|
target: {
|
|
1643
1718
|
type: string;
|
|
1644
1719
|
};
|
|
1720
|
+
validation: {
|
|
1721
|
+
$ref: string;
|
|
1722
|
+
};
|
|
1723
|
+
validators: {
|
|
1724
|
+
$ref: string;
|
|
1725
|
+
};
|
|
1645
1726
|
defaultValue: {
|
|
1646
1727
|
description: string;
|
|
1647
1728
|
};
|
package/dist/swagger/index.js
CHANGED
|
@@ -1188,6 +1188,64 @@ var swagger_default = {
|
|
|
1188
1188
|
},
|
|
1189
1189
|
additionalProperties: true
|
|
1190
1190
|
},
|
|
1191
|
+
validationRule: {
|
|
1192
|
+
type: "object",
|
|
1193
|
+
description: "One server-side Joi validation rule for repository create/update.",
|
|
1194
|
+
properties: {
|
|
1195
|
+
key: {
|
|
1196
|
+
type: "string",
|
|
1197
|
+
description: "Stable rule key. Generated by fields:apply when omitted."
|
|
1198
|
+
},
|
|
1199
|
+
name: {
|
|
1200
|
+
type: "string",
|
|
1201
|
+
description: "Joi rule name, for example `required`, `min`, `max`, `length`, `pattern`, `email`, or `precision`."
|
|
1202
|
+
},
|
|
1203
|
+
args: {
|
|
1204
|
+
type: "object",
|
|
1205
|
+
description: 'Rule arguments. Common examples: `{ "limit": 2 }`, `{ "regex": "^[A-Z]+$" }`, `{ "allowUnicode": true }`.',
|
|
1206
|
+
additionalProperties: true
|
|
1207
|
+
},
|
|
1208
|
+
paramsType: {
|
|
1209
|
+
type: "string",
|
|
1210
|
+
enum: ["object"],
|
|
1211
|
+
description: "Use `object` when the Joi rule expects a single options object, such as `email` or `uuid`."
|
|
1212
|
+
}
|
|
1213
|
+
},
|
|
1214
|
+
required: ["name"],
|
|
1215
|
+
additionalProperties: true
|
|
1216
|
+
},
|
|
1217
|
+
validation: {
|
|
1218
|
+
type: "object",
|
|
1219
|
+
description: [
|
|
1220
|
+
"Server-side field validation executed by repository create/update.",
|
|
1221
|
+
"",
|
|
1222
|
+
"Validation is based on Joi. The `type` selects the Joi schema type, and `rules` are applied in order."
|
|
1223
|
+
].join("\n"),
|
|
1224
|
+
properties: {
|
|
1225
|
+
type: {
|
|
1226
|
+
type: "string",
|
|
1227
|
+
description: "Joi schema type, commonly `string`, `number`, `date`, `boolean`, or `object`."
|
|
1228
|
+
},
|
|
1229
|
+
rules: {
|
|
1230
|
+
type: "array",
|
|
1231
|
+
items: { $ref: "#/components/schemas/field/validationRule" }
|
|
1232
|
+
}
|
|
1233
|
+
},
|
|
1234
|
+
required: ["type", "rules"],
|
|
1235
|
+
additionalProperties: true
|
|
1236
|
+
},
|
|
1237
|
+
validators: {
|
|
1238
|
+
type: "array",
|
|
1239
|
+
description: [
|
|
1240
|
+
"AI-friendly shorthand for server-side validation rules.",
|
|
1241
|
+
"",
|
|
1242
|
+
"Each item can be a Joi rule name string or a validation rule object.",
|
|
1243
|
+
"fields:apply converts this to `validation.rules`, fills missing rule keys, and infers `validation.type` from the field interface when omitted."
|
|
1244
|
+
].join("\n"),
|
|
1245
|
+
items: {
|
|
1246
|
+
anyOf: [{ type: "string" }, { $ref: "#/components/schemas/field/validationRule" }]
|
|
1247
|
+
}
|
|
1248
|
+
},
|
|
1191
1249
|
model: {
|
|
1192
1250
|
type: "object",
|
|
1193
1251
|
properties: {
|
|
@@ -1218,6 +1276,7 @@ var swagger_default = {
|
|
|
1218
1276
|
autoIncrement: { type: "boolean", nullable: true },
|
|
1219
1277
|
unique: { type: "boolean", nullable: true },
|
|
1220
1278
|
allowNull: { type: "boolean", nullable: true },
|
|
1279
|
+
validation: { $ref: "#/components/schemas/field/validation" },
|
|
1221
1280
|
defaultValue: {
|
|
1222
1281
|
nullable: true
|
|
1223
1282
|
},
|
|
@@ -1286,6 +1345,7 @@ var swagger_default = {
|
|
|
1286
1345
|
autoIncrement: { type: "boolean" },
|
|
1287
1346
|
unique: { type: "boolean" },
|
|
1288
1347
|
allowNull: { type: "boolean" },
|
|
1348
|
+
validation: { $ref: "#/components/schemas/field/validation" },
|
|
1289
1349
|
defaultValue: {},
|
|
1290
1350
|
enum: {
|
|
1291
1351
|
type: "array",
|
|
@@ -1333,6 +1393,8 @@ var swagger_default = {
|
|
|
1333
1393
|
interface: { type: "string" },
|
|
1334
1394
|
description: { type: "string" },
|
|
1335
1395
|
target: { type: "string" },
|
|
1396
|
+
validation: { $ref: "#/components/schemas/field/validation" },
|
|
1397
|
+
validators: { $ref: "#/components/schemas/field/validators" },
|
|
1336
1398
|
defaultValue: {
|
|
1337
1399
|
description: "Optional default value JSON. Structure depends on the selected interface."
|
|
1338
1400
|
},
|
|
@@ -1379,6 +1441,8 @@ var swagger_default = {
|
|
|
1379
1441
|
title: { type: "string" },
|
|
1380
1442
|
interface: { type: "string" },
|
|
1381
1443
|
target: { type: "string" },
|
|
1444
|
+
validation: { $ref: "#/components/schemas/field/validation" },
|
|
1445
|
+
validators: { $ref: "#/components/schemas/field/validators" },
|
|
1382
1446
|
settings: {
|
|
1383
1447
|
type: "object",
|
|
1384
1448
|
additionalProperties: true
|
|
@@ -1395,6 +1459,8 @@ var swagger_default = {
|
|
|
1395
1459
|
interface: { type: "string" },
|
|
1396
1460
|
description: { type: "string" },
|
|
1397
1461
|
target: { type: "string" },
|
|
1462
|
+
validation: { $ref: "#/components/schemas/field/validation" },
|
|
1463
|
+
validators: { $ref: "#/components/schemas/field/validators" },
|
|
1398
1464
|
defaultValue: {
|
|
1399
1465
|
description: "Optional default value JSON. Structure depends on the selected interface."
|
|
1400
1466
|
},
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"description": "NocoBase main database, supports relational databases such as PostgreSQL, MySQL, MariaDB and so on.",
|
|
7
7
|
"description.ru-RU": "Основная база данных NocoBase: поддерживает реляционные СУБД, включая PostgreSQL, MySQL, MariaDB и другие.",
|
|
8
8
|
"description.zh-CN": "NocoBase 主数据库,支持 PostgreSQL、MySQL、MariaDB 等关系型数据库。",
|
|
9
|
-
"version": "2.1.0-beta.
|
|
9
|
+
"version": "2.1.0-beta.33",
|
|
10
10
|
"main": "./dist/server/index.js",
|
|
11
11
|
"homepage": "https://docs.nocobase.com/handbook/data-source-main",
|
|
12
12
|
"homepage.ru-RU": "https://docs-ru.nocobase.com/handbook/data-source-main",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"@nocobase/test": "2.x",
|
|
26
26
|
"@nocobase/utils": "2.x"
|
|
27
27
|
},
|
|
28
|
-
"gitHead": "
|
|
28
|
+
"gitHead": "4815c394e80a264fa8ed619246280923c47aeb72",
|
|
29
29
|
"keywords": [
|
|
30
30
|
"Data sources"
|
|
31
31
|
]
|