adminforth 3.19.0 → 3.20.0

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.
@@ -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,EAGhB,kBAAkB,EAMlB,kBAAkB,EAClB,kBAAkB,EAElB,SAAS,EAKV,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;AA0XD,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;YAGrB,uBAAuB;IAerC,OAAO,CAAC,oBAAoB;IAkB5B;;OAEG;IACG,yBAAyB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE,EAAE,kBAAkB,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAYxH,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"}
1
+ {"version":3,"file":"restApi.d.ts","sourceRoot":"","sources":["../../modules/restApi.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,WAAW,EAGhB,kBAAkB,EAMlB,kBAAkB,EAClB,kBAAkB,EAElB,SAAS,EAKV,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;AA0XD,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;YAGrB,uBAAuB;IAerC,OAAO,CAAC,oBAAoB;IAkB5B;;OAEG;IACG,yBAAyB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE,EAAE,kBAAkB,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAYxH,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;CAo9DtC"}
@@ -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";
@@ -1170,7 +1171,20 @@ export default class AdminForthRestAPI {
1170
1171
  }
1171
1172
  const meta = { requestBody: body, pk: undefined };
1172
1173
  if ((source === 'edit' || source === 'show') && body.filters) {
1173
- meta.pk = (_a = body.filters.find((f) => f.field === resource.columns.find((col) => col.primaryKey).name)) === null || _a === void 0 ? void 0 : _a.value;
1174
+ if (isCompositePrimaryKey(resource)) {
1175
+ const pkFieldNames = primaryKeyColumnNames(resource);
1176
+ const pkValues = pkFieldNames.reduce((acc, name) => {
1177
+ var _a;
1178
+ acc[name] = (_a = body.filters.find((f) => f.field === name)) === null || _a === void 0 ? void 0 : _a.value;
1179
+ return acc;
1180
+ }, {});
1181
+ if (pkFieldNames.every((name) => pkValues[name] !== undefined)) {
1182
+ meta.pk = encodeRecordId(resource, pkValues);
1183
+ }
1184
+ }
1185
+ else {
1186
+ meta.pk = (_a = body.filters.find((f) => f.field === resource.columns.find((col) => col.primaryKey).name)) === null || _a === void 0 ? void 0 : _a.value;
1187
+ }
1174
1188
  }
1175
1189
  const { allowedActions } = await interpretResource(adminUser, resource, meta, {
1176
1190
  'show': ActionCheckSource.ShowRequest,
@@ -1237,6 +1251,12 @@ export default class AdminForthRestAPI {
1237
1251
  selectedDataSourceColumnNameSet.add(col.foreignResource.polymorphicOn);
1238
1252
  }
1239
1253
  }
1254
+ if (isCompositePrimaryKey(resource)) {
1255
+ // all key columns are needed to build record id out of the row
1256
+ for (const pkName of primaryKeyColumnNames(resource)) {
1257
+ selectedDataSourceColumnNameSet.add(pkName);
1258
+ }
1259
+ }
1240
1260
  }
1241
1261
  const selectedDataSourceColumns = selectedDataSourceColumnNameSet && !shouldAddListHelpers
1242
1262
  ? (resource.dataSourceColumns.some((col) => selectedDataSourceColumnNameSet.has(col.name))
@@ -1287,6 +1307,12 @@ export default class AdminForthRestAPI {
1287
1307
  getTotals: source === 'list',
1288
1308
  columns: selectedDataSourceColumns,
1289
1309
  });
1310
+ if (shouldAddListHelpers && isCompositePrimaryKey(resource)) {
1311
+ // single primary key rows keep exactly the shape they had before, frontend derives id itself
1312
+ for (const item of data.data) {
1313
+ item._primaryKeyValue = encodeRecordId(resource, item);
1314
+ }
1315
+ }
1290
1316
  // for foreign keys, add references
1291
1317
  await Promise.all(resource.columns.filter((col) => (col.foreignResource && (!selectedColumnNameSet || shouldAddListHelpers || selectedColumnNameSet.has(col.name)))).map(async (col) => {
1292
1318
  let targetDataMap = {};
@@ -1412,6 +1438,9 @@ export default class AdminForthRestAPI {
1412
1438
  };
1413
1439
  for (const item of data.data) {
1414
1440
  for (const key of Object.keys(item)) {
1441
+ if (key === '_primaryKeyValue') {
1442
+ continue;
1443
+ }
1415
1444
  const col = resource.columns.find((c) => c.name === key);
1416
1445
  const bo = col ? await isBackendOnly(col, ctx) : true;
1417
1446
  if (!col || bo) {
@@ -1451,15 +1480,20 @@ export default class AdminForthRestAPI {
1451
1480
  if (selectedColumnNameSet) {
1452
1481
  for (const item of data.data) {
1453
1482
  for (const key of Object.keys(item)) {
1454
- if (!selectedColumnNameSet.has(key) && key !== '_label' && key !== '_clickUrl') {
1483
+ if (!selectedColumnNameSet.has(key) && key !== '_label' && key !== '_clickUrl' && key !== '_primaryKeyValue') {
1455
1484
  delete item[key];
1456
1485
  }
1457
1486
  }
1458
1487
  }
1459
1488
  }
1460
1489
  if (source === 'list') {
1461
- const pkField = resource.columns.find((col) => col.primaryKey).name;
1462
- data.recordIds = data.data.map((item) => item[pkField]);
1490
+ if (isCompositePrimaryKey(resource)) {
1491
+ data.recordIds = data.data.map((item) => { var _a; return (_a = item._primaryKeyValue) !== null && _a !== void 0 ? _a : encodeRecordId(resource, item); });
1492
+ }
1493
+ else {
1494
+ const pkField = resource.columns.find((col) => col.primaryKey).name;
1495
+ data.recordIds = data.data.map((item) => item[pkField]);
1496
+ }
1463
1497
  }
1464
1498
  return data;
1465
1499
  },
@@ -1899,11 +1933,25 @@ export default class AdminForthRestAPI {
1899
1933
  }
1900
1934
  }
1901
1935
  }
1902
- const primaryKeyColumn = resource.columns.find((col) => col.primaryKey);
1903
- if (record[primaryKeyColumn.name] !== undefined) {
1904
- const existingRecord = await this.adminforth.resource(resource.resourceId).get([Filters.EQ(primaryKeyColumn.name, record[primaryKeyColumn.name])]);
1905
- if (existingRecord) {
1906
- return { error: `Record with ${primaryKeyColumn.name} '${record[primaryKeyColumn.name]}' already exists`, ok: false };
1936
+ if (isCompositePrimaryKey(resource)) {
1937
+ const createPkColumnNames = primaryKeyColumnNames(resource);
1938
+ if (createPkColumnNames.every((name) => record[name] !== undefined)) {
1939
+ const existingRecord = await this.adminforth.resource(resource.resourceId).get(createPkColumnNames.map((name) => Filters.EQ(name, record[name])));
1940
+ if (existingRecord) {
1941
+ return {
1942
+ error: `Record with ${createPkColumnNames.map((name) => `${name} '${record[name]}'`).join(', ')} already exists`,
1943
+ ok: false,
1944
+ };
1945
+ }
1946
+ }
1947
+ }
1948
+ else {
1949
+ const primaryKeyColumn = resource.columns.find((col) => col.primaryKey);
1950
+ if (record[primaryKeyColumn.name] !== undefined) {
1951
+ const existingRecord = await this.adminforth.resource(resource.resourceId).get([Filters.EQ(primaryKeyColumn.name, record[primaryKeyColumn.name])]);
1952
+ if (existingRecord) {
1953
+ return { error: `Record with ${primaryKeyColumn.name} '${record[primaryKeyColumn.name]}' already exists`, ok: false };
1954
+ }
1907
1955
  }
1908
1956
  }
1909
1957
  const ctxCreate = {
@@ -1999,9 +2047,12 @@ export default class AdminForthRestAPI {
1999
2047
  };
2000
2048
  }
2001
2049
  const connector = this.adminforth.connectors[resource.dataSource];
2050
+ const newRecordId = isCompositePrimaryKey(resource)
2051
+ ? encodeRecordId(resource, createRecordResponse.createdRecord)
2052
+ : createRecordResponse.createdRecord[connector.getPrimaryKey(resource)];
2002
2053
  return {
2003
- newRecordId: createRecordResponse.createdRecord[connector.getPrimaryKey(resource)],
2004
- redirectToRecordId: createRecordResponse.createdRecord[connector.getPrimaryKey(resource)],
2054
+ newRecordId,
2055
+ redirectToRecordId: newRecordId,
2005
2056
  ok: true
2006
2057
  };
2007
2058
  }
@@ -2026,7 +2077,7 @@ export default class AdminForthRestAPI {
2026
2077
  const oldRecord = await connector.getRecordByPrimaryKey(resource, recordId);
2027
2078
  if (!oldRecord) {
2028
2079
  const primaryKeyColumn = resource.columns.find((col) => col.primaryKey);
2029
- return { error: `Record with ${primaryKeyColumn.name} ${recordId} not found` };
2080
+ return { error: `Record with ${isCompositePrimaryKey(resource) ? primaryKeyColumnNames(resource).join(', ') : primaryKeyColumn.name} ${recordId} not found` };
2030
2081
  }
2031
2082
  const record = body['record'];
2032
2083
  const { allowedActions } = await interpretResource(adminUser, resource, { requestBody: body, newRecord: record, oldRecord, pk: recordId }, ActionCheckSource.EditRequest, this.adminforth);
@@ -2034,11 +2085,30 @@ export default class AdminForthRestAPI {
2034
2085
  if (!allowed) {
2035
2086
  return { error: allowedError };
2036
2087
  }
2037
- const primaryKeyColumn = resource.columns.find((col) => col.primaryKey);
2038
- if (record[primaryKeyColumn.name] !== undefined) {
2039
- const existingRecord = await this.adminforth.resource(resource.resourceId).get([Filters.EQ(primaryKeyColumn.name, record[primaryKeyColumn.name])]);
2040
- if (existingRecord) {
2041
- return { error: `Record with ${primaryKeyColumn.name} '${record[primaryKeyColumn.name]}' already exists`, ok: false };
2088
+ if (isCompositePrimaryKey(resource)) {
2089
+ const pkColumnNames = primaryKeyColumnNames(resource);
2090
+ const pkIsChanged = pkColumnNames.some((name) => record[name] !== undefined && String(record[name]) !== String(oldRecord[name]));
2091
+ if (pkIsChanged) {
2092
+ const newPkValues = pkColumnNames.reduce((acc, name) => {
2093
+ acc[name] = record[name] !== undefined ? record[name] : oldRecord[name];
2094
+ return acc;
2095
+ }, {});
2096
+ const existingRecord = await this.adminforth.resource(resource.resourceId).get(pkColumnNames.map((name) => Filters.EQ(name, newPkValues[name])));
2097
+ if (existingRecord) {
2098
+ return {
2099
+ error: `Record with ${pkColumnNames.map((name) => `${name} '${newPkValues[name]}'`).join(', ')} already exists`,
2100
+ ok: false,
2101
+ };
2102
+ }
2103
+ }
2104
+ }
2105
+ else {
2106
+ const primaryKeyColumn = resource.columns.find((col) => col.primaryKey);
2107
+ if (record[primaryKeyColumn.name] !== undefined) {
2108
+ const existingRecord = await this.adminforth.resource(resource.resourceId).get([Filters.EQ(primaryKeyColumn.name, record[primaryKeyColumn.name])]);
2109
+ if (existingRecord) {
2110
+ return { error: `Record with ${primaryKeyColumn.name} '${record[primaryKeyColumn.name]}' already exists`, ok: false };
2111
+ }
2042
2112
  }
2043
2113
  }
2044
2114
  const ctxEdit = {
@@ -2129,7 +2199,9 @@ export default class AdminForthRestAPI {
2129
2199
  return { error };
2130
2200
  }
2131
2201
  return {
2132
- recordId: record.id,
2202
+ recordId: isCompositePrimaryKey(resource)
2203
+ ? encodeRecordId(resource, Object.assign(Object.assign({}, oldRecord), record))
2204
+ : record.id,
2133
2205
  ok: true
2134
2206
  };
2135
2207
  }