@get-dx/cli 0.1.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,596 @@
1
+ import { Command } from "commander";
2
+ import { createExampleText, getContext, parsePositiveIntOption, wrapAction, } from "../../commandHelpers.js";
3
+ import { CliError, EXIT_CODES, HttpError } from "../../errors.js";
4
+ import { request } from "../../http.js";
5
+ import { renderJson } from "../../renderers.js";
6
+ import { buildRuntime } from "../../runtime.js";
7
+ import { getEntityType } from "./entityTypes.js";
8
+ import { renderEntity, renderEntityDeleted, renderEntityList, renderEntityScorecardList, renderEntityTaskList, } from "./entitiesRendering.js";
9
+ import * as ui from "../../ui.js";
10
+ export function entitiesCommand() {
11
+ const entities = new Command()
12
+ .name("entities")
13
+ .description("Manage entities");
14
+ entities
15
+ .command("create")
16
+ .description("Create a new entity in your software catalog")
17
+ .option("--type <type>", "Entity type identifier (required)")
18
+ .option("--identifier <identifier>", "Unique identifier for the new entity (required)")
19
+ .option("--name <name>", "Display name of the entity")
20
+ .option("--description <desc>", "Description of the entity")
21
+ .option("--owner-team-ids <ids>", "Comma-separated owner team IDs")
22
+ .option("--owner-user-ids <ids>", "Comma-separated owner user IDs")
23
+ .option("--property <kv>", "Set a property value as key=value. Repeat for multiple properties.", (val, prev) => [...prev, val], [])
24
+ .addHelpText("afterAll", createExampleText([
25
+ {
26
+ label: "Create a new service entity",
27
+ command: "dx catalog entities create --type service --identifier my-service",
28
+ },
29
+ {
30
+ label: "Create with a name and return as JSON",
31
+ command: 'dx catalog entities create --type service --identifier my-service --name "My Service" --json',
32
+ },
33
+ {
34
+ label: "Create and assign owner teams",
35
+ command: "dx catalog entities create --type service --identifier my-service --owner-team-ids MzI1NTA,MzI1NTk",
36
+ },
37
+ {
38
+ label: "Create with properties",
39
+ command: 'dx catalog entities create --type service --identifier my-service --property tier=Tier-1 --property "languages=Ruby,TypeScript"',
40
+ },
41
+ ]))
42
+ .action(wrapAction(async (options, command) => {
43
+ if (!options.identifier) {
44
+ throw new CliError("--identifier is required", EXIT_CODES.ARGUMENT_ERROR);
45
+ }
46
+ if (!options.type) {
47
+ throw new CliError("--type is required", EXIT_CODES.ARGUMENT_ERROR);
48
+ }
49
+ const runtime = buildRuntime(getContext(command));
50
+ const properties = await resolvePropertiesForEntityType(runtime, options.type, options.property);
51
+ const response = await createEntity(runtime, options.identifier, {
52
+ type: options.type,
53
+ name: options.name,
54
+ description: options.description,
55
+ owner_team_ids: options.ownerTeamIds
56
+ ?.split(",")
57
+ .map((s) => s.trim()),
58
+ owner_user_ids: options.ownerUserIds
59
+ ?.split(",")
60
+ .map((s) => s.trim()),
61
+ properties,
62
+ });
63
+ if (runtime.context.json) {
64
+ renderJson(response);
65
+ }
66
+ else {
67
+ renderEntity(response.entity);
68
+ }
69
+ }));
70
+ entities
71
+ .command("delete")
72
+ .description("Delete an entity from your software catalog")
73
+ .argument("<identifier>", "Entity identifier")
74
+ .addHelpText("afterAll", createExampleText([
75
+ {
76
+ label: "Delete the `login-frontend` entity",
77
+ command: "dx catalog entities delete login-frontend",
78
+ },
79
+ {
80
+ label: "Delete an entity and return JSON",
81
+ command: "dx catalog entities delete login-frontend --json",
82
+ },
83
+ ]))
84
+ .action(wrapAction(async (identifier, _options, command) => {
85
+ const runtime = buildRuntime(getContext(command));
86
+ const response = await deleteEntity(runtime, identifier);
87
+ if (runtime.context.json) {
88
+ renderJson(response);
89
+ }
90
+ else {
91
+ renderEntityDeleted(response.entity);
92
+ }
93
+ }));
94
+ entities
95
+ .command("update")
96
+ .description("Update an entity in your software catalog")
97
+ .argument("<identifier>", "Entity identifier")
98
+ .option("--name <name>", "Display name of the entity")
99
+ .option("--description <desc>", "Description of the entity")
100
+ .option("--owner-team-ids <ids>", "Comma-separated owner team IDs")
101
+ .option("--owner-user-ids <ids>", "Comma-separated owner user IDs")
102
+ .option("--property <kv>", "Set a property value as key=value. Repeat for multiple properties. Use value null to remove a property.", (val, prev) => [...prev, val], [])
103
+ .addHelpText("afterAll", createExampleText([
104
+ {
105
+ label: "Rename an entity",
106
+ command: 'dx catalog entities update my-service --name "My Service"',
107
+ },
108
+ {
109
+ label: "Update owners and return JSON",
110
+ command: "dx catalog entities update my-service --owner-team-ids MzI1NTA,MzI1NTk --json",
111
+ },
112
+ {
113
+ label: "Update properties",
114
+ command: 'dx catalog entities update my-service --property tier=Tier-1 --property "languages=Ruby,TypeScript"',
115
+ },
116
+ ]))
117
+ .action(wrapAction(async (identifier, options, command) => {
118
+ const runtime = buildRuntime(getContext(command));
119
+ const properties = await resolvePropertiesForExistingEntity(runtime, identifier, options.property);
120
+ const response = await updateEntity(runtime, identifier, {
121
+ ...getEntityMutationOptionValues(options),
122
+ properties,
123
+ });
124
+ if (runtime.context.json) {
125
+ renderJson(response);
126
+ }
127
+ else {
128
+ renderEntity(response.entity, `${ui.success(ui.GLYPHS.CHECK)} Entity updated`);
129
+ }
130
+ }));
131
+ entities
132
+ .command("upsert")
133
+ .description("Create a new entity in your software catalog, or update it if it already exists")
134
+ .option("--type <type>", "Entity type identifier (required)")
135
+ .option("--identifier <identifier>", "Unique identifier for the entity (required)")
136
+ .option("--name <name>", "Display name of the entity")
137
+ .option("--description <desc>", "Description of the entity")
138
+ .option("--owner-team-ids <ids>", "Comma-separated owner team IDs")
139
+ .option("--owner-user-ids <ids>", "Comma-separated owner user IDs")
140
+ .option("--property <kv>", "Set a property value as key=value. Repeat for multiple properties. Use value null to remove a property.", (val, prev) => [...prev, val], [])
141
+ .addHelpText("afterAll", createExampleText([
142
+ {
143
+ label: "Create a new service entity if it does not exist",
144
+ command: "dx catalog entities upsert --type service --identifier my-service --name 'My Service'",
145
+ },
146
+ {
147
+ label: "Update owners and return JSON",
148
+ command: "dx catalog entities upsert --type service --identifier my-service --owner-team-ids MzI1NTA,MzI1NTk --json",
149
+ },
150
+ {
151
+ label: "Set properties while preserving omitted fields",
152
+ command: 'dx catalog entities upsert --type service --identifier my-service --property tier=Tier-1 --property "languages=Ruby,TypeScript"',
153
+ },
154
+ ]))
155
+ .action(wrapAction(async (options, command) => {
156
+ if (!options.type) {
157
+ throw new CliError("--type is required", EXIT_CODES.ARGUMENT_ERROR);
158
+ }
159
+ if (!options.identifier) {
160
+ throw new CliError("--identifier is required", EXIT_CODES.ARGUMENT_ERROR);
161
+ }
162
+ const runtime = buildRuntime(getContext(command));
163
+ const properties = await resolvePropertiesForEntityType(runtime, options.type, options.property);
164
+ const response = await upsertEntity(runtime, options.identifier, {
165
+ type: options.type,
166
+ ...getEntityMutationOptionValues(options),
167
+ properties,
168
+ });
169
+ if (runtime.context.json) {
170
+ renderJson(response);
171
+ }
172
+ else {
173
+ const title = response.result === "created_new_entity"
174
+ ? `${ui.success(ui.GLYPHS.CHECK)} Created new entity`
175
+ : `${ui.success(ui.GLYPHS.CHECK)} Updated existing entity`;
176
+ renderEntity(response.entity, title);
177
+ }
178
+ }));
179
+ entities
180
+ .command("info")
181
+ .argument("<identifier>", "Entity identifier")
182
+ .option("--include <include>", "Show only these comma-separated sections: core, owners, properties, aliases")
183
+ .addHelpText("afterAll", createExampleText([
184
+ {
185
+ label: "Fetch info for the `login-frontend` entity",
186
+ command: "dx catalog entities info login-frontend",
187
+ },
188
+ {
189
+ label: "Fetch info and return as JSON",
190
+ command: "dx catalog entities info login-frontend --json",
191
+ },
192
+ {
193
+ label: "Fetch info but only include the `core` and `owners` sections",
194
+ command: "dx catalog entities info login-frontend --include core,owners",
195
+ },
196
+ ]))
197
+ .action(wrapAction(async (identifier, options, command) => {
198
+ const runtime = buildRuntime(getContext(command));
199
+ const response = await getEntity(runtime, identifier);
200
+ const processedResponse = processIncludes(response, options);
201
+ if (runtime.context.json) {
202
+ renderJson(processedResponse);
203
+ }
204
+ else {
205
+ renderEntity(processedResponse.entity);
206
+ }
207
+ }));
208
+ entities
209
+ .command("list")
210
+ .description("List entities from your software catalog")
211
+ .option("--cursor <cursor>", "Cursor for the next page of results")
212
+ .option("--limit <n>", "Max entities per page (default is 50)", (value) => parsePositiveIntOption(value, "--limit"))
213
+ .option("--type <type>", "Only include entities of this type")
214
+ .option("--search-term <term>", "Filter by name, identifier, squad owner name, and/or user owner name")
215
+ .option("--include <include>", "Show only these comma-separated sections: core, owners, properties, aliases")
216
+ .addHelpText("afterAll", createExampleText([
217
+ {
218
+ label: "List the first page of entities",
219
+ command: "dx catalog entities list",
220
+ },
221
+ {
222
+ label: "List with a limit and return JSON",
223
+ command: "dx catalog entities list --limit 10 --json",
224
+ },
225
+ {
226
+ label: "Search and fetch the next page",
227
+ command: "dx catalog entities list --search-term payment --cursor avsgf30ccan3",
228
+ },
229
+ {
230
+ label: "List and only include core and owners sections",
231
+ command: "dx catalog entities list --include core,owners",
232
+ },
233
+ ]))
234
+ .action(wrapAction(async (options, command) => {
235
+ const runtime = buildRuntime(getContext(command));
236
+ const response = await listEntities(runtime, {
237
+ cursor: options.cursor,
238
+ limit: options.limit,
239
+ type: options.type,
240
+ search_term: options.searchTerm,
241
+ });
242
+ const processedEntities = response.entities.map((entity) => processIncludes({ ok: true, entity: entity }, options)
243
+ .entity);
244
+ if (runtime.context.json) {
245
+ renderJson({ ...response, entities: processedEntities });
246
+ }
247
+ else {
248
+ renderEntityList(processedEntities, response.response_metadata?.next_cursor ?? null);
249
+ }
250
+ }));
251
+ entities
252
+ .command("tasks")
253
+ .description("Get outstanding initiative tasks for an entity")
254
+ .argument("<identifier>", "Entity identifier")
255
+ .option("--cursor <cursor>", "Cursor for the next page of results")
256
+ .option("--limit <n>", "Max tasks per page (default is 50)", (value) => parsePositiveIntOption(value, "--limit"))
257
+ .addHelpText("afterAll", createExampleText([
258
+ {
259
+ label: "Get tasks for the `login-frontend` entity",
260
+ command: "dx catalog entities tasks login-frontend",
261
+ },
262
+ {
263
+ label: "Get tasks and return as JSON",
264
+ command: "dx catalog entities tasks login-frontend --json",
265
+ },
266
+ {
267
+ label: "Fetch the next page of tasks",
268
+ command: "dx catalog entities tasks login-frontend --cursor xuvkgfq9t0ty",
269
+ },
270
+ ]))
271
+ .action(wrapAction(async (identifier, options, command) => {
272
+ const runtime = buildRuntime(getContext(command));
273
+ const response = await getEntityTasks(runtime, identifier, {
274
+ cursor: options.cursor,
275
+ limit: options.limit,
276
+ });
277
+ if (runtime.context.json) {
278
+ renderJson(response);
279
+ }
280
+ else {
281
+ renderEntityTaskList(response.tasks, response.response_metadata?.next_cursor ?? null);
282
+ }
283
+ }));
284
+ entities
285
+ .command("scorecards")
286
+ .description("Get the current scorecard report for an entity")
287
+ .argument("<identifier>", "Entity identifier")
288
+ .option("--cursor <cursor>", "Cursor for the next page of results")
289
+ .option("--limit <n>", "Max scorecards per page (default is 50)", (value) => parsePositiveIntOption(value, "--limit"))
290
+ .addHelpText("afterAll", createExampleText([
291
+ {
292
+ label: "Get scorecards for the `login-frontend` entity",
293
+ command: "dx catalog entities scorecards login-frontend",
294
+ },
295
+ {
296
+ label: "Get scorecards and return as JSON",
297
+ command: "dx catalog entities scorecards login-frontend --json",
298
+ },
299
+ {
300
+ label: "Fetch the next page of scorecards",
301
+ command: "dx catalog entities scorecards login-frontend --cursor xuvkgfq9t0ty",
302
+ },
303
+ ]))
304
+ .action(wrapAction(async (identifier, options, command) => {
305
+ const runtime = buildRuntime(getContext(command));
306
+ const response = await getEntityScorecards(runtime, identifier, {
307
+ cursor: options.cursor,
308
+ limit: options.limit,
309
+ });
310
+ if (runtime.context.json) {
311
+ renderJson(response);
312
+ }
313
+ else {
314
+ renderEntityScorecardList(response.scorecards, response.response_metadata?.next_cursor ?? null);
315
+ }
316
+ }));
317
+ return entities;
318
+ }
319
+ async function getEntity(runtime, identifier) {
320
+ const response = await request(runtime, "/catalog.entities.info", {
321
+ method: "GET",
322
+ query: { identifier },
323
+ });
324
+ return response.body;
325
+ }
326
+ async function listEntities(runtime, params) {
327
+ const query = {};
328
+ if (params.cursor !== undefined)
329
+ query.cursor = params.cursor;
330
+ if (params.limit !== undefined)
331
+ query.limit = params.limit;
332
+ if (params.type !== undefined)
333
+ query.type = params.type;
334
+ if (params.search_term !== undefined)
335
+ query.search_term = params.search_term;
336
+ const response = await request(runtime, "/catalog.entities.list", {
337
+ method: "GET",
338
+ query,
339
+ });
340
+ return response.body;
341
+ }
342
+ async function createEntity(runtime, identifier, params) {
343
+ const response = await request(runtime, "/catalog.entities.create", {
344
+ method: "POST",
345
+ body: buildEntityMutationBody(identifier, params),
346
+ });
347
+ return response.body;
348
+ }
349
+ async function updateEntity(runtime, identifier, params) {
350
+ const response = await request(runtime, "/catalog.entities.update", {
351
+ method: "POST",
352
+ body: buildEntityMutationBody(identifier, params),
353
+ });
354
+ return response.body;
355
+ }
356
+ async function deleteEntity(runtime, identifier) {
357
+ const response = await request(runtime, "/catalog.entities.delete", {
358
+ method: "POST",
359
+ query: { identifier },
360
+ });
361
+ return response.body;
362
+ }
363
+ async function upsertEntity(runtime, identifier, params) {
364
+ const response = await request(runtime, "/catalog.entities.upsert", {
365
+ method: "POST",
366
+ body: buildEntityMutationBody(identifier, params),
367
+ });
368
+ return response.body;
369
+ }
370
+ async function getEntityScorecards(runtime, identifier, params) {
371
+ const query = { identifier };
372
+ if (params.cursor !== undefined)
373
+ query.cursor = params.cursor;
374
+ if (params.limit !== undefined)
375
+ query.limit = params.limit;
376
+ const response = await request(runtime, "/catalog.entities.scorecards", {
377
+ method: "GET",
378
+ query,
379
+ });
380
+ return response.body;
381
+ }
382
+ async function getEntityTasks(runtime, identifier, params) {
383
+ const query = { identifier };
384
+ if (params.cursor !== undefined)
385
+ query.cursor = params.cursor;
386
+ if (params.limit !== undefined)
387
+ query.limit = params.limit;
388
+ const response = await request(runtime, "/catalog.entities.tasks", {
389
+ method: "GET",
390
+ query,
391
+ });
392
+ return response.body;
393
+ }
394
+ // --- Property parsing helpers ---
395
+ export function parseEntityProperties(entityTypeIdentifier, rawProperties, knownProperties) {
396
+ const propMap = new Map(knownProperties.map((p) => [p.identifier, p]));
397
+ const result = {};
398
+ for (const kv of rawProperties) {
399
+ const eqIndex = kv.indexOf("=");
400
+ if (eqIndex === -1) {
401
+ throw new CliError(`Invalid --property "${kv}": expected format key=value`, EXIT_CODES.ARGUMENT_ERROR);
402
+ }
403
+ const key = kv.slice(0, eqIndex);
404
+ const rawValue = kv.slice(eqIndex + 1);
405
+ const prop = propMap.get(key);
406
+ if (!prop) {
407
+ const availableProperties = Array.from(propMap.keys())
408
+ .sort()
409
+ .map((k) => `\`${k}\``)
410
+ .join(", ");
411
+ const availablePropertiesText = availableProperties.length > 0 ? availableProperties : "(none)";
412
+ throw new CliError(`Unknown property \`${key}\` for entity type \`${entityTypeIdentifier}\`. Available properties: ${availablePropertiesText}`, EXIT_CODES.ARGUMENT_ERROR);
413
+ }
414
+ result[key] = parsePropertyValue(prop, rawValue);
415
+ }
416
+ return result;
417
+ }
418
+ export function parsePropertyValue(prop, rawValue) {
419
+ if (rawValue === "null")
420
+ return null;
421
+ switch (prop.type) {
422
+ case "text":
423
+ case "url":
424
+ case "date":
425
+ case "email":
426
+ case "slack_channel":
427
+ case "msteams_channel":
428
+ case "select":
429
+ case "user":
430
+ return rawValue;
431
+ case "number": {
432
+ const n = Number(rawValue);
433
+ if (isNaN(n)) {
434
+ throw new CliError(`Invalid number value for property "${prop.identifier}": "${rawValue}"`, EXIT_CODES.ARGUMENT_ERROR);
435
+ }
436
+ return n;
437
+ }
438
+ case "boolean":
439
+ if (rawValue === "true")
440
+ return true;
441
+ if (rawValue === "false")
442
+ return false;
443
+ throw new CliError(`Invalid boolean value for property "${prop.identifier}": expected "true" or "false", got "${rawValue}"`, EXIT_CODES.ARGUMENT_ERROR);
444
+ case "multi_select":
445
+ case "list":
446
+ return rawValue.split(",").map((s) => s.trim());
447
+ case "json":
448
+ case "openapi":
449
+ try {
450
+ return JSON.parse(rawValue);
451
+ }
452
+ catch {
453
+ throw new CliError(`Invalid JSON value for property "${prop.identifier}": ${rawValue}`, EXIT_CODES.ARGUMENT_ERROR);
454
+ }
455
+ case "computed":
456
+ case "file_matching_rule":
457
+ throw new CliError(`Property "${prop.identifier}" is read-only (type: ${prop.type}) and cannot be set`, EXIT_CODES.ARGUMENT_ERROR);
458
+ default:
459
+ return rawValue;
460
+ }
461
+ }
462
+ // --- Include helpers ---
463
+ const ENTITY_INCLUDE_SECTIONS = [
464
+ "core",
465
+ "owners",
466
+ "properties",
467
+ "aliases",
468
+ ];
469
+ function processIncludes(response, options) {
470
+ const includeSections = parseIncludeSections(options.include);
471
+ if (includeSections.length === 0) {
472
+ return response;
473
+ }
474
+ const sections = uniqueSectionsInOrder(includeSections);
475
+ const entity = redactEntityByInclude(response.entity, sections);
476
+ return { ok: true, entity: entity };
477
+ }
478
+ function parseIncludeSections(include) {
479
+ if (!include) {
480
+ return [];
481
+ }
482
+ const results = include.split(",");
483
+ for (const result of results) {
484
+ if (!ENTITY_INCLUDE_SECTIONS.includes(result)) {
485
+ throw new CliError(`Invalid --include "${result}". Expected one of: ${ENTITY_INCLUDE_SECTIONS.join(", ")}`, EXIT_CODES.ARGUMENT_ERROR);
486
+ }
487
+ }
488
+ return results;
489
+ }
490
+ async function resolvePropertiesForEntityType(runtime, entityTypeIdentifier, rawProperties) {
491
+ if (rawProperties.length === 0) {
492
+ return undefined;
493
+ }
494
+ let entityTypeResponse;
495
+ try {
496
+ entityTypeResponse = await getEntityType(runtime, entityTypeIdentifier);
497
+ }
498
+ catch (err) {
499
+ throw wrapEntityTypeLookupError(entityTypeIdentifier, err);
500
+ }
501
+ return parseEntityProperties(entityTypeIdentifier, rawProperties, entityTypeResponse.entity_type.properties);
502
+ }
503
+ async function resolvePropertiesForExistingEntity(runtime, identifier, rawProperties) {
504
+ if (rawProperties.length === 0) {
505
+ return undefined;
506
+ }
507
+ let entityResponse;
508
+ try {
509
+ entityResponse = await getEntity(runtime, identifier);
510
+ }
511
+ catch (err) {
512
+ const exitCode = err instanceof HttpError && err.status !== undefined && err.status < 500
513
+ ? EXIT_CODES.ARGUMENT_ERROR
514
+ : EXIT_CODES.RETRY_RECOMMENDED;
515
+ throw new CliError(`Failed to fetch entity "${identifier}" to resolve property types: ${err instanceof Error ? err.message : String(err)}`, exitCode);
516
+ }
517
+ return resolvePropertiesForEntityType(runtime, entityResponse.entity.type, rawProperties);
518
+ }
519
+ function getEntityMutationOptionValues(options) {
520
+ return {
521
+ name: options.name,
522
+ description: options.description,
523
+ owner_team_ids: parseCommaSeparatedIds(options.ownerTeamIds),
524
+ owner_user_ids: parseCommaSeparatedIds(options.ownerUserIds),
525
+ };
526
+ }
527
+ function parseCommaSeparatedIds(value) {
528
+ if (value === undefined) {
529
+ return undefined;
530
+ }
531
+ const parsed = value
532
+ .split(",")
533
+ .map((entry) => entry.trim())
534
+ .filter((entry) => entry.length > 0);
535
+ return parsed.length > 0 ? parsed : undefined;
536
+ }
537
+ function buildEntityMutationBody(identifier, params) {
538
+ const body = { identifier };
539
+ if ("type" in params && params.type !== undefined) {
540
+ body.type = params.type;
541
+ }
542
+ if (params.name !== undefined)
543
+ body.name = params.name;
544
+ if (params.description !== undefined)
545
+ body.description = params.description;
546
+ if (params.owner_team_ids?.length)
547
+ body.owner_team_ids = params.owner_team_ids;
548
+ if (params.owner_user_ids?.length)
549
+ body.owner_user_ids = params.owner_user_ids;
550
+ if (params.properties !== undefined)
551
+ body.properties = params.properties;
552
+ return body;
553
+ }
554
+ function wrapEntityTypeLookupError(entityTypeIdentifier, err) {
555
+ const exitCode = err instanceof HttpError && err.status !== undefined && err.status < 500
556
+ ? EXIT_CODES.ARGUMENT_ERROR
557
+ : EXIT_CODES.RETRY_RECOMMENDED;
558
+ return new CliError(`Failed to fetch entity type "${entityTypeIdentifier}" to resolve property types: ${err instanceof Error ? err.message : String(err)}`, exitCode);
559
+ }
560
+ function uniqueSectionsInOrder(sections) {
561
+ const seen = new Set();
562
+ const ordered = [];
563
+ for (const s of ENTITY_INCLUDE_SECTIONS) {
564
+ if (sections.includes(s) && !seen.has(s)) {
565
+ seen.add(s);
566
+ ordered.push(s);
567
+ }
568
+ }
569
+ return ordered;
570
+ }
571
+ function redactEntityByInclude(entity, include) {
572
+ const SECTION_KEYS = {
573
+ core: [
574
+ "identifier",
575
+ "name",
576
+ "type",
577
+ "created_at",
578
+ "updated_at",
579
+ "description",
580
+ "domain",
581
+ ],
582
+ owners: ["owner_teams", "owner_users"],
583
+ properties: ["properties"],
584
+ aliases: ["aliases"],
585
+ };
586
+ const sections = uniqueSectionsInOrder(include);
587
+ const out = {};
588
+ for (const section of sections) {
589
+ for (const key of SECTION_KEYS[section]) {
590
+ if (key in entity) {
591
+ out[key] = entity[key];
592
+ }
593
+ }
594
+ }
595
+ return out;
596
+ }