adminforth 3.12.4 → 3.13.0-next.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 (37) hide show
  1. package/commands/createApp/templates/index.ts.hbs +0 -7
  2. package/dist/dataConnectors/baseConnector.d.ts +40 -2
  3. package/dist/dataConnectors/baseConnector.d.ts.map +1 -1
  4. package/dist/dataConnectors/baseConnector.js +115 -21
  5. package/dist/dataConnectors/baseConnector.js.map +1 -1
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +44 -3
  9. package/dist/index.js.map +1 -1
  10. package/dist/modules/configValidator.d.ts.map +1 -1
  11. package/dist/modules/configValidator.js +9 -12
  12. package/dist/modules/configValidator.js.map +1 -1
  13. package/dist/modules/operationalResource.d.ts.map +1 -1
  14. package/dist/modules/operationalResource.js +6 -1
  15. package/dist/modules/operationalResource.js.map +1 -1
  16. package/dist/modules/recordId.d.ts +42 -0
  17. package/dist/modules/recordId.d.ts.map +1 -0
  18. package/dist/modules/recordId.js +102 -0
  19. package/dist/modules/recordId.js.map +1 -0
  20. package/dist/modules/restApi.d.ts.map +1 -1
  21. package/dist/modules/restApi.js +90 -18
  22. package/dist/modules/restApi.js.map +1 -1
  23. package/dist/modules/socketBroker.d.ts.map +1 -1
  24. package/dist/modules/socketBroker.js +8 -17
  25. package/dist/modules/socketBroker.js.map +1 -1
  26. package/dist/modules/utils.d.ts.map +1 -1
  27. package/dist/modules/utils.js +7 -3
  28. package/dist/modules/utils.js.map +1 -1
  29. package/dist/spa/src/stores/core.ts +17 -10
  30. package/dist/spa/src/types/Back.ts +40 -3
  31. package/dist/spa/src/utils/listUtils.ts +5 -1
  32. package/dist/spa/src/utils/recordId.ts +99 -0
  33. package/dist/spa/src/views/ListView.vue +18 -7
  34. package/dist/types/Back.d.ts +38 -3
  35. package/dist/types/Back.d.ts.map +1 -1
  36. package/dist/types/Back.js.map +1 -1
  37. package/package.json +1 -2
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Separator used to glue values of composite primary key columns into one record id string.
3
+ * Values are URI-encoded before gluing, and '~' inside of value is escaped to '%7E',
4
+ * so separator can't appear in encoded parts.
5
+ */
6
+ export const COMPOSITE_RECORD_ID_SEPARATOR = '~';
7
+ /**
8
+ * Returns all columns marked with primaryKey: true, in order in which they are defined in resource.
9
+ * For resources with composite primary key returns more than one column.
10
+ */
11
+ export function primaryKeyColumns(resource) {
12
+ var _a;
13
+ const columns = (((_a = resource.dataSourceColumns) === null || _a === void 0 ? void 0 : _a.length) ? resource.dataSourceColumns : resource.columns) || [];
14
+ return columns.filter((col) => col.primaryKey);
15
+ }
16
+ export function primaryKeyColumnNames(resource) {
17
+ return primaryKeyColumns(resource).map((col) => col.name);
18
+ }
19
+ export function isCompositePrimaryKey(resource) {
20
+ return primaryKeyColumns(resource).length > 1;
21
+ }
22
+ function escapeRecordIdPart(value) {
23
+ // encodeURIComponent does not escape '~' (it is unreserved), so escape it manually
24
+ return encodeURIComponent(String(value)).replace(/~/g, '%7E');
25
+ }
26
+ function unescapeRecordIdPart(part) {
27
+ return decodeURIComponent(part);
28
+ }
29
+ /**
30
+ * Foreign resource columns are returned to frontend as { pk, label } objects,
31
+ * so unwrap them when record id is built from such record
32
+ */
33
+ function unwrapPrimaryKeyValue(value) {
34
+ if (value !== null && typeof value === 'object' && 'pk' in value) {
35
+ return value.pk;
36
+ }
37
+ return value;
38
+ }
39
+ /**
40
+ * Builds record id (value used in urls and in all AdminForth APIs) from record object.
41
+ *
42
+ * For resource with single primary key column returns raw value of that column (no encoding is done,
43
+ * so behavior is fully backward compatible).
44
+ *
45
+ * For resource with composite primary key returns string like 'BTC~ERC20' where each part is
46
+ * URI-encoded value of corresponding primary key column, in order of definition in resource.
47
+ */
48
+ export function encodeRecordId(resource, record) {
49
+ const pkColumns = primaryKeyColumns(resource);
50
+ if (!pkColumns.length) {
51
+ throw new Error(`Resource '${resource.resourceId}' has no primaryKey column`);
52
+ }
53
+ if (pkColumns.length === 1) {
54
+ return unwrapPrimaryKeyValue(record === null || record === void 0 ? void 0 : record[pkColumns[0].name]);
55
+ }
56
+ return pkColumns.map((col) => {
57
+ const value = unwrapPrimaryKeyValue(record === null || record === void 0 ? void 0 : record[col.name]);
58
+ if (value === undefined || value === null) {
59
+ throw new Error(`Can't build record id for resource '${resource.resourceId}': primary key column '${col.name}' has no value in record`);
60
+ }
61
+ return escapeRecordIdPart(value);
62
+ }).join(COMPOSITE_RECORD_ID_SEPARATOR);
63
+ }
64
+ /**
65
+ * Splits record id back into values of primary key columns.
66
+ * Returns object like { assetSymbol: 'BTC', networkCode: 'ERC20' }.
67
+ *
68
+ * Values are returned as strings for composite keys (they come from url/API),
69
+ * connectors cast them to column types using setFieldValue.
70
+ */
71
+ export function decodeRecordId(resource, recordId) {
72
+ const pkColumns = primaryKeyColumns(resource);
73
+ if (!pkColumns.length) {
74
+ throw new Error(`Resource '${resource.resourceId}' has no primaryKey column`);
75
+ }
76
+ if (pkColumns.length === 1) {
77
+ return { [pkColumns[0].name]: recordId };
78
+ }
79
+ const parts = String(recordId).split(COMPOSITE_RECORD_ID_SEPARATOR);
80
+ if (parts.length !== pkColumns.length) {
81
+ throw new Error(`Record id '${recordId}' is not valid for resource '${resource.resourceId}': expected ${pkColumns.length} ` +
82
+ `parts (${pkColumns.map((col) => col.name).join(', ')}) separated by '${COMPOSITE_RECORD_ID_SEPARATOR}', got ${parts.length}`);
83
+ }
84
+ return pkColumns.reduce((acc, col, i) => {
85
+ acc[col.name] = unescapeRecordIdPart(parts[i]);
86
+ return acc;
87
+ }, {});
88
+ }
89
+ /**
90
+ * Returns pkValues argument for connector's updateRecordOriginalValues/deleteRecord.
91
+ *
92
+ * Returns undefined for resources with single primary key, so connectors receive exactly the same
93
+ * arguments as before composite primary key support was added (pure extension, old path untouched).
94
+ */
95
+ export function compositePkValues(connector, resource, recordId) {
96
+ var _a;
97
+ if (!isCompositePrimaryKey(resource)) {
98
+ return undefined;
99
+ }
100
+ return (_a = connector.getPrimaryKeyValues) === null || _a === void 0 ? void 0 : _a.call(connector, resource, recordId);
101
+ }
102
+ //# sourceMappingURL=recordId.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recordId.js","sourceRoot":"","sources":["../../modules/recordId.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"restApi.d.ts","sourceRoot":"","sources":["../../modules/restApi.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,WAAW,EAEhB,kBAAkB,EAKlB,kBAAkB,EAElB,SAAS,EAIV,MAAM,kBAAkB,CAAC;AAO1B,OAAO,EAA+F,WAAW,EAAE,MAAM,YAAY,CAAC;AAGtI,OAAO,EAAE,iBAAiB,EAEvB,SAAS,EAAsB,sBAAsB,EAGtC,MAAM,oBAAoB,CAAC;AAuO7C,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,GAAG,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAI/E;AA+VD,wBAAsB,iBAAiB,CACrC,SAAS,EAAE,SAAS,EACpB,QAAQ,EAAE,kBAAkB,EAC5B,IAAI,EAAE,GAAG,EACT,MAAM,EAAE,iBAAiB,EACzB,UAAU,EAAE,WAAW,GACtB,OAAO,CAAC;IAAC,cAAc,EAAE,sBAAsB,CAAA;CAAC,CAAC,CAyCnD;AAED,MAAM,CAAC,OAAO,OAAO,iBAAkB,YAAW,kBAAkB;IAElE,UAAU,EAAE,WAAW,CAAC;IACxB,iBAAiB,EAAE,WAAW,EAAE,CAAM;gBAE1B,UAAU,EAAE,WAAW;IAInC,OAAO,CAAC,oBAAoB;IAkBtB,qBAAqB,CAAC,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,EAAC,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,KAAK,EAAE,SAAS,EAAE,eAAe,CAAC,EAAE,MAAM;IAsBpL,gBAAgB,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO;IAOnD,iBAAiB,CAAC,MAAM,EAAE,WAAW;CAg0DtC"}
1
+ {"version":3,"file":"restApi.d.ts","sourceRoot":"","sources":["../../modules/restApi.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,WAAW,EAEhB,kBAAkB,EAKlB,kBAAkB,EAElB,SAAS,EAIV,MAAM,kBAAkB,CAAC;AAQ1B,OAAO,EAA+F,WAAW,EAAE,MAAM,YAAY,CAAC;AAGtI,OAAO,EAAE,iBAAiB,EAEvB,SAAS,EAAsB,sBAAsB,EAGtC,MAAM,oBAAoB,CAAC;AAuO7C,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,GAAG,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAI/E;AA+VD,wBAAsB,iBAAiB,CACrC,SAAS,EAAE,SAAS,EACpB,QAAQ,EAAE,kBAAkB,EAC5B,IAAI,EAAE,GAAG,EACT,MAAM,EAAE,iBAAiB,EACzB,UAAU,EAAE,WAAW,GACtB,OAAO,CAAC;IAAC,cAAc,EAAE,sBAAsB,CAAA;CAAC,CAAC,CAyCnD;AAED,MAAM,CAAC,OAAO,OAAO,iBAAkB,YAAW,kBAAkB;IAElE,UAAU,EAAE,WAAW,CAAC;IACxB,iBAAiB,EAAE,WAAW,EAAE,CAAM;gBAE1B,UAAU,EAAE,WAAW;IAInC,OAAO,CAAC,oBAAoB;IAkBtB,qBAAqB,CAAC,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,EAAC,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,KAAK,EAAE,SAAS,EAAE,eAAe,CAAC,EAAE,MAAM;IAsBpL,gBAAgB,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO;IAOnD,iBAAiB,CAAC,MAAM,EAAE,WAAW;CA04DtC"}
@@ -11,6 +11,7 @@ var __rest = (this && this.__rest) || function (s, e) {
11
11
  };
12
12
  import { Filters, } from "../types/Back.js";
13
13
  import { cascadeChildrenDelete } from './utils.js';
14
+ import { encodeRecordId, isCompositePrimaryKey, primaryKeyColumnNames } from './recordId.js';
14
15
  import { afLogger } from "./logger.js";
15
16
  import { ADMINFORTH_VERSION, listify, getLoginPromptHTML, hookResponseError, parseLooseJson, RateLimiter } from './utils.js';
16
17
  import AdminForthAuth from "../auth.js";
@@ -1119,7 +1120,20 @@ export default class AdminForthRestAPI {
1119
1120
  }
1120
1121
  const meta = { requestBody: body, pk: undefined };
1121
1122
  if ((source === 'edit' || source === 'show') && body.filters) {
1122
- meta.pk = (_a = body.filters.find((f) => f.field === resource.columns.find((col) => col.primaryKey).name)) === null || _a === void 0 ? void 0 : _a.value;
1123
+ if (isCompositePrimaryKey(resource)) {
1124
+ const pkFieldNames = primaryKeyColumnNames(resource);
1125
+ const pkValues = pkFieldNames.reduce((acc, name) => {
1126
+ var _a;
1127
+ acc[name] = (_a = body.filters.find((f) => f.field === name)) === null || _a === void 0 ? void 0 : _a.value;
1128
+ return acc;
1129
+ }, {});
1130
+ if (pkFieldNames.every((name) => pkValues[name] !== undefined)) {
1131
+ meta.pk = encodeRecordId(resource, pkValues);
1132
+ }
1133
+ }
1134
+ else {
1135
+ meta.pk = (_a = body.filters.find((f) => f.field === resource.columns.find((col) => col.primaryKey).name)) === null || _a === void 0 ? void 0 : _a.value;
1136
+ }
1123
1137
  }
1124
1138
  const { allowedActions } = await interpretResource(adminUser, resource, meta, {
1125
1139
  'show': ActionCheckSource.ShowRequest,
@@ -1186,6 +1200,12 @@ export default class AdminForthRestAPI {
1186
1200
  selectedDataSourceColumnNameSet.add(col.foreignResource.polymorphicOn);
1187
1201
  }
1188
1202
  }
1203
+ if (isCompositePrimaryKey(resource)) {
1204
+ // all key columns are needed to build record id out of the row
1205
+ for (const pkName of primaryKeyColumnNames(resource)) {
1206
+ selectedDataSourceColumnNameSet.add(pkName);
1207
+ }
1208
+ }
1189
1209
  }
1190
1210
  const selectedDataSourceColumns = selectedDataSourceColumnNameSet && !shouldAddListHelpers
1191
1211
  ? (resource.dataSourceColumns.some((col) => selectedDataSourceColumnNameSet.has(col.name))
@@ -1236,6 +1256,12 @@ export default class AdminForthRestAPI {
1236
1256
  getTotals: source === 'list',
1237
1257
  columns: selectedDataSourceColumns,
1238
1258
  });
1259
+ if (shouldAddListHelpers && isCompositePrimaryKey(resource)) {
1260
+ // single primary key rows keep exactly the shape they had before, frontend derives id itself
1261
+ for (const item of data.data) {
1262
+ item._primaryKeyValue = encodeRecordId(resource, item);
1263
+ }
1264
+ }
1239
1265
  // for foreign keys, add references
1240
1266
  await Promise.all(resource.columns.filter((col) => (col.foreignResource && (!selectedColumnNameSet || shouldAddListHelpers || selectedColumnNameSet.has(col.name)))).map(async (col) => {
1241
1267
  let targetDataMap = {};
@@ -1358,6 +1384,9 @@ export default class AdminForthRestAPI {
1358
1384
  };
1359
1385
  for (const item of data.data) {
1360
1386
  for (const key of Object.keys(item)) {
1387
+ if (key === '_primaryKeyValue') {
1388
+ continue;
1389
+ }
1361
1390
  const col = resource.columns.find((c) => c.name === key);
1362
1391
  const bo = col ? await isBackendOnly(col, ctx) : true;
1363
1392
  if (!col || bo) {
@@ -1397,15 +1426,20 @@ export default class AdminForthRestAPI {
1397
1426
  if (selectedColumnNameSet) {
1398
1427
  for (const item of data.data) {
1399
1428
  for (const key of Object.keys(item)) {
1400
- if (!selectedColumnNameSet.has(key) && key !== '_label' && key !== '_clickUrl') {
1429
+ if (!selectedColumnNameSet.has(key) && key !== '_label' && key !== '_clickUrl' && key !== '_primaryKeyValue') {
1401
1430
  delete item[key];
1402
1431
  }
1403
1432
  }
1404
1433
  }
1405
1434
  }
1406
1435
  if (source === 'list') {
1407
- const pkField = resource.columns.find((col) => col.primaryKey).name;
1408
- data.recordIds = data.data.map((item) => item[pkField]);
1436
+ if (isCompositePrimaryKey(resource)) {
1437
+ data.recordIds = data.data.map((item) => { var _a; return (_a = item._primaryKeyValue) !== null && _a !== void 0 ? _a : encodeRecordId(resource, item); });
1438
+ }
1439
+ else {
1440
+ const pkField = resource.columns.find((col) => col.primaryKey).name;
1441
+ data.recordIds = data.data.map((item) => item[pkField]);
1442
+ }
1409
1443
  }
1410
1444
  return data;
1411
1445
  },
@@ -1787,11 +1821,25 @@ export default class AdminForthRestAPI {
1787
1821
  }
1788
1822
  }
1789
1823
  }
1790
- const primaryKeyColumn = resource.columns.find((col) => col.primaryKey);
1791
- if (record[primaryKeyColumn.name] !== undefined) {
1792
- const existingRecord = await this.adminforth.resource(resource.resourceId).get([Filters.EQ(primaryKeyColumn.name, record[primaryKeyColumn.name])]);
1793
- if (existingRecord) {
1794
- return { error: `Record with ${primaryKeyColumn.name} '${record[primaryKeyColumn.name]}' already exists`, ok: false };
1824
+ if (isCompositePrimaryKey(resource)) {
1825
+ const createPkColumnNames = primaryKeyColumnNames(resource);
1826
+ if (createPkColumnNames.every((name) => record[name] !== undefined)) {
1827
+ const existingRecord = await this.adminforth.resource(resource.resourceId).get(createPkColumnNames.map((name) => Filters.EQ(name, record[name])));
1828
+ if (existingRecord) {
1829
+ return {
1830
+ error: `Record with ${createPkColumnNames.map((name) => `${name} '${record[name]}'`).join(', ')} already exists`,
1831
+ ok: false,
1832
+ };
1833
+ }
1834
+ }
1835
+ }
1836
+ else {
1837
+ const primaryKeyColumn = resource.columns.find((col) => col.primaryKey);
1838
+ if (record[primaryKeyColumn.name] !== undefined) {
1839
+ const existingRecord = await this.adminforth.resource(resource.resourceId).get([Filters.EQ(primaryKeyColumn.name, record[primaryKeyColumn.name])]);
1840
+ if (existingRecord) {
1841
+ return { error: `Record with ${primaryKeyColumn.name} '${record[primaryKeyColumn.name]}' already exists`, ok: false };
1842
+ }
1795
1843
  }
1796
1844
  }
1797
1845
  const ctxCreate = {
@@ -1887,9 +1935,12 @@ export default class AdminForthRestAPI {
1887
1935
  };
1888
1936
  }
1889
1937
  const connector = this.adminforth.connectors[resource.dataSource];
1938
+ const newRecordId = isCompositePrimaryKey(resource)
1939
+ ? encodeRecordId(resource, createRecordResponse.createdRecord)
1940
+ : createRecordResponse.createdRecord[connector.getPrimaryKey(resource)];
1890
1941
  return {
1891
- newRecordId: createRecordResponse.createdRecord[connector.getPrimaryKey(resource)],
1892
- redirectToRecordId: createRecordResponse.createdRecord[connector.getPrimaryKey(resource)],
1942
+ newRecordId,
1943
+ redirectToRecordId: newRecordId,
1893
1944
  ok: true
1894
1945
  };
1895
1946
  }
@@ -1914,7 +1965,7 @@ export default class AdminForthRestAPI {
1914
1965
  const oldRecord = await connector.getRecordByPrimaryKey(resource, recordId);
1915
1966
  if (!oldRecord) {
1916
1967
  const primaryKeyColumn = resource.columns.find((col) => col.primaryKey);
1917
- return { error: `Record with ${primaryKeyColumn.name} ${recordId} not found` };
1968
+ return { error: `Record with ${isCompositePrimaryKey(resource) ? primaryKeyColumnNames(resource).join(', ') : primaryKeyColumn.name} ${recordId} not found` };
1918
1969
  }
1919
1970
  const record = body['record'];
1920
1971
  const { allowedActions } = await interpretResource(adminUser, resource, { requestBody: body, newRecord: record, oldRecord, pk: recordId }, ActionCheckSource.EditRequest, this.adminforth);
@@ -1922,11 +1973,30 @@ export default class AdminForthRestAPI {
1922
1973
  if (!allowed) {
1923
1974
  return { error: allowedError };
1924
1975
  }
1925
- const primaryKeyColumn = resource.columns.find((col) => col.primaryKey);
1926
- if (record[primaryKeyColumn.name] !== undefined) {
1927
- const existingRecord = await this.adminforth.resource(resource.resourceId).get([Filters.EQ(primaryKeyColumn.name, record[primaryKeyColumn.name])]);
1928
- if (existingRecord) {
1929
- return { error: `Record with ${primaryKeyColumn.name} '${record[primaryKeyColumn.name]}' already exists`, ok: false };
1976
+ if (isCompositePrimaryKey(resource)) {
1977
+ const pkColumnNames = primaryKeyColumnNames(resource);
1978
+ const pkIsChanged = pkColumnNames.some((name) => record[name] !== undefined && String(record[name]) !== String(oldRecord[name]));
1979
+ if (pkIsChanged) {
1980
+ const newPkValues = pkColumnNames.reduce((acc, name) => {
1981
+ acc[name] = record[name] !== undefined ? record[name] : oldRecord[name];
1982
+ return acc;
1983
+ }, {});
1984
+ const existingRecord = await this.adminforth.resource(resource.resourceId).get(pkColumnNames.map((name) => Filters.EQ(name, newPkValues[name])));
1985
+ if (existingRecord) {
1986
+ return {
1987
+ error: `Record with ${pkColumnNames.map((name) => `${name} '${newPkValues[name]}'`).join(', ')} already exists`,
1988
+ ok: false,
1989
+ };
1990
+ }
1991
+ }
1992
+ }
1993
+ else {
1994
+ const primaryKeyColumn = resource.columns.find((col) => col.primaryKey);
1995
+ if (record[primaryKeyColumn.name] !== undefined) {
1996
+ const existingRecord = await this.adminforth.resource(resource.resourceId).get([Filters.EQ(primaryKeyColumn.name, record[primaryKeyColumn.name])]);
1997
+ if (existingRecord) {
1998
+ return { error: `Record with ${primaryKeyColumn.name} '${record[primaryKeyColumn.name]}' already exists`, ok: false };
1999
+ }
1930
2000
  }
1931
2001
  }
1932
2002
  const ctxEdit = {
@@ -2017,7 +2087,9 @@ export default class AdminForthRestAPI {
2017
2087
  return { error };
2018
2088
  }
2019
2089
  return {
2020
- recordId: record.id,
2090
+ recordId: isCompositePrimaryKey(resource)
2091
+ ? encodeRecordId(resource, Object.assign(Object.assign({}, oldRecord), record))
2092
+ : record.id,
2021
2093
  ok: true
2022
2094
  };
2023
2095
  }