sg-widgets-core 0.3.0 → 0.4.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.
@@ -236,6 +236,36 @@ function isTemporalType(dataType) {
236
236
  return dataType === "date" || dataType === "date_time";
237
237
  }
238
238
  //#endregion
239
+ //#region src/field-name.ts
240
+ /** The prefix the server puts in front of every custom field it creates. */
241
+ const CUSTOM_FIELD_PREFIX = "sg_";
242
+ /**
243
+ * Whether a display name holds a character outside ASCII. The server discards such a name
244
+ * whole when it derives the programmatic name (findings/117_field_types_and_names).
245
+ */
246
+ function hasNonAscii(displayName) {
247
+ return /[^\u0000-\u007F]/u.test(displayName);
248
+ }
249
+ /**
250
+ * The programmatic name the server derives from a display name, or null when it cannot be
251
+ * predicted: a blank display name, or a non-ASCII one without its data type.
252
+ *
253
+ * An ASCII display name gives the prefix, then the name lowercased with each character
254
+ * outside `[a-z0-9_]` replaced by one underscore. Nothing is trimmed, collapsed or cut, a
255
+ * leading digit stays, and a prefix already there is doubled (findings/019_create_fields,
256
+ * findings/117_field_types_and_names).
257
+ *
258
+ * One non-ASCII character anywhere and the server ignores the display name: the field is
259
+ * `sg_<data_type>`. That is measured on a `currency` field and inferred for the other types.
260
+ * A name already taken comes back suffixed `_1`, `_2`, and a name a retired field holds is
261
+ * refused; neither is predicted here.
262
+ */
263
+ function predictFieldName(displayName, dataType) {
264
+ if (displayName.trim() === "") return null;
265
+ if (hasNonAscii(displayName)) return dataType ? "sg_" + dataType : null;
266
+ return "sg_" + displayName.toLowerCase().replace(/[^a-z0-9_]/g, "_");
267
+ }
268
+ //#endregion
239
269
  //#region src/filter.ts
240
270
  /**
241
271
  * The filter array `entity_types` wants, `[]` for no filter
@@ -450,7 +480,7 @@ function normalizeField(name, raw) {
450
480
  if (hiddenValues) field.hiddenValues = hiddenValues;
451
481
  const defaultValue = prop(props, "default_value");
452
482
  if (defaultValue !== void 0) field.defaultValue = defaultValue;
453
- const description = prop(props, "description");
483
+ const description = raw.description?.value;
454
484
  if (description) field.description = description;
455
485
  if (raw.visible) field.hideable = raw.visible.editable;
456
486
  return {
@@ -1053,4 +1083,356 @@ function toStatusIcon(a) {
1053
1083
  }
1054
1084
  }
1055
1085
  //#endregion
1056
- export { NEGATING_OPERATORS as A, isTemporalType as B, isBlankCondition as C, toFilterArray as D, toApi3Hash as E, isDataType as F, supportsOperator as H, isFilterable as I, isLinkType as L, OPERATORS_BY_TYPE as M, TIME_UNITS as N, walk as O, VALUE_SHAPE as P, isNumericType as R, group as S, referencedPaths as T, operatorsFor as V, undeclaredField as _, eventLogFilters as a, emptyFilter as b, normalizeHierarchyNode as c, displayNameOf as d, fieldSchemaOverride as f, statusFieldNameFor as g, statusFieldFor as h, batchResults as i, OPERATORS as j, DATA_TYPES as k, DISPLAY_NAME_FIELDS as l, normalizeFields as m, RestClient as n, hierarchyEntity as o, normalizeField as p, SgApiError as r, normalizeEventLogEntry as s, EVENT_LOG_FIELDS as t, FIELD_SCHEMA_OVERRIDES as u, pluralPath as v, isEmptyFilter as w, fromWire as x, condition as y, isOperator as z };
1086
+ //#region src/schema-write.ts
1087
+ function toSchemaProperties(properties) {
1088
+ if (!properties) return [];
1089
+ if (Array.isArray(properties)) return properties;
1090
+ return Object.entries(properties).map(([property_name, value]) => ({
1091
+ property_name,
1092
+ value
1093
+ }));
1094
+ }
1095
+ function firstError(body) {
1096
+ if (body === null || typeof body !== "object") return {};
1097
+ const errors = body.errors;
1098
+ const first = Array.isArray(errors) ? errors[0] : null;
1099
+ return first !== null && typeof first === "object" ? first : {};
1100
+ }
1101
+ /** The strings a `source` holds, e.g. `{"revive": ["revive is missing"]}`. */
1102
+ function sourceMessages(source) {
1103
+ if (source === null || typeof source !== "object") return [];
1104
+ return Object.values(source).flat().filter((value) => typeof value === "string");
1105
+ }
1106
+ /**
1107
+ * A refusal from a schema call, as the server sent it: `status` and `body` whole,
1108
+ * and the first error's `code`, `title`, `detail` and `source` lifted out.
1109
+ *
1110
+ * `message` is the `detail` where there is one and the `title` otherwise, followed
1111
+ * by whatever only the `source` says. A 404 is titled `Not Found` and says which
1112
+ * name is wrong in its `detail` (002_schema).
1113
+ */
1114
+ var SchemaApiError = class extends SgApiError {
1115
+ code;
1116
+ title;
1117
+ detail;
1118
+ source;
1119
+ constructor(status, body, message) {
1120
+ const first = firstError(body);
1121
+ const title = typeof first.title === "string" ? first.title : null;
1122
+ const detail = typeof first.detail === "string" ? first.detail : null;
1123
+ const said = detail ?? title;
1124
+ const composed = [said, ...sourceMessages(first.source).filter((text) => !said?.includes(text))].filter((text) => text).join(" ");
1125
+ super(status, body, message ?? (composed || `Flow PT API error ${status}`));
1126
+ this.name = "SchemaApiError";
1127
+ this.code = typeof first.code === "number" ? first.code : null;
1128
+ this.title = title;
1129
+ this.detail = detail;
1130
+ this.source = first.source ?? null;
1131
+ }
1132
+ };
1133
+ /** The one 404 that means the field is absent; an unknown type's 404 says `Entity type` (get_schema_type_fields_field). */
1134
+ const FIELD_MISSING = /^Field '.*' does not exist/;
1135
+ function isMissingField(error) {
1136
+ return error.status === 404 && FIELD_MISSING.test(error.detail ?? error.title ?? error.message);
1137
+ }
1138
+ function isRecord(value) {
1139
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1140
+ }
1141
+ /**
1142
+ * A wrapped record as a `FieldSchema`. The record is left as it came; a key the
1143
+ * response leaves out is read as its default, with the type taken from the path.
1144
+ */
1145
+ function fieldFromRaw(entityType, name, raw) {
1146
+ const fixed = (value) => ({
1147
+ value,
1148
+ editable: false
1149
+ });
1150
+ const loose = raw;
1151
+ return normalizeField(name, {
1152
+ ...raw,
1153
+ name: loose.name ?? fixed(name),
1154
+ entity_type: loose.entity_type ?? fixed(entityType),
1155
+ data_type: loose.data_type ?? fixed(""),
1156
+ editable: loose.editable ?? fixed(false),
1157
+ mandatory: loose.mandatory ?? fixed(false),
1158
+ unique: loose.unique ?? fixed(false),
1159
+ properties: loose.properties ?? {}
1160
+ });
1161
+ }
1162
+ function fieldPath(entityType, field) {
1163
+ const base = `/schema/${encodeURIComponent(entityType)}/fields`;
1164
+ return field === void 0 ? base : `${base}/${encodeURIComponent(field)}`;
1165
+ }
1166
+ var RestSchemaClient = class {
1167
+ options;
1168
+ base;
1169
+ fetchFn;
1170
+ constructor(options) {
1171
+ this.options = options;
1172
+ this.base = options.siteUrl.replace(/\/+$/, "") + "/api/v1";
1173
+ this.fetchFn = options.fetch ?? globalThis.fetch.bind(globalThis);
1174
+ }
1175
+ async request(method, path, body, projectId) {
1176
+ const url = new URL(this.base + path);
1177
+ if (projectId !== void 0) url.searchParams.set("project_id", String(projectId));
1178
+ const headers = {
1179
+ Accept: "application/json",
1180
+ Authorization: `Bearer ${await this.options.token()}`
1181
+ };
1182
+ const init = {
1183
+ method,
1184
+ headers
1185
+ };
1186
+ if (body !== void 0) {
1187
+ headers["Content-Type"] = "application/json";
1188
+ init.body = JSON.stringify(body);
1189
+ }
1190
+ const res = await this.fetchFn(url, init);
1191
+ const text = await res.text();
1192
+ let parsed = null;
1193
+ try {
1194
+ parsed = text ? JSON.parse(text) : null;
1195
+ } catch {
1196
+ parsed = text;
1197
+ }
1198
+ if (!res.ok) throw new SchemaApiError(res.status, parsed);
1199
+ return {
1200
+ status: res.status,
1201
+ body: parsed
1202
+ };
1203
+ }
1204
+ async readField(entityType, field, projectId) {
1205
+ let reply;
1206
+ try {
1207
+ reply = await this.request("GET", fieldPath(entityType, field), void 0, projectId);
1208
+ } catch (error) {
1209
+ if (error instanceof SchemaApiError && isMissingField(error)) return {
1210
+ status: "missing",
1211
+ message: error.message
1212
+ };
1213
+ throw error;
1214
+ }
1215
+ const raw = isRecord(reply.body) ? reply.body["data"] : null;
1216
+ if (!isRecord(raw)) return { status: "undeclared" };
1217
+ const record = raw;
1218
+ return {
1219
+ status: "found",
1220
+ field: fieldFromRaw(entityType, field, record),
1221
+ raw: record
1222
+ };
1223
+ }
1224
+ async readRawFields(entityType, projectId) {
1225
+ const reply = await this.request("GET", fieldPath(entityType), void 0, projectId);
1226
+ const data = isRecord(reply.body) ? reply.body["data"] : null;
1227
+ return isRecord(data) ? data : {};
1228
+ }
1229
+ async createField(entityType, input) {
1230
+ const body = {
1231
+ data_type: input.dataType,
1232
+ properties: [{
1233
+ property_name: "name",
1234
+ value: input.displayName
1235
+ }, ...toSchemaProperties(input.properties)]
1236
+ };
1237
+ const reply = await this.request("POST", fieldPath(entityType), body);
1238
+ const envelope = isRecord(reply.body) ? reply.body : {};
1239
+ const links = envelope["links"];
1240
+ const self = isRecord(links) && typeof links["self"] === "string" ? links["self"] : "";
1241
+ const name = decodeURIComponent(self.split("?")[0]?.split("/").filter(Boolean).pop() ?? "");
1242
+ if (!name) throw new SchemaApiError(reply.status, reply.body, "The create answered no links.self, so the name of the field is unknown");
1243
+ const data = envelope["data"];
1244
+ return {
1245
+ name,
1246
+ self,
1247
+ raw: isRecord(data) ? data : null
1248
+ };
1249
+ }
1250
+ async updateField(entityType, field, properties, projectId) {
1251
+ const body = { properties: toSchemaProperties(properties) };
1252
+ if (projectId !== void 0) body["project_id"] = projectId;
1253
+ const reply = await this.request("PUT", fieldPath(entityType, field), body);
1254
+ const data = isRecord(reply.body) ? reply.body["data"] : null;
1255
+ return isRecord(data) ? data : null;
1256
+ }
1257
+ async retireField(entityType, field) {
1258
+ await this.request("DELETE", fieldPath(entityType, field));
1259
+ }
1260
+ async reviveField(entityType, field) {
1261
+ try {
1262
+ await this.request("POST", fieldPath(entityType, field), { revive: true });
1263
+ return { revived: true };
1264
+ } catch (error) {
1265
+ if (error instanceof SchemaApiError && isDeclined(error.status)) return {
1266
+ revived: false,
1267
+ status: error.status,
1268
+ message: error.message
1269
+ };
1270
+ throw error;
1271
+ }
1272
+ }
1273
+ };
1274
+ /** A refusal of the revive itself, as against a refused sign-in, a missing right or a fault. */
1275
+ function isDeclined(status) {
1276
+ return status >= 400 && status < 500 && status !== 401 && status !== 403;
1277
+ }
1278
+ /** The schema methods the proxy protocol carries, one POST each, beside `PROXY_METHODS`. */
1279
+ const SCHEMA_PROXY_METHODS = [
1280
+ "readField",
1281
+ "readRawFields",
1282
+ "createField",
1283
+ "updateField",
1284
+ "retireField",
1285
+ "reviveField"
1286
+ ];
1287
+ function isSchemaProxyMethod(value) {
1288
+ return SCHEMA_PROXY_METHODS.includes(value);
1289
+ }
1290
+ var BadRequest = class extends Error {};
1291
+ function str(value, name) {
1292
+ if (typeof value !== "string" || value.length === 0) throw new BadRequest(`'${name}' must be a non-empty string`);
1293
+ return value;
1294
+ }
1295
+ function optionalNum(value, name) {
1296
+ if (value === void 0 || value === null) return void 0;
1297
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new BadRequest(`'${name}' must be a number`);
1298
+ return value;
1299
+ }
1300
+ /** A list of `{property_name, value}` or a record; the API names what is wrong inside one. */
1301
+ function properties(value, name) {
1302
+ if (Array.isArray(value)) {
1303
+ for (const [i, item] of value.entries()) if (!isRecord(item) || typeof item["property_name"] !== "string") throw new BadRequest(`'${name}[${i}]' must be a {property_name, value} object`);
1304
+ return value;
1305
+ }
1306
+ if (!isRecord(value)) throw new BadRequest(`'${name}' must be a list or a JSON object`);
1307
+ return value;
1308
+ }
1309
+ function createInput(value) {
1310
+ if (!isRecord(value)) throw new BadRequest("'input' must be a JSON object");
1311
+ const input = {
1312
+ dataType: str(value["dataType"], "input.dataType"),
1313
+ displayName: str(value["displayName"], "input.displayName")
1314
+ };
1315
+ if (value["properties"] !== void 0 && value["properties"] !== null) input.properties = properties(value["properties"], "input.properties");
1316
+ return input;
1317
+ }
1318
+ function fail(status, message, body = null) {
1319
+ return {
1320
+ status,
1321
+ body: { error: {
1322
+ status,
1323
+ message,
1324
+ body
1325
+ } }
1326
+ };
1327
+ }
1328
+ function call(client, method, p) {
1329
+ const entityType = str(p["entityType"], "entityType");
1330
+ switch (method) {
1331
+ case "readField": return client.readField(entityType, str(p["field"], "field"), optionalNum(p["projectId"], "projectId"));
1332
+ case "readRawFields": return client.readRawFields(entityType, optionalNum(p["projectId"], "projectId"));
1333
+ case "createField": return client.createField(entityType, createInput(p["input"]));
1334
+ case "updateField": return client.updateField(entityType, str(p["field"], "field"), properties(p["properties"], "properties"), optionalNum(p["projectId"], "projectId"));
1335
+ case "retireField": return client.retireField(entityType, str(p["field"], "field")).then(() => null);
1336
+ case "reviveField": return client.reviveField(entityType, str(p["field"], "field"));
1337
+ }
1338
+ }
1339
+ /**
1340
+ * The server half of `ProxySchemaClient`, in the protocol of `createProxyHandler`.
1341
+ * It answers 404 to a method that is not a schema one, so a host routes on
1342
+ * `isSchemaProxyMethod`, or hands the schema client to `createProxyHandler`.
1343
+ *
1344
+ * These calls change the site's schema for everyone: expose them only to the
1345
+ * people who may.
1346
+ */
1347
+ function createSchemaProxyHandler(client) {
1348
+ return async function handle(method, body) {
1349
+ if (!isSchemaProxyMethod(method)) return fail(404, `Unknown method '${method}'`);
1350
+ if (body !== void 0 && body !== null && !isRecord(body)) return fail(400, "Body must be a JSON object");
1351
+ try {
1352
+ return {
1353
+ status: 200,
1354
+ body: { data: await call(client, method, body ?? {}) }
1355
+ };
1356
+ } catch (error) {
1357
+ if (error instanceof BadRequest) return fail(400, error.message);
1358
+ if (error instanceof SgApiError) return fail(error.status, error.message, error.body);
1359
+ return fail(500, error instanceof Error ? error.message : String(error));
1360
+ }
1361
+ };
1362
+ }
1363
+ /** A `SchemaClient` over the host app's own endpoints: one POST per method under `basePath`. */
1364
+ var ProxySchemaClient = class {
1365
+ options;
1366
+ base;
1367
+ fetchFn;
1368
+ constructor(options) {
1369
+ this.options = options;
1370
+ this.base = options.basePath.replace(/\/+$/, "");
1371
+ this.fetchFn = options.fetch ?? globalThis.fetch.bind(globalThis);
1372
+ }
1373
+ async post(method, body) {
1374
+ const supplied = typeof this.options.headers === "function" ? await this.options.headers() : this.options.headers ?? {};
1375
+ const res = await this.fetchFn(`${this.base}/${method}`, {
1376
+ method: "POST",
1377
+ headers: {
1378
+ Accept: "application/json",
1379
+ "Content-Type": "application/json",
1380
+ ...supplied
1381
+ },
1382
+ body: JSON.stringify(body)
1383
+ });
1384
+ const text = await res.text();
1385
+ let payload = null;
1386
+ try {
1387
+ payload = text ? JSON.parse(text) : null;
1388
+ } catch {
1389
+ throw new SchemaApiError(res.status, text, `Proxy returned a non-JSON body for '${method}'`);
1390
+ }
1391
+ const envelope = isRecord(payload) ? payload : {};
1392
+ const error = envelope["error"];
1393
+ if (isRecord(error) && typeof error["status"] === "number" && typeof error["message"] === "string") throw new SchemaApiError(error["status"], error["body"] ?? null, error["message"]);
1394
+ if (!res.ok) throw new SchemaApiError(res.status, payload, `Proxy error ${res.status} for '${method}'`);
1395
+ return envelope["data"] ?? null;
1396
+ }
1397
+ readField(entityType, field, projectId) {
1398
+ return this.post("readField", {
1399
+ entityType,
1400
+ field,
1401
+ projectId: projectId ?? null
1402
+ });
1403
+ }
1404
+ readRawFields(entityType, projectId) {
1405
+ return this.post("readRawFields", {
1406
+ entityType,
1407
+ projectId: projectId ?? null
1408
+ });
1409
+ }
1410
+ createField(entityType, input) {
1411
+ return this.post("createField", {
1412
+ entityType,
1413
+ input
1414
+ });
1415
+ }
1416
+ updateField(entityType, field, properties, projectId) {
1417
+ return this.post("updateField", {
1418
+ entityType,
1419
+ field,
1420
+ properties,
1421
+ projectId: projectId ?? null
1422
+ });
1423
+ }
1424
+ async retireField(entityType, field) {
1425
+ await this.post("retireField", {
1426
+ entityType,
1427
+ field
1428
+ });
1429
+ }
1430
+ reviveField(entityType, field) {
1431
+ return this.post("reviveField", {
1432
+ entityType,
1433
+ field
1434
+ });
1435
+ }
1436
+ };
1437
+ //#endregion
1438
+ export { supportsOperator as $, group as A, DATA_TYPES as B, statusFieldFor as C, condition as D, pluralPath as E, toFilterArray as F, VALUE_SHAPE as G, OPERATORS as H, walk as I, isLinkType as J, isDataType as K, CUSTOM_FIELD_PREFIX as L, isEmptyFilter as M, referencedPaths as N, emptyFilter as O, toApi3Hash as P, operatorsFor as Q, hasNonAscii as R, normalizeFields as S, undeclaredField as T, OPERATORS_BY_TYPE as U, NEGATING_OPERATORS as V, TIME_UNITS as W, isOperator as X, isNumericType as Y, isTemporalType as Z, DISPLAY_NAME_FIELDS as _, createSchemaProxyHandler as a, fieldSchemaOverride as b, toSchemaProperties as c, SgApiError as d, batchResults as f, normalizeHierarchyNode as g, normalizeEventLogEntry as h, SchemaApiError as i, isBlankCondition as j, fromWire as k, EVENT_LOG_FIELDS as l, hierarchyEntity as m, RestSchemaClient as n, fieldFromRaw as o, eventLogFilters as p, isFilterable as q, SCHEMA_PROXY_METHODS as r, isSchemaProxyMethod as s, ProxySchemaClient as t, RestClient as u, FIELD_SCHEMA_OVERRIDES as v, statusFieldNameFor as w, normalizeField as x, displayNameOf as y, predictFieldName as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sg-widgets-core",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Headless, framework-agnostic model of Flow Production Tracking (ShotGrid) fields, filters, statuses and a client adapter, for building widgets.",
5
5
  "keywords": [
6
6
  "shotgrid",